Android插件化的兼容性(上):Android O的适配

Posted 包建强的无线技术空间

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android插件化的兼容性(上):Android O的适配相关的知识,希望对你有一定的参考价值。

      首先声明,《android插件化开发指南》这本书所介绍的Android底层是基于Android6.0(API level 23)的,而本书介绍的各种插件化解决方案,以及配套的70多个例子,在Android7.0(API level 24)手机上测试都是能正常工作的。

     如果读者您的手机是Android 26、27,甚至28(也就是Android P),那么会有30个插件化的例子不能正常工作,这是因为Android系统底层的源码改动导致的。

     本篇文章,专门介绍Android O的改动对插件化产生的影响,以及相应的插件化解决方案。

 

(一)从ActivityManagerNative的重构谈起

     首先是ActivityManagerNative这个类的gDefault字段,这个字段在API 25以及之前的版本,定义如下:

public abstract class ActivityManagerNative extends Binder implements IActivityManager {
    private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
        protected IActivityManager create() {
            IBinder b = ServiceManager.getService("activity");
            if (false) {
                Log.v("ActivityManager", "default service binder = " + b);
            }
            IActivityManager am = asInterface(b);
            if (false) {
                Log.v("ActivityManager", "default service = " + am);
            }
            return am;
        }
    };
}

 

     所以,我们可以通过反射获取ActivityManagerNative的gDefault字段,执行它的create方法,得到IActivityManager接口类型的对象。

     看到这个接口类型,我们眼前一亮,可以通过Proxy.newProxyInstance方法,hook掉这个IActivityManager对象,拦截它的startActivity方法,把要启动的、没有在Manifest中声明的Activity,替换成占坑StubActivity,代码如下所示:

    public static void hookAMN() throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            IllegalAccessException, NoSuchFieldException {

        //获取AMN的gDefault单例gDefault,gDefault是final静态的
        Object gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault");
        // gDefault是一个 android.util.Singleton<T>对象; 我们取出这个单例里面的mInstance字段
        Object mInstance = RefInvoke.getFieldObject("android.util.Singleton", gDefault, "mInstance");

        // 创建一个这个对象的代理对象MockClass1, 然后替换这个字段, 让我们的代理对象帮忙干活
        Class<?> classB2Interface = Class.forName("android.app.IActivityManager");
        Object proxy = Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class<?>[] { classB2Interface },
                new MockClass1(mInstance));

        //把gDefault的mInstance字段,修改为proxy
        Class class1 = gDefault.getClass();
        RefInvoke.setFieldObject("android.util.Singleton", gDefault, "mInstance", proxy);
    }

 

     我们在书中的第5章详细讲解过上述这些代码。但不幸的是,这些代码在Android O(API level 26)以上的系统版本中就不能运行了,在运行到这句话的时候,gDefault的值为空:

Object gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault");

 

     这是因为Google在Android O中,把ActivityManagerNative中的这个gDefault字段删除了,转移到了ActivityManager类中,但此时,这个字段改名为IActivityManagerSingleton,所以在Android P中,要把这句话改为:

Object gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManager", "IActivityManagerSingleton");

    

     但这又不兼容于Android O以下的版本了,所以写一个if-else条件语句,根据Android系统的版本,来做不同的处理,如下所示:

        Object gDefault = null;
        if (android.os.Build.VERSION.SDK_INT <= 25) {
            //获取AMN的gDefault单例gDefault,gDefault是静态的
            gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault");
        } else {
            //获取ActivityManager的单例IActivityManagerSingleton,他其实就是之前的gDefault
            gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManager", "IActivityManagerSingleton");
        }

 

(二)Element和DexFile的兴衰史

     接下来我们把目光转移到插件类的加载。我们在书中介绍了3种加载方式:

     1. 为每一个插件创建一个ClassLoader,用插件ClassLoader去加载插件中的类。

     2. 把所有插件中的dex,都合并到宿主App的dex数组中。

     3. 把宿主App所使用的ClassLoader,替换成我们自己创建的ClassLoader,在这个新的ClassLoader中,有一个容器变量,承载所有插件的ClassLoader,用来加载插件中的类。

     这其中,第2种方式的实现是最简单的,也就是合并所有插件的dex到一个数组中,具体代码实现如下所示:

public final class BaseDexClassLoaderHookHelper {

    public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
            throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        // 获取 BaseDexClassLoader : pathList
        Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList");

        // 获取 PathList: Element[] dexElements
        Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements");

        // Element 类型
        Class<?> elementClass = dexElements.getClass().getComponentType();

        // 创建一个数组, 用来替换原始的数组
        Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);

        // 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
        Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
        Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
        Object o = RefInvoke.createObject(elementClass, p1, v1);

        Object[] toAddElementArray = new Object[] { o };
        // 把原始的elements复制进去
        System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
        // 插件的那个element复制进去
        System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);

        // 替换
        RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);
    }
}

 

     这个思路没问题。注意其中的这么几句话:

Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
Object o = RefInvoke.createObject(elementClass, p1, v1);
Object[] toAddElementArray = new Object[] { o };

 

     这几句话中,通过反射执行了Element的带有4个参数的构造函数,但不幸的是,在Android O以及之后的版本,这个带有4个参数的构造函数就被废弃了。

     此外,在这个构造函数中使用到的DexFile这个类,也被废弃了,对此Google给出的解释是,只有Android系统可以使用DexFile,App层面不能使用它。

     于是,我们不得不另辟蹊径,通过执行DexPathList类的makeDexElements方法,来生成插件中的dex:

List<File> legalFiles = new ArrayList<>();
legalFiles.add(apkFile);

List<IOException> suppressedExceptions = new ArrayList<IOException>();

Class[] p1 = {List.class, File.class, List.class, ClassLoader.class};
Object[] v1 = {legalFiles, optDexFile, suppressedExceptions, cl};
Object[] toAddElementArray = (Object[])
RefInvoke.invokeStaticMethod("dalvik.system.DexPathList", "makeDexElements", p1, v1);

 

     这段代码,在Android O之前的版本也是适用的。所以,我们找到了比DexFile更好用的makeDexElements方法,进行Hook。

以上是关于Android插件化的兼容性(上):Android O的适配的主要内容,如果未能解决你的问题,请参考以下文章

android组件化和插件化的区别

新方向 直面Shadow,零反射无Hook如何实现Android插件化?

如何将Android程序做成插件化的形式?详解插件化实现原理

Android 插件化的前世今生大揭秘

怎么将 Android 程序做成插件化的形式

怎么将 Android 程序做成插件化的形式