Android插件化原理

804 阅读8分钟

介绍

定义

插件化从APK中拆分出单独的业务或功能模块,可独立的进行下载、安装、运行。

优势

  • 动态化:快速功能更新、快速修复问题、按需拉取功能包
  • 包大小:大大缩减宿主包体积,提高下载安装速度,提高用户激活率,减少投放成本

应用

Case 1

Case 2

原理

如下图,Apk中有classes.dex、res资源、assets资源、resources.arsc、naitve lib、AndroidManifest.xml等,这些也都是用来承载我们需求功能的文件,那我们一个完整的插件化框架就需要支持以下文件的单独运行。

1. 双亲委派模型

Android中可以通过ClassLoader加载类,每个ClassLoader构造的时候都会传入一个parentClassLoader作为父亲,大家最熟知的就是loadClass方法,ClassLoader默认加载类的逻辑也都遵循“双亲委派机制”,即优先尝试用parent来加载类,加载不到再从自己的ClassLoader中加载类

public abstract class ClassLoader {
    ...
    public ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
    }
    protected Class<?> loadClass(String name, boolean resolve) {
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }

            if (c == null) {
                c = findClass(name);
            }
        }
        return c;
    }
    ...
}

最上层的ParentClassLoader通常为系统核心类,这样可以避免因为加载自定义同名类,与系统核心类冲突。

2. DexClassLoader

Android提供了ClassLoader的具体实现类DexClassLoader类,这个类可以帮我们加载一个apk中的类,我们只需要传入apkFile,就可以通过loadClass加载类,我们可以通过这种方式,给每个插件新建一个PluginClassLoader

public class PluginClassLoader extends DexClassLoader {

    public PluginClassLoader(String apkFile, String optimizedDirectory, String libraryPath, ClassLoader systemClassLoader) {  
        this(apkFile, optimizedDirectory, libraryPath, systemClassLoader, false);  
    }
    
    @Override
    protected Class<?> loadClass(String name, boolean resolve) {
            ...
    }
}

3. 实现对插件类的加载

Java每个类对象都有一个classLoader属性,表示当前类是由哪个ClassLoader加载,当我们用宿主的A类去调用插件的B类时,会尝试用A类的classLoader去loadClass B,显然是加载不到的。

但如果我们想让宿主类可以直接加载插件类,那需要宿主类的classloader具备加载插件类的能力。在Android中呢,默认用PathClassLoader来加载宿主APK中的类,其Parent为BootClassLoader来加载系统的类。SDK自定义一个新的类DelegateClassLoader实现对插件类的加载,并将其插到PathClassLoader与BootClassLoader之间

反射替换parent

 public static DelegateClassLoader installHook() {
    try {
        ClassLoader pathClassLoader = Utils.getAppContext().getClassLoader();
        ClassLoader parentClassLoader = pathClassLoader.getParent();
        if (!(parentClassLoader instanceof DelegateClassLoader)) {
            DelegateClassLoader DelegateClassLoader = new DelegateClassLoader(parentClassLoader, pathClassLoader);
            Field parentField = ClassLoader.class.getDeclaredField("parent");
            parentField.setAccessible(true);
            parentField.set(pathClassLoader, DelegateClassLoader);
            return DelegateClassLoader;
        }
    } catch (Exception var4) {
    }
    return null;
}

我们通过DelegateClassLoader中的逻辑来加载插件的类,分为几步:

  • 首先调用PathClassLoader的findLoadedClass()从宿主中查找
  • 交给BootClassLoader处理
  • DelegateClassLoader先调用PathClassLoader#findClass查找
  • 遍历所有已加载启动的插件Dex中查找
  • 最后按类名匹配到目标插件,尝试加载插件并查找
public class DelegateClassLoader extends ClassLoader {

    private Method findClassMethod;
    private Method findLoadedClassMethod;
    private ClassLoader pathClassLoader;
    
    public DelegateClassLoader(ClassLoader parent, ClassLoader pathClassLoader) {
        super(parent);
        this.findClassMethod = MethodUtils.getAccessibleMethod(ClassLoader.class, "findClass", String.class);
        this.findLoadedClassMethod = MethodUtils.getAccessibleMethod(ClassLoader.class, "findLoadedClass", String.class);
        this.pathClassLoader = pathClassLoader;
    }
    
      
    @Override
    protected Class<?> findClass(String className) {
        // pathClassLoader.findLoadedClass(name)
        if (clazz == null && findLoadedClassMethod != null) {
            try {
                clazz = (Class<?>) findLoadedClassMethod.invoke(pathClassLoader, className);
            } catch (Throwable e) {
                exception = e;
            }
        }
        // pathClassLoader.findClass(name)
        if (clazz == null && findClassMethod != null) {
            try {
                clazz = (Class<?>) findClassMethod.invoke(pathClassLoader, className);
            } catch (Throwable e) {
                exception = e;
            }
        }
        // PluginClassLoader.findClassFromCurrent(name) from loaded Plugin
        if (clazz == null) {
            Map<String, PluginClassLoader> pluginClassLoaders = new ConcurrentHashMap<>(PluginLoader.sCachedPluginClassLoader);
            Iterator<Map.Entry<String, PluginClassLoader>> iterator = pluginClassLoaders.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, PluginClassLoader> entry = iterator.next();
                PluginClassLoader loader = entry.getValue();
                try {
                    clazz = loader.findClassFromCurrent(className);
                } catch (Throwable e) {
                    exception = e;
                }
                if (clazz != null) {
                    break;
                }
            }
        }
        // PluginClassLoader.findClassFromCurrent(name) from unloaded Plugin
        if (clazz == null) {
            for (Plugin plugin : PluginManager.getInstance().listPlugins()) {
                if (needLoadPlugin(plugin, className)) {
                        // preload
            if (PluginLoader.sCachedPluginClassLoader.get(plugin.mPackageName) == null) {
                PluginManager.getInstance().preload(plugin.mPackageName);
            }
            // find
            PluginClassLoader pluginClassLoader = PluginLoader.sCachedPluginClassLoader.get(plugin.mPackageName);
            if (pluginClassLoader != null) {
                try {
                    clazz = pluginClassLoader.findClassFromCurrent(className);
                } catch (Throwable e) {
                    exception = e;
                }
                if (clazz != null) {
                    break;
                                }
                        }
                }
            }
        }
        ...
    }
}

image.png

四大组件

1. Manifest检查

AMS是Android中最核心的服务,主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作。其中在PackageManagerService.java打开一个Activity时,会先通过Package查找此APK是否Manifest中声明过此Activity,如果没声明就无法打开目标Activity。

2. Activity-偷梁换柱

Android是通过Intent告诉AMS要打开的Activity的,当我们要启动插件的Activity时,如果我们假装告诉系统要启动一个宿主Activity,就可以通过系统的检测了

  • startActivity最终都会统一经过Instrumention,我们可以通过Hook Instrumention来做一些定制化操作

  • 在HookInstrumention中,我们在执行execStartActivity之前对Intent内容替换为SubActivity

private Intent wrapIntent(Object target, Intent intent, int requestCode) {
    boolean shouldInterrupt = false;
    if (intent != null && !intent.getBooleanExtra("start_only_for_android", false)) {
        String packageName = this.getPluginPackageName(intent);
        if (PluginPackageManager.isPluginPackage(packageName)) {
            HookManager.getInstance().installHookActivityManagerProxy();
            shouldInterrupt = !PluginPackageManager.checkPluginInstalled(packageName) || !PluginManager.getInstance().loadPlugin(packageName);
        }
    }

    if (shouldInterrupt) {
        Intent wrapIntent = new Intent(Utils.getAppContext(), PluginLoaderActivity.class);
        wrapIntent.putExtra("target_intent", intent);
        wrapIntent.putExtra("request_code", requestCode);
        wrapIntent.putExtra("plugin_package_name", this.getPluginPackageName(intent));
        return wrapIntent;
    } else {
        List<ResolveInfo> hostActivities = Utils.getAppContext().getPackageManager().queryIntentActivities(intent, 16842752);
        if (hostActivities != null && hostActivities.size() > 0) {
            return intent;
        } else {
            //真实要启动的组件
            Intent targetIntent = intent;
            if (intent != null) {
                intent.putExtra("hookInstrumentationHasWrapIntent", true);
            }

            if (intent != null && !intent.getBooleanExtra("start_only_for_android", false)) {
                List<ResolveInfo> resolveInfos = PluginPackageManager.queryIntentActivities(intent, 0);
                if (resolveInfos != null && resolveInfos.size() > 0) {
                    ActivityInfo targetActivityInfo = ((ResolveInfo)resolveInfos.get(0)).activityInfo;
                    if (targetActivityInfo != null) {
                        //插桩的组件
                        ActivityInfo stubActivityInfo = PluginActivityManager.selectStubActivity(targetActivityInfo);
                        if (stubActivityInfo != null) {
                            intent.putExtra("target_activityinfo", targetActivityInfo);
                            intent.putExtra("stub_activityinfo", stubActivityInfo);
                            Intent wrapIntent = new Intent();
                            //设置代理className
                            wrapIntent.setClassName(stubActivityInfo.packageName, stubActivityInfo.name);
                            wrapIntent.setFlags(intent.getFlags());
                            wrapIntent.putExtra("target_intent", intent);
                            wrapIntent.putExtra("target_activityinfo", targetActivityInfo);
                            wrapIntent.putExtra("stub_activityinfo", stubActivityInfo);
                            String stubProcessName = "";
                            if (stubActivityInfo.applicationInfo != null) {
                                stubProcessName = stubActivityInfo.applicationInfo.processName;
                            }

                            String createInfo = System.currentTimeMillis() + "#" + Process.myPid() + "#" + Utils.getAppContext().getApplicationInfo().processName + "#" + stubProcessName;
                            wrapIntent.putExtra("stub_createinfo", createInfo);
                            targetIntent = wrapIntent;
                            wrapIntent.putExtra("hookInstrumentationHasWrapIntent", true);
                        } else {
                            Logger.w("hook/activity", "HookInstrumentation wrapIntent selectStubActivityInfo null");
                        }
                    }
                } else {
                    Logger.w("hook/activity", "HookInstrumentation wrapIntent queryIntentActivities from plugin empty");
                }
            }

            return targetIntent;
        }
    }
}
  • AMS处理完系统会通过ActivityThreadHandler的消息的方式打开目标Activity

  • 这时候我们可以用同样的方法Hook ActivityThreadHandler对传回来的Intent中SubActivity替换为原本的插件Activity
private boolean handleLaunchPluginActivity(Message message) {
    Intent stubIntent = null;

    try {
        Object activityClientRecord = message.obj;
        stubIntent = (Intent)FieldUtils.readField(activityClientRecord, "intent");
        stubIntent.setExtrasClassLoader(this.getClass().getClassLoader());
        //从Intent中获取要启动的真实activity
        Intent targetIntent = (Intent)stubIntent.getParcelableExtra("target_intent");
        ActivityInfo targetActivityInfo = (ActivityInfo)stubIntent.getParcelableExtra("target_activityinfo");
        ActivityInfo stubActivityInfo = (ActivityInfo)stubIntent.getParcelableExtra("stub_activityinfo");
        ActivityInfo stubActivityInfoFromSystem = (ActivityInfo)FieldUtils.readField(activityClientRecord, "activityInfo");
        if (stubActivityInfo == null || stubActivityInfoFromSystem == null || stubActivityInfo.exported || stubActivityInfoFromSystem.exported || !TextUtils.equals(stubActivityInfo.packageName, stubActivityInfoFromSystem.packageName) || !TextUtils.equals(stubActivityInfo.name, stubActivityInfoFromSystem.name)) {
            Logger.d("hook/activity", "HookHandlerCallbackStart source verification failed.");
            return false;
        }

        if (targetIntent != null && targetActivityInfo != null) {
            if (targetActivityInfo.applicationInfo != null && !(new File(targetActivityInfo.applicationInfo.sourceDir)).exists()) {
                Logger.e("hook/activity", "HookHandlerCallback handleLaunchPluginActivity, targetActivityInfo.applicationInfo.sourceDir is not exists, " + targetActivityInfo.applicationInfo.sourceDir);
                targetIntent.putExtra("extra_stub_intent", stubIntent);
                targetActivityInfo.applicationInfo = Utils.getAppContext().getApplicationInfo();
                targetIntent.setClassName(Utils.getAppContext(), ErrorBackupActivity.class.getName());
            } else {
                this.reportIfTimeout(stubIntent);
                Logger.d("hook/activity", "HookHandlerCallback handleLaunchPluginActivity, then launchPluginApp applicationInfo = " + targetActivityInfo.applicationInfo);
                if (targetActivityInfo.applicationInfo != null) {
                    PluginLoader.launchPluginApp(targetActivityInfo.applicationInfo.packageName, targetActivityInfo);
                }

                String contextPackageName = PluginPackageManager.generateContextPackageName(targetActivityInfo.packageName);
                targetIntent.setClassName(contextPackageName, targetActivityInfo.name);
            }

            FieldUtils.writeField(activityClientRecord, "intent", targetIntent);
            FieldUtils.writeField(activityClientRecord, "activityInfo", targetActivityInfo);
            Logger.w("hook/activity", "HookHandlerCallback handleLaunchPluginActivity, " + String.format("Target[%s] <<< Stub[%s]", targetActivityInfo.name, stubActivityInfo.name));
        }

        if (OSUtil.isAndroidO()) {
            try {
                ViewRootImpl.ActivityConfigCallback callback = (ViewRootImpl.ActivityConfigCallback)FieldUtils.readField(activityClientRecord, "configCallback");
                ViewRootImpl.ActivityConfigCallback newCallback = new ActivityConfigCallbackProxy(callback);
                FieldUtils.writeField(activityClientRecord, "configCallback", newCallback);
                Logger.i("hook/activity", "HookHandlerCallback hook replace ViewRootImpl.ActivityConfigCallback");
            } catch (Exception var11) {
                Logger.e("hook/activity", "HookHandlerCallback hook replace ViewRootImpl.ActivityConfigCallback failed.", var11);
            }
        }
    } catch (Exception var12) {
        Logger.e("hook/activity", "HookHandlerCallback handleLaunchPluginActivity failed.", var12);
        if (stubIntent != null && var12 instanceof BadParcelableException) {
            try {
                Object mExtras = FieldUtils.readField(stubIntent, "mExtras");
                FieldUtils.writeField(mExtras, "mParcelledData", (Object)null);
            } catch (Throwable var10) {
                stubIntent.replaceExtras(new Bundle());
            }
        }
    }

    return false;
}

3. Service-偷梁换柱

与Activity同样的方式:

  • Hook ActivityManagerNative 拦截Service的start/stop/bind/unBind方法,将Intent换成SubService

  • Hook ActivityThreadHandler 将Intent目标还原

4. Receiver注册

广播分为静态广播和动态广播,动态广播不需要注册,静态广播只需要在插件启动时,按照动态广播的方式手动进行注册即可。

private static void registerReceivers(ApplicationInfo pluginAppInfo) {
    List<ReceiverInfo> receiverInfos = PluginPackageManager.getReceivers(pluginAppInfo.packageName, 0);
    if (receiverInfos != null && receiverInfos.size() > 0) {
        ClassLoader pluginClassLoader = (ClassLoader)sCachedPluginClassLoader.get(pluginAppInfo.packageName);
        PackageManager packageManager = Utils.getAppContext().getPackageManager();
        Iterator var4 = receiverInfos.iterator();

        while(true) {
            ReceiverInfo receiverInfo;
            List hostReceiversList;
            do {
                if (!var4.hasNext()) {
                    return;
                }

                receiverInfo = (ReceiverInfo)var4.next();
                String packageName = Utils.getAppContext().getPackageName();
                Intent intent = new Intent();
                intent.setComponent(new ComponentName(packageName, receiverInfo.name));
                hostReceiversList = packageManager.queryBroadcastReceivers(intent, 16842752);
            } while(hostReceiversList != null && hostReceiversList.size() > 0);

            try {
                BroadcastReceiver receiver = (BroadcastReceiver)pluginClassLoader.loadClass(receiverInfo.name).newInstance();
                Iterator var10 = receiverInfo.intentFilters.iterator();

                while(var10.hasNext()) {
                    IntentFilter intentFilter = (IntentFilter)var10.next();
                    Utils.getAppContext().registerReceiver(receiver, intentFilter);
                }

                Logger.i("hook/load", "PluginLoader registerReceivers, " + receiver + ", " + pluginAppInfo.packageName);
            } catch (Exception var12) {
                Logger.e("hook/load", "PluginLoader registerReceivers failed, " + receiverInfo.name + "pkg = " + pluginAppInfo.packageName, var12);
            }
        }
    }
}

5. ContentProvider安装

类似Receiver的静态改态的方式,相同进程的数据访问只需要将Provider执行ActivityThread.installContentProvider即可(反射调用)

private static boolean installContentProviders(Context context, String pluginPkgName, String processName) {
    List<ProviderInfo> providers = PluginPackageManager.getProviders(pluginPkgName, processName, 0);
    if (providers != null && providers.size() > 0) {
        Iterator<ProviderInfo> iterator = providers.iterator();

        while(iterator.hasNext()) {
            ProviderInfo providerInfo = (ProviderInfo)iterator.next();
            if (context.getPackageManager().resolveContentProvider(providerInfo.authority, 16777216) != null) {
                iterator.remove();
            }

            if (providerInfo != null && !TextUtils.equals(providerInfo.applicationInfo.packageName, context.getPackageName())) {
                providerInfo.applicationInfo.packageName = context.getPackageName();
            }
        }

        ActivityThreadHelper.installContentProviders(context, providers);
        Logger.i("hook/load", "PluginLoader installContentProviders, " + providers + ", " + pluginPkgName);
    }

    return true;
}

资源

1. 加载插件资源

Android是通过Resources对象加载资源的,Resources有个核心实现类AssetManager,他负责具体的资源读取,它有一个很重要的方法addAssetPath(path)可以将一个资源路径添加到当前Resources中,当我们加载一个插件时,调用它就能把插件资源添加到Resources对象中

private AssetManager appendAssetPathSafely(AssetManager assetManager, String sourceDir, boolean asSharedLibrary) {
    int tryCount = 3;

    while(tryCount-- >= 0) {
        String errorMsg = null;

        try {
            synchronized(assetManager) {
                int cookie = 0;

                for(int i = 0; i < 3; ++i) {
                    if (OSUtil.isAndroidLM()) {
                        cookie = (Integer)MethodUtils.invokeMethod(assetManager, "addAssetPathNative", new Object[]{sourceDir}, new Class[]{String.class});
                    } else if (OSUtil.isAndroidNMR1()) {
                        cookie = (Integer)MethodUtils.invokeMethod(assetManager, "addAssetPathNative", new Object[]{sourceDir, asSharedLibrary}, new Class[]{String.class, Boolean.TYPE});
                    }

                    if (cookie != 0) {
                        break;
                    }
                }

                if (cookie == 0) {
                    errorMsg = "cookie == 0";
                    break;
                }

                Object seed = FieldUtils.readField(assetManager, "mStringBlocks");
                int seedNum = seed != null ? Array.getLength(seed) : 0;
                int num = (Integer)MethodUtils.invokeMethod(assetManager, "getStringBlockCount", new Object[0]);
                Object newStringBlocks = Array.newInstance(seed.getClass().getComponentType(), num);

                for(int i = 0; i < num; ++i) {
                    if (i < seedNum) {
                        Array.set(newStringBlocks, i, Array.get(seed, i));
                    } else {
                        long nativeStringBlockObj = (Long)MethodUtils.invokeMethod(assetManager, "getNativeStringBlock", new Object[]{i}, new Class[]{Integer.TYPE});
                        Object stri = MethodUtils.invokeConstructor(seed.getClass().getComponentType(), new Object[]{nativeStringBlockObj, true}, new Class[]{Long.TYPE, Boolean.TYPE});
                        Array.set(newStringBlocks, i, stri);
                    }
                }

                FieldUtils.writeField(assetManager, "mStringBlocks", newStringBlocks);
            }

            Logger.w("hook/load", "AssetManagerProcessorCompat appendAssetPathSafely success, sourceDir = " + sourceDir);
            break;
        } catch (Exception var18) {
            Logger.e("hook/load", "AssetManagerProcessorCompat appendAssetPathSafely failed, sourceDir = " + sourceDir, var18);
        }
    }

    return assetManager;
}

2. 插件与宿主的资源ID冲突

Java代码通常通过资源ID(Int值)来访问具体资源,资源id是由4个字节组成,用十六进制表示:0x PPTTEEEE,其中PP代表命名空间,TT代表资源类型(animator、anim、color、drawable等),EEEE代表的是每一个资源所出现的顺序。我们打包时的资源PP值默认都是7F,就会导致插件和宿主的资源冲突:

恰好aapt工具支持我们通过命令参数的方式设置PP值 : aapt2 --package-id 0xPP ,我们给每个插件的PP段设置不同的值,就能保证每个插件和宿主的资源都不冲突。

3. 插件访问宿主资源

如果插件使用了宿主的资源,是否将资源单独在插件中打一份呢?如下图,会带来包体积的增加:

针对这个问题,正好Android官方提供了public.xml来帮助我们固定资源名与资源ID

Native so

当我们执行System.loadLibrary(libName)的时候,会调用VMStack.getCallingClassLoader()返回当前类的classLoader对象,用于加载so文件,插件类的classLoader就是PluginClassLoader

而我们在创建PluginClassLoader的时候,通过librarySearchPath参数传入so的路径。