Android插件化探索(三)LoadedApk式插件化

4,921 阅读4分钟

1.前言

在前面讲述的Android插件化探索(二)hook式插件化中有优点也有缺点。优点就是插件中的Activity等组件不需要依赖宿主APP的环境,可以随意的使用thiscontext等上下文对象。但是缺点也非常明显,就是如果插件越多,内存中的dexElements数组就会越来越大,可能会造成内存溢出等异常。今天讲的LoadedApk式插件化就解决了Hook式插件化的这个缺点。

2.startActivity源码分析

我们还是从startActivity 开始分析。

startActivity() -->  Activity.startActivity() --> Activity.startActivityForResult() --> Instrumentation.execStartActivity() --> ActivityTaskManager.getService().startActivity(AMS检查)
ActivityThread.handleLaunchActivity() --> performLaunchActivity()(自己去处理LoadedApk中的ClassLoader)

我们知道,Activity 调用 startActivity 之后最后会在 ActivityThread 执行 handleLaunchActivity我们直接跟进这个方法查到我们的 Activity 初始化就是由 LoadedApk 里面的 mClassLoader进行加载的。由此得到我们的Hook点,我们如果自定定义一个LoadedApk 并把里面的 mClassLoader 给替换为我们自己的 ClassLoader 就可以加载插件的类了。由以下图中可以看出,我们在回调 handleLaunchActivity() 之前会通过 getPackageInfoNoCheck() 方法初始化我们的 LoadedApk 对象,并存放如全局的变量 mPackages 中,所以我们要创建一个插件的 LoadedApk 并且把它添加到 mPackages 这个集合中就行了。

我们简单画个图来简述一下该流程:

3.代码实现自定义LoadedApk

下面我们开始撸码实现自己的 LoadedApk,跟前面讲的 Hook 式插件化一下,我们也需要 Hook住AMS的检查和ActivityThreadHandle 的回调方法,所以我们在之前的代码上面改动,只是屏蔽掉了融合宿主和插件的 dexElements数组。没有看过Hook式插件化的小伙伴可以点击这里查看(Android插件化探索(二)hook式插件化)。

前面我们查看源码可以得到,ActivityThread 里面有个存放 LoadedApk 的集合为 mPackages,所以我们首先获取到这个变量,其次自定义我们的 LoadedApk,由于 LoadedApk是不对开发人员开放的,所以我们只能通过上面讲到的 getPackageInfoNoCheck() 方法来返回一个 LoadedApk 实例,接着拿到 自定义一个 ClassLoader,把 LoadedApk 里面的 ,,mClassLoader替换为我们自己定义的 ClassLoader。最后把我们自定义的 LoadedApk 存入 mPackages 这个集合中。

简单流程分析为:

1、反射获取 ActivityThreadmPackages

2、自定义一个 LoadedApk

3、自定义一个 ClassLoader

4、反射 LoadedApkmClassLoader,并将自定义的 ClassLoader 赋值给它

5、把自定义的 LoadedApk 存入 mPackages

下面我们跟着上面的流程一步一步的进行代码实现。

3.1 获取ActivityThread的mPackages

这个比较简单,我这里直接贴上代码了。

	//获取 ActivityThread 类
        Class<?> mActivityThreadClass = Class.forName("android.app.ActivityThread");
        //获取 ActivityThread 的 currentActivityThread() 方法
        Method currentActivityThread = mActivityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThread.setAccessible(true);
        //获取 ActivityThread 实例
        Object mActivityThread = currentActivityThread.invoke(null);

        //final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
        //获取 mPackages 属性
        Field mPackagesField = mActivityThreadClass.getDeclaredField("mPackages");
        mPackagesField.setAccessible(true);
        //获取 mPackages 属性的值
        ArrayMap<String, Object> mPackages = (ArrayMap<String, Object>) mPackagesField.get(mActivityThread);

3.2 自定义一个LoadedApk

自定义我们的 LoadedApk 比较复杂,我们着重分析一下这个。首先我们知道获取一个 LoadedApk 实例我们可以通过反射调用 getPackageInfoNoCheck(ApplicationInfo ai,CompatibilityInfo compatInfo) 方法。该方法要传递两个参数,第一个类型是 ApplicationInfo, 第一个类型是 CompatibilityInfo,我们如何能获取到这两个类的实例呢?

3.2.1 获取CompatibilityInfo实例

第二个参数比较容易获取,我们先易后难。跟进 CompatibilityInfo 这个类的源码可以得到,该类有一个静态变量 DEFAULT_COMPATIBILITY_INFO 就是它本身实例对象,我们可以通过反射来拿到这个变量,然后就解决第二个参数问题了。

3.2.2 获取ApplicationInfo实例

到第二个参数了,在之前第一篇文章中提过,源码中有一个PackageParser.java 类,里面有一个方法 generateApplicationInfo() 可以返回一个 ApplicationInfo 实例,我们可以通过反射这个方法来获取一个 ApplicationInfo 实例,但是方法中,需要传递三个参数,类型分别为 Package(该类是 PackageParser 的内部类),和int以及 PackageUserState。这三个类型的实例我们从何获取呢?

查看 PackageParser 源码发现可以通过 parsePackage() 方法可以返回一个 Package 实例,并且只需要传入我们插件的 File实例和一个int值就可以了。 这样第一个参数解决了,第三个参数我们可以通过反射获取类,然后执行 newInstance() 来获取一个实例,第二个参数我们直接传个0就好了,通过以上方法,我们就可以获取到一个 ApplicationInfo 实例了,如下:

 private ApplicationInfo getAppInfo(File file) throws Exception {
        /*
            执行此方法获取 ApplicationInfo
            public static ApplicationInfo generateApplicationInfo(Package p, int flags,PackageUserState state)
         */
        Class<?> mPackageParserClass = Class.forName("android.content.pm.PackageParser");
        Class<?> mPackageClass = Class.forName("android.content.pm.PackageParser$Package");
        Class<?> mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
        //获取 generateApplicationInfo 方法
        Method generateApplicationInfoMethod = mPackageParserClass.getDeclaredMethod("generateApplicationInfo",
                mPackageClass, int.class, mPackageUserStateClass);

        //创建 PackageParser 实例
        Object mPackageParser = mPackageParserClass.newInstance();

        //获取 Package 实例
        /*
            执行此方法获取一个 Package 实例
            public Package parsePackage(File packageFile, int flags)
         */
        //获取 parsePackage 方法
        Method parsePackageMethod = mPackageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);
        //执行 parsePackage 方法获取 Package 实例
        Object mPackage = parsePackageMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);

        //执行 generateApplicationInfo 方法,获取 ApplicationInfo 实例
        ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(null, mPackage, 0,
                mPackageUserStateClass.newInstance());
        //我们获取的 ApplicationInfo 默认路径是没有设置的,我们要自己设置
        // applicationInfo.sourceDir = 插件路径;
        // applicationInfo.publicSourceDir = 插件路径;
        applicationInfo.sourceDir = file.getAbsolutePath();
        applicationInfo.publicSourceDir = file.getAbsolutePath();
        return applicationInfo;
    }

3.2.3 获取LoadedApk实例

得到两个参数的实例之后,我们就可以获取一个 LoadedApk实例了,代码如下:

	//自定义一个 LoadedApk,系统是如何创建的我们就如何创建
        //执行下面的方法会返回一个 LoadedApk,我们就仿照系统执行此方法
        /*
              this.packageInfo = client.getPackageInfoNoCheck(activityInfo.applicationInfo,
                    compatInfo);
              public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,
                    int flags)
         */
        Class<?> mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
        Method getLoadedApkMethod = mActivityThreadClass.getDeclaredMethod("getPackageInfoNoCheck",
                ApplicationInfo.class, mCompatibilityInfoClass);

        /*
             public static final CompatibilityInfo DEFAULT_COMPATIBILITY_INFO = new CompatibilityInfo() {};
         */
        //以上注释是获取默认的 CompatibilityInfo 实例
        Field mCompatibilityInfoDefaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
        Object mCompatibilityInfo = mCompatibilityInfoDefaultField.get(null);

        //获取一个 ApplicationInfo实例
        ApplicationInfo applicationInfo = getAppInfo(file);
        //执行此方法,获取一个 LoadedApk
        Object mLoadedApk = getLoadedApkMethod.invoke(mActivityThread, applicationInfo, mCompatibilityInfo);

3.3 自定义一个ClassLoader

这个比较简单,前面文章也有提过,就不多讲了,直接上代码:

  	//自定义一个 ClassLoader
        String optimizedDirectory = context.getDir("plugin", Context.MODE_PRIVATE).getAbsolutePath();
        DexClassLoader classLoader = new DexClassLoader(file.getAbsolutePath(), optimizedDirectory,
                null, context.getClassLoader());

3.4 替换LoadedApk的mClassLoader为自定义的ClassLoader

这个也是很简单,直接上代码:

        //获取 LoadedApk 的 mClassLoader 属性
        Field mClassLoaderField = mLoadedApk.getClass().getDeclaredField("mClassLoader");
        mClassLoaderField.setAccessible(true);
        //设置自定义的 classLoader 到 mClassLoader 属性中
        mClassLoaderField.set(mLoadedApk, classLoader);

3.5 把自定义的LoadedApk存入mPackages中

 	WeakReference loadApkReference = new WeakReference(mLoadedApk);
        //添加自定义的 LoadedApk
        mPackages.put(applicationInfo.packageName, loadApkReference);
        //重新设置 mPackages
        mPackagesField.set(mActivityThread, mPackages);

通过上面的步骤,我们就成功的将自定义的 LoadedApk 存入 mPackages 中。完整的代码来一波:

	public void customLoadApkAction() throws Exception {
        File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "plugin2.apk");
        if (!file.exists()) {
            throw new FileNotFoundException("插件包不存在");
        }
        //获取 ActivityThread 类
        Class<?> mActivityThreadClass = Class.forName("android.app.ActivityThread");
        //获取 ActivityThread 的 currentActivityThread() 方法
        Method currentActivityThread = mActivityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThread.setAccessible(true);
        //获取 ActivityThread 实例
        Object mActivityThread = currentActivityThread.invoke(null);

        //final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
        //获取 mPackages 属性
        Field mPackagesField = mActivityThreadClass.getDeclaredField("mPackages");
        mPackagesField.setAccessible(true);
        //获取 mPackages 属性的值
        ArrayMap<String, Object> mPackages = (ArrayMap<String, Object>) mPackagesField.get(mActivityThread);

        //自定义一个 LoadedApk,系统是如何创建的我们就如何创建
        //执行下面的方法会返回一个 LoadedApk,我们就仿照系统执行此方法
        /*
              this.packageInfo = client.getPackageInfoNoCheck(activityInfo.applicationInfo,
                    compatInfo);
              public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,
                    int flags)
         */
        Class<?> mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
        Method getLoadedApkMethod = mActivityThreadClass.getDeclaredMethod("getPackageInfoNoCheck",
                ApplicationInfo.class, mCompatibilityInfoClass);

        /*
             public static final CompatibilityInfo DEFAULT_COMPATIBILITY_INFO = new CompatibilityInfo() {};
         */
        //以上注释是获取默认的 CompatibilityInfo 实例
        Field mCompatibilityInfoDefaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
        Object mCompatibilityInfo = mCompatibilityInfoDefaultField.get(null);

        //获取一个 ApplicationInfo实例
        ApplicationInfo applicationInfo = getAppInfo(file);
//        applicationInfo.uid = context.getApplicationInfo().uid;
        //执行此方法,获取一个 LoadedApk
        Object mLoadedApk = getLoadedApkMethod.invoke(mActivityThread, applicationInfo, mCompatibilityInfo);

        //自定义一个 ClassLoader
        String optimizedDirectory = context.getDir("plugin", Context.MODE_PRIVATE).getAbsolutePath();
        DexClassLoader classLoader = new DexClassLoader(file.getAbsolutePath(), optimizedDirectory,
                null, context.getClassLoader());

        //private ClassLoader mClassLoader;
        //获取 LoadedApk 的 mClassLoader 属性
        Field mClassLoaderField = mLoadedApk.getClass().getDeclaredField("mClassLoader");
        mClassLoaderField.setAccessible(true);
        //设置自定义的 classLoader 到 mClassLoader 属性中
        mClassLoaderField.set(mLoadedApk, classLoader);

        WeakReference loadApkReference = new WeakReference(mLoadedApk);
        //添加自定义的 LoadedApk
        mPackages.put(applicationInfo.packageName, loadApkReference);
        //重新设置 mPackages
        mPackagesField.set(mActivityThread, mPackages);
        Thread.sleep(2000);
    }

4. 运行报错解决

代码编写好了,运行一下,大力出奇迹,竟然崩溃了。

还是要静下心来看看崩溃日志,我的工程是运行在 Android10.0的设备上的,在低版本的设备上不会报此异常。 原因是我的插件不属于这个进程,查看一下调用的系统api方法,最终定位在了 ContentProviderNative.call() 上,由于这个是远程Binder,我们hook不到,所以我们退回来看android.provider.Settings.NameValueCache.getStringForUser(Settings.java:2374) 这一段,跟踪这一个发现,报错的位置了。 我们发现, NameValueCache 这个类的 mProviderHolder 属性可以返回一个 IContentProvider 实例,IContentProvider 是一个接口,我们可以以动态代理的方式,替换掉 call() 方法的包名为宿主的包名应该就可以避免上面那个异常了。通过日志调用栈发现我们先是通过 Settings$Global.getStringForUser() 通过源码发现,Global 类里有这两个关键属性,通过这两属性,我们就可以反射到刚才导致报错的call方法了。下面按步骤进行反射获取:

1、获取Settings$Global类的sProviderHolder属性

  	 Field sProviderHolderFiled = Settings.Global.class.getDeclaredField("sProviderHolder");
  	 sProviderHolderFiled.setAccessible(true);
  	 Object sProviderHolder = sProviderHolderFiled.get(null);

2、获取Settings$ContentProviderHolder的getProvider()方法

 	Method getProviderMethod = sProviderHolder.getClass().getDeclaredMethod("getProvider", ContentResolver.class);
        getProviderMethod.setAccessible(true);

3、获取原来的 IContentProvider 实例对象

	 final Object iContentProvider = getProviderMethod.invoke(sProviderHolder, context.getContentResolver());

4、Settings$获取 ContentProviderHolder类的mContentProvider属性

	Field mContentProviderFiled = sProviderHolder.getClass().getDeclaredField("mContentProvider");
        mContentProviderFiled.setAccessible(true);

5、获取IContentProvider类

 	Class<?> mIContentProviderClass = Class.forName("android.content.IContentProvider");

6、创建我们自己的代理对象

 	Object mContentProviderProxy = Proxy.newProxyInstance(
                context.getClassLoader(),
                new Class[]{mIContentProviderClass},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        if (method.getName().equals("call")) {
                            Log.d("yuongzw", method.getName());
                            //替换成宿主的包名
                            args[0] = context.getPackageName();
                        }
                        return method.invoke(iContentProvider, args);
                    }
                }
        );

7、设置我们的代理对象到 mContentProvider属性中

	mContentProviderFiled.set(sProviderHolder, mContentProviderProxy);

通过这7个步骤完成了我们对错误日志报错的修复,再次运行一遍看看效果。

又出现报错了,不应该啊?我不是已经修复了吗?看了一下错误日志,报的错跟上次的不太一样了,这次的是 Settings$System 这个类了,我们根据前面的7个步骤再次对这个类的一些属性进行hook就行了。到这里,所有的错误信息都修复了,最后上一个效果图: 项目地址:LoadApkDemo