• 0

  • 470

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

Mellon

大家好

3星期前

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

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

android

470

相关文章推荐

未登录头像

暂无评论