Android插件化探索免安装运行Activity(上)

Posted maplejaw_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android插件化探索免安装运行Activity(上)相关的知识,希望对你有一定的参考价值。

转载请注明本文出自maplejaw的博客http://blog.csdn.net/maplejaw_

【Android插件化探索(一)类加载器DexClassLoader】
【Android插件化探索(二)资源加载】

前情提要

在上一篇中有一个细节没有提到,那就是getResourcesForApplication和AssetManager的区别。

getResourcesForApplication

getResourcesForApplication(String packageName),很显然需要传入一个包名,换言之,这个插件必须已经被安装在系统内,然后才能通过包名来获取资源。你可能会想,不安装照样可以获取包名啊。的确,通过pm.getPackageArchiveInfo()可以获取安装包信息。但是,这些包都是没有在PMS中注册的。如果仍然这样获取,会提示如下错误。

android.content.pm.PackageManager$NameNotFoundException: com.maplejaw.hotplugin

现在我们就从源码角度来分析getResourcesForApplication。源码在ApplicationPackageManager中,如下:

    @Override
    public Resources getResourcesForApplication(String appPackageName)
            throws NameNotFoundException {
        return getResourcesForApplication(
            getApplicationInfo(appPackageName, sDefaultFlags));
    }

可以看出内部调用了重载方法。getApplicationInfo返回的是ApplicationInfo对象。

    @Override
    public Resources getResourcesForApplication(@NonNull ApplicationInfo app){

        //...
        //省略了部分源码
        final Resources r = mContext.mMainThread.getTopLevelResources(
                sameUid ? app.sourceDir : app.publicSourceDir,
                sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs,
                app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
                null, mContext.mPackageInfo);
        if (r != null) {
            return r;
        }

    }

最终走的仍旧是ActivityThread的getTopLevelResources,ActivityThread里面的相关源码我就不分析了,跟上一篇是一样的也是调用ResourcesManager中的getTopLevelResources,这里不做赘述。

现在我们主要来看看getApplicationInfo里面做了什么?

  @Override
public ApplicationInfo getApplicationInfo(String packageName, int flags) throws NameNotFoundException {
      ApplicationInfo ai =   mPM.getApplicationInfo(packageName, flags, mContext.getUserId());
        //...
        //省略了部分源码
        throw new NameNotFoundException(packageName);
    }

mPM的初始化源码如下,可以看出是一个PMS(PackageManagerService)对象。

    public static IPackageManager getPackageManager() {
        if (sPackageManager != null) {
            return sPackageManager;
        }
        IBinder b = ServiceManager.getService("package");
        sPackageManager = IPackageManager.Stub.asInterface(b);
        return sPackageManager;
    }

继续深究,找出PMS中相关源码。

@Override
    public ApplicationInfo getApplicationInfo(String packageName, int flags, int userId) {
        if (!sUserManager.exists(userId)) return null;
        enforceCrossUserPermission(Binder.getCallingUid(), userId, false, "get application info");
        // writer
        synchronized (mPackages) {
            PackageParser.Package p = mPackages.get(packageName);
            if (DEBUG_PACKAGE_INFO) Log.v(
                    TAG, "getApplicationInfo " + packageName
                    + ": " + p);
            if (p != null) {
                PackageSetting ps = mSettings.mPackages.get(packageName);
                if (ps == null) return null;
                // Note: isEnabledLP() does not apply here - always return info
                return PackageParser.generateApplicationInfo(
                        p, flags, ps.readUserState(userId), userId);
            }
            if ("android".equals(packageName)||"system".equals(packageName)) {
                return mAndroidApplication;
            }
            if ((flags & PackageManager.GET_UNINSTALLED_PACKAGES) != 0) {
                return generateApplicationInfoFromSettingsLPw(packageName, flags, userId);
            }
        }
        return null;
    }

可以看出会去mPackages中找,然而根本就找不到,因为根本就没有安装。

AssetManager

AssetManager这里就不做赘述了,上一篇已经简单看过,可以直接指定目录。换言之,也就更加灵活。

从上面可以看出,AssetManager比getResourcesForApplication要灵活很多,使用场景也更广。

免安装运行Activity(上)

看完了前面的部分,我们知道可以通过DexClassLoader来加载类,通过AssetManager可以来加载资源。可是现在问题来了,怎么运行一个未安装APK中的Activity?Activity不仅有类有资源,最最重要的是,它有生命!。

我们先来看看按照之前的写法会发生什么状况吧,首先在插件的PluginClass中加入启动Activity的代码如下:

    public void startPluginActivity(Context context, Class<?> cls) {
        Intent intent=new Intent(context,cls);
        context.startActivity(intent);
    }

  public void startPluginActivity(Context context) {
        Intent intent=new Intent(context,PluginActivity.class);
        context.startActivity(intent);
    }

然后修改核心测试代码,分别测试两种形式。


    private void useDexClassLoader(String path){
        loadResources(path);
        File codeDir=getDir("dex", Context.MODE_PRIVATE);

        //创建类加载器,把dex加载到虚拟机中
        ClassLoader classLoader = new DexClassLoader(path,codeDir.getAbsolutePath() ,null,
                this.getClass().getClassLoader());
        //获得包管理器
        PackageManager pm = getPackageManager();
        PackageInfo packageInfo=pm.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES);
        String packageName=packageInfo.packageName;

        try {
            Class<?> clazz = classLoader.loadClass(packageName+".PluginClass");
            Comm obj = (Comm) clazz.newInstance();
            obj.startPluginActivity(this,classLoader.loadClass(packageName+".PluginActivity"));
          //  obj.startPluginActivity(this);

        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

果然没有想象中那么轻松,直接报错提示。

 android.content.ActivityNotFoundException: Unable to find explicit activity class {com.maplejaw.hotfix/com.maplejaw.hotplugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?

提示找不到Activity,是否在AndroidManifest.xml中声明?说的也是,并没有在宿主APK中进行声明啊,插件APK的清单是没有效果的。于是怀着满满的自信在AndroidManifest.xml中加入声明。

  <activity android:name="com.maplejaw.hotplugin.PluginActivity"/>

笔者心想,这回应该可以了吧,再次运行测试。WTF!又报错。

java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.maplejaw.hotfix/com.maplejaw.hotplugin.PluginActivity}: java.lang.ClassNotFoundException: Didn't find class "com.maplejaw.hotplugin.PluginActivity" on path: DexPathList[[zip file "/data/app/com.maplejaw.hotfix-2/base.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]

从打印信息可以看出提示没有找到该类。这就奇怪了,明明可以找到PluginClass类,为什么提示找不到PluginActivity这个类呢?简直没有道理啊。

为了进行对比,笔者故意修改核心测试代码去加载一个不存在的PluginClass2类,看看有什么提示。

java.lang.ClassNotFoundException: Didn't find class "com.maplejaw.hotplugin.PluginClass2" on path: DexPathList[[zip file "/storage/emulated/0/2.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]

同样提示找不到该类。
但是!!!注意看DexPathList这里,它们指向的dex目录居然不一样。换言之,它们两个的ClassLoader不是同一个。
我们先不想其他问题,暂时不去研究startActivity的源码(下篇探索动态代理会进行研究)。我们先来想一个解决思路,有没有一种方法可以将dex目录指向到插件APK的dex?

替换ClassLoader

要更改dex目录指向谈何容易啊,更何况还要同时兼顾两个dex目录。幸亏ClassLoader遵循着双亲委托原则,让这一切变得不是特别困难。

还记得我们在第一篇DexClassLoader中提到过,一个BaseClassLoader对应一个DexPathList 吗?

public BaseDexClassLoader(String dexPath, File optimizedDirectory,String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

那么我们把启动Activity的那个ClassLoader替换成我们的,不就间接的改变了dex目录指向吗?你可能会担心,替换成我们的ClassLoader,那宿主APK中的类还找得到吗?由于双亲委托原则,会首先从父ClassLoader中去找,只要我们的父ClassLoader是默认的系统ClassLoader即可。

所以,我们现在的任务是要把ClassLoader替换掉,翻了翻源码,发现ClassLoader对象在LoadedApk中

LoadedApk

而ActivityThread中有着相关引用。

这里写图片描述

于是做了如下反射替换。

private void replaceClassLoader(ClassLoader dLoader,String resPath){
        try{
            String packageName = this.getPackageName();
            ClassLoader loader=ClassLoader.getSystemClassLoader();
            Class<?> loadApkCls =loader.loadClass("android.app.LoadedApk");
            Class<?> activityThreadCls =loader.loadClass("android.app.ActivityThread");

            //获取ActivityThread对象
            Method currentActivityThreadMethod=activityThreadCls.getMethod("currentActivityThread");
            Object currentActivityThread= currentActivityThreadMethod.invoke(null);
            //反射获取mPackages中的LoadedApk
            Field filed=activityThreadCls.getDeclaredField("mPackages");
            filed.setAccessible(true);
            Map mPackages= (Map) filed.get(currentActivityThread);
            WeakReference wr = (WeakReference) mPackages.get(packageName);
             //反射修改LoadedApk中的mClassLoader
            Field classLoaderFiled=loadApkCls.getDeclaredField("mClassLoader");
            classLoaderFiled.setAccessible(true);
            classLoaderFiled.set(wr.get(),dLoader);


        }catch(Exception e){
           e.printStackTrace();
        }

    }

插件Activity的代码如下:

public class PluginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_plugin);
        Log.i("JG",  "包名:"+getPackageName());
        Log.w("JG",  "代码路径:"+getPackageCodePath());
        Log.e("JG",  "资源路径:"+getPackageResourcePath());

    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d("JG","onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d("JG","onResume");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d("JG","onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d("JG","onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d("JG","onDestroy");
    }

运行测试通过,Activity是能启动了,生命周期完全正常,但是发现资源却完全加载不了,一片白(也有加载到宿主界面的,那是因为资源id刚好和插件的重复)!控制台打印信息如下:
这里写图片描述
可以看出代码路径和资源路径全部指向了宿主APK,即使使用loadResources也完全没有效果,因为一个Activity一个Context,我们的loadResources只对那个Activity的Context有效果。迫不得已,又去翻看了源码,最后在上面的反射基础中加入如下反射修改LoadedApk中的mResDir代码。

 //反射修改LoadedApk中的资源目录
            Field filed2=loadApkCls.getDeclaredField("mResDir");
            filed2.setAccessible(true);
            filed2.set(wr.get(),resPath);

测试,启动成功,加载出插件的界面。查看控制台,发现成功修改资源目录,生命周期完全正常。
image_1ajsv6rti1ocj1umj10un1kqc11b5l.png-34.1kB
但是呢,这种方法是有弊端的,因为反射导致它彻底改变了资源目录,假如你要回到宿主Activity还要重新切换目录才行。不由得想,要是资源也有双亲委托该有多好啊。

合并DexPathList

这种方式类似于热修复方案。将插件的dexElements插入到系统的dexElements中,这样我们启动Activity时就不会提示找不到该类。在第一篇中,我们简单看过DexPathList源码,现在再来回顾下。
首先,一个ClassLoader一个DexPathList。
这里写图片描述
然后,一个DexPathList中含有一个dexElements数组
这里写图片描述
最后,加载类时从dexElements数组中遍历。
这里写图片描述

好了,思路很清晰,通过反射,将插件的dexElements与宿主的合并,并赋值给宿主的dexPathList。
实现方案如下:

    private void combinePathList(ClassLoader loader){
        //获取系统的classloader
        PathClassLoader pathLoader = (PathClassLoader) getClassLoader();

        try {
            //反射dexpathlist
            Field pathListFiled = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
            pathListFiled.setAccessible(true);
            //反射dexElements
            Field dexElementsFiled=Class.forName("dalvik.system.DexPathList").getDeclaredField("dexElements");
            dexElementsFiled.setAccessible(true);
            //获取系统的pathList
            Object pathList1= pathListFiled.get(pathLoader);
            //获取系统的dexElements
            Object dexElements1=dexElementsFiled.get(pathList1);

            //获取插件的pathlist
            Object pathList2= pathListFiled.get(loader);
            //获取插件的dexElements
            Object dexElements2=dexElementsFiled.get(pathList2);
            //合并dexElements
            Object combineDexElements=combineArray(dexElements1,dexElements2);
            //设置给系统的dexpathlist
            dexElementsFiled.set(pathList1,combineDexElements);

        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }



   //合并两个数组,返回一个新数组
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }

测试通过,成功启动Activity。
但是,同样需要在清单文件注册,同样加载不了资源。仍然需要去反射替换掉LoadedApk中的资源目录。

源码下载地址:https://github.com/maplejaw/HotPluginDemo

最后

关于上面启动免安装Activity的方案,可以看出存在很明显的缺陷,首先,需要在清单文件提前注册,此外资源反射修改也很蛋疼。如果不想用反射,我们可以提前将资源内置于宿主中,或者纯用JAVA代码来写。

但是,这两种方案总归很麻烦。有没有更好的方案呢?没错,就是动态代理!
下一篇准备探索动态代理启动Activity。

以上是关于Android插件化探索免安装运行Activity(上)的主要内容,如果未能解决你的问题,请参考以下文章

又一开源项目爆火于GitHub,Android高级插件化强化实战

大厂 “ 青睐的 ” 插件化技术,实战效果简直起飞

Android 插件化

DroidPlugin插件化开发

Android 插件化开发:资源插件化

Android 插件化Hook 插件化框架 ( Hook Activity 启动流程 | 主线程创建 Activity 实例之前使用插件 Activity 类替换占位的组件 )