Android 插件化小结

Posted guangdeshishe

tags:

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

android 插件化小结

简介

插件化可以用于让apk不经过安装而运行起来,将一些不常用的模块做成插件,当需要用到时再下载运行,这可以减小主app安装包的大小,在开发过程中也可以单独调试某个插件模块,避免整个项目太大编译运行太慢问题;

常见的插件化框架有:dynamic-load-apk、VirtualApp、RePlugin、shadow等

实现原理

插件化的实现主要需要解决以下几个问题:

  • 插件apk中类的加载,以及和宿主app的相互调用

  • 插件apk中资源的加载,以及和宿主app的相互使用

  • 插件apk中四大组件的生命周期管理

类加载

插件apk中的类主要是通过DexClassCloader去加载的,这里也分为两种方式:

  • 单独为每个插件apk创建一个ClassLoader;这种方式的优点是插件之间ClassLoader相互隔离,如果不同插件引入相同类库不同版本不会存在问题;缺点就是插件之间和宿主之间相互调用需要持有对应的ClassLoader

  • 将多个插件ClassLoader与宿主ClassLoader合并为一个ClassLoader;这种方式主要是通过反射DexPathList,将不同插件的dex路径添加到同一个数组中,跟热修复原理相似;优点是插件之间和宿主之间的类都可以直接访问;缺点就是不同插件引入同一类库不同版本时会有问题,需要将用到的公共类库抽离到一个独立的公共插件中

资源加载

插件资源的加载一般是通过反射构建Assetmanager并调用addAssetPath将apk路径添加进去,然后通过传入AssertManager构建Resources对象;不同插件之间的资源也是可以单独分开管理和合并为一个这两种方式的:

  • 单独分开管理:优点是相互独立,就算资源名重复也不会出现问题,只需要重写代理Activity的getResources方法返回插件的资源对象就可以;缺点是互相使用对方资源需要先获取到Resource对象

  • 合并为一个Resources:通过反射调用Assetmanager的addAssetPath方法将所有插件和宿主的资源添加进去,然后利用反射替换到原来宿主中的Resource对象,比如:ContextThemeWrapper.mResources、LoadedApk.mResources、ActivityThread.mResourcesManager等;缺点就是需要反射修改更多地方的Resources对象,并且需要解决资源id可能重复问题(可以通过修改apk中resources文件,或者修改aapt编译源码)

四大组件的支持

四大组件的类仅仅创建出实例是不能正常使用的,需要结合生命周期的调用才能正常使用;四大组件中Activity是最复杂的,在插件化中Activity主要有两种实现方式:

  • 代理Activity:

    • 当启动插件里的Activity时,会先启动一个代理Activity,代理Activity里会根据插件Activity相关信息,创建插件Activity实例,并传入当前代理Activity对象给插件Activity,然后在各个生命周期方法中,调用插件Activity中各个生命周期方法,还需要重写getClassLoader、getResources等方法返回对应插件的ClassLoader和资源对象

    • 插件Activity这边需要统一继承自一个插件Activity基类,在这个基类中会重写setContentView、startActivity等方法,转而调用代理Activity中的方法

    • 优点是不需要hook系统api,没有兼容性问题

    • 缺点是如果Activity不是默认启动模式,而是SingleTask、SingleInstance等,需要自己管理Activity栈,会相对麻烦一些

  • Hook系统启动Activity过程

    • Activity启动过程:

      • startActivity/startActivityForResult

      • Instrumentation.execStartActivity

      • ActivityTaskManager.startActivity

      • AMS从PMS中查找Activity信息、创建进程、处理启动模式、任务栈等等

      • 通过Handler向ActivityThread发送消息,调用Instrumentation.newActivity方法通过反射创建Activity实例

      • 接着调用Activity各个生命周期方法

    • Hook启动Activity过程:

    • Activity启动过程中,校验Activity是否注册到AndroidManifest文件中是在AMS中,所以关键的Hook点有两个:一个是在调用AMS之前 ,将要启动的插件Activity替换成预置的Activity用于躲过AMS的检查;另一个点是在ActivityThead中创建Activity之前,需要将预置的Activity再替换回插件Activity

    • Hook点根据不同插件化框架会有所不同,比如有些Hook Instrumentation的execStartActivity和newActivity方法;也可以Hook ActivityTaskManager(静态Sington单利)和ActivityThread中的Handler(Handler中的callback)

    • 优点:生命周期和启动模式完全由系统服务负责

    • 缺点:通过Hook方式不稳定,需要适配不同Android版本

  • 其他组件:

    • Service和ContentProvider与Activity类似

    • BroadCastReceiver是通过解析Manifest,将静态广播转为动态注册

Shadow实现方法

Shadow号称零反射插件化框架,主要体现在以下几个方面:

  • 加载插件里的类是通过DexClassLoader加载,没有用到反射去合并各个插件的dex

  • 资源加载时并没有通过AssetManager反射构建,而是通过PM的getResourcesForApplication方法创建

  • 在四大组件支持方面,则是走的是Activity代理方式避免Hook系统api

滴滴开源Android插件化框架VirtualAPK原理分析

概述

滴滴出行公司的首个对外开源项目 - VirtualAPK。地址:https://github.com/didi/VirtualAPK

滴滴自行研发了这款插件化框架,功能全面、兼容性好,还能够适用于有耦合的业务插件,这就是VirtualAPK存在的意义。业内认为,在加载耦合插件方面,VirtualAPK可以说是开源方案的首选。据说滴滴打车里面已经用上了,所以还是有必要一探究竟的~~

VirtualAPK 的工作流程如图所示:

VirtualAPK 对于插件没有额外的约束,原生的 apk 即可作为一个插件。插件工程编译生成 apk 后,通过宿主 App 加载,每个插件 apk 被加载后,都会在宿主中创建一个单独的 LoadedPlugin 对象。如上图所示,通过这些 LoadedPlugin 对象,VirtualAPK 就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的App一样运行。

Activity 支持

Hook ActivityManagerService

插件化支持首先要解决的一点就是插件里的Activity并未在宿主程序的 AndroidMainfest.xml 注册,常规方法肯定无法直接启动插件的Activity,这个时候就需要去了解Activity的启动流程,关于启动过程主要的几个步骤请参考:浅析Android Activity的启动过程

从上文中可知,Activity 启动实际上是调用了 Instrumentation.execStartActivity 这个方法。源码如下:

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,  
            Intent intent, int requestCode, Bundle options)   
    IApplicationThread whoThread = (IApplicationThread) contextThread;  
    if (mActivityMonitors != null)   
        synchronized (mSync)   
            final int N = mActivityMonitors.size();  
            for (int i=0; i<N; i++)  //先查找一遍看是否存在这个activity  
                final ActivityMonitor am = mActivityMonitors.get(i);  
                if (am.match(who, null, intent))   
                    am.mHits++;  
                    if (am.isBlocking())   
                        return requestCode >= 0 ? am.getResult() : null;  
                      
                    break;  
                  
              
          
      
    try   
        intent.migrateExtraStreamToClipData();  
        intent.prepareToLeaveProcess();  
        //这里才是真正打开activity的地方,其核心功能在whoThread中完成。  
        int result = ActivityManagerNative.getDefault().startActivity(whoThread, who.getBasePackageName(), intent,  
                    intent.resolveTypeIfNeeded(who.getContentResolver()),token, target != null ? target.mEmbeddedID : null,  
                    requestCode, 0, null, options);         
        checkStartActivityResult(result, intent); // 处理各种异常,如ActivityNotFound  
     catch (RemoteException e)   
      
    return null;  

可见, startActivity 最终通过 ActivityManagerNative.getDefault() 远程调用了AMS的startActivity方法, ActivityManagerNative 实际上就是 ActivityManagerService 这个远程对象的 Binder 代理对象,每次需要与AMS交互时,需要通过这个 Binder 对象完成远程IPC调用。

还不了解Binder的童鞋,可以看看老罗的Android进程间通信(IPC)机制Binder简要介绍和学习计划


// ActivityManagerNative.getDefault()
static public IActivityManager getDefault() 
    return gDefault.get();


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.getDefault() 实际上是返回了一个 IActivityManager 的单例对象。

那么,VirtualApk 所要做的第一件事,就是把这个 AMS 代理对象保存起来。首先,我们可以看一下 VirtualApk 核心库里面 com.didi.virtualapk.PluginManager 这个类的初始化:


// 构造方法
private PluginManager(Context context) 
    Context app = context.getApplicationContext();
    if (app == null) 
        this.mContext = context;
     else 
        this.mContext = ((Application)app).getBaseContext();
    
    prepare();


// 初始化
private void prepare() 
    Systems.sHostContext = getHostContext();
    this.hookInstrumentationAndHandler();
    this.hookSystemServices();


/**
 * Hook 出一个IActivityManager,也就是 AMS 的代理对象
 */
private void hookSystemServices() 
    try 
        // 反射调用 ActivityManagerNative.getDefault(),实际上这在6.0中是公开的静态方法,反射可能是考虑到版本兼容性吧?
        Singleton<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManagerNative.class, null, "gDefault");
        // 通过动态代理的方式去创建代理对象,之后所有ActivityManagerNative中的方法被调用的时候都会经过这个代理
        IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());

        // Hook IActivityManager from ActivityManagerNative,实际上就是把 ActivityManagerNative 替换为刚创建的 activityManagerProxy
        ReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, "mInstance", activityManagerProxy);

        if (defaultSingleton.get() == activityManagerProxy) 
            // 两者一样,保存下来
            this.mActivityManager = activityManagerProxy;
        
     catch (Exception e) 
        e.printStackTrace();
    

实际上除了 startActivity 是调用 AMS 的方法以外,startServicebindService 等方法,最终调用到AMS的里的方法,这个我们在动态代理类 com.didi.virtualapk.delegate.ActivityManagerProxy 也可以找到:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
    if ("startService".equals(method.getName())) 
        try 
            // 执行自定义的 startService 过程,后面会提到
            return startService(proxy, method, args);
         catch (Throwable e) 
            Log.e(TAG, "Start service error", e);
        
     else if ("stopService".equals(method.getName())) 
        try 
            return stopService(proxy, method, args);
         catch (Throwable e) 
            Log.e(TAG, "Stop Service error", e);
        
     else if ("stopServiceToken".equals(method.getName())) 
        try 
            return stopServiceToken(proxy, method, args);
         catch (Throwable e) 
            Log.e(TAG, "Stop service token error", e);
        
     else if ("bindService".equals(method.getName())) 
        try 
            return bindService(proxy, method, args);
         catch (Throwable e) 
            e.printStackTrace();
        
     else if ("unbindService".equals(method.getName())) 
        try 
            return unbindService(proxy, method, args);
         catch (Throwable e) 
            e.printStackTrace();
        
     else if ("getIntentSender".equals(method.getName())) 
        try 
            getIntentSender(method, args);
         catch (Exception e) 
            e.printStackTrace();
        
     else if ("overridePendingTransition".equals(method.getName()))
        try 
            overridePendingTransition(method, args);
         catch (Exception e)
            e.printStackTrace();
        
    

    try 
        // sometimes system binder has problems.
        return method.invoke(this.mActivityManager, args);
     catch (Throwable th) 
        Throwable c = th.getCause();
        if (c != null && c instanceof DeadObjectException) 
            // retry connect to system binder
            IBinder ams = ServiceManager.getService(Context.ACTIVITY_SERVICE);
            if (ams != null) 
                IActivityManager am = ActivityManagerNative.asInterface(ams);
                mActivityManager = am;
            
        

        Throwable cause = th;
        do 
            if (cause instanceof RemoteException) 
                throw cause;
            
         while ((cause = cause.getCause()) != null);

        throw c != null ? c : th;
    

所以实际上就等同于我们重写了一些 ActivityService 的相关操作。具体做些什么,后面会提到~

Hook Instrumentation

回过头去看看 Instrumentation.execStartActivity 这个方法,在最后有这么一句代码:

checkStartActivityResult(result, intent); // 处理各种异常,如ActivityNotFound
static void checkStartActivityResult(int res, Object intent)   
    if (res >= ActivityManager.START_SUCCESS)   
        return;  
      

    switch (res)   
        case ActivityManager.START_INTENT_NOT_RESOLVED:  
        case ActivityManager.START_CLASS_NOT_FOUND:  
            if (intent instanceof Intent && ((Intent)intent).getComponent() != null)  
                throw new ActivityNotFoundException(  
                        "Unable to find explicit activity class "  
                        + ((Intent)intent).getComponent().toShortString()  
                        + "; have you declared this activity in your AndroidManifest.xml?");  
            throw new ActivityNotFoundException(  
                    "No Activity found to handle " + intent);  
        case ActivityManager.START_PERMISSION_DENIED:  
            throw new SecurityException("Not allowed to start activity "  
                    + intent);  
        case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:  
            throw new AndroidRuntimeException(  
                    "FORWARD_RESULT_FLAG used while also requesting a result");  
        case ActivityManager.START_NOT_ACTIVITY:  
            throw new IllegalArgumentException(  
                    "PendingIntent is not an activity");  
        default:  
            throw new AndroidRuntimeException("Unknown error code "  
                    + res + " when starting " + intent);  
      
 

相信大家对上面的这些异常信息不陌生吧,其中最熟悉的非 Unable to find explicit activity class 莫属了,如果 Activity 没有在 AndroidMainfest.xml 注册,将会抛出此异常。

那么就得思考一个问题了,插件的 Activity 并未在宿主程序的 AndroidMainfest.xml 注册,要如何才能绕过这一层检测?

前文中提到,com.didi.virtualapk.PluginManager 这个类的初始化的时候,除了 Hook 出一个 AMS 代理对象以外,还 Hook 出一个 Instrumentation 对象。代码如下:

private void hookInstrumentationAndHandler() 
    try 
        Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
        if (baseInstrumentation.getClass().getName().contains("lbe")) 
            // reject executing in paralell space, for example, lbe.
            System.exit(0);
        

        // 创建自定义的 instrumentation,重写了 newActivity() 等一些方法
        // baseInstrumentation 后面还会用到,也保存下来
        final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);

        // 获取 ActivityThread 的实例
        Object activityThread = ReflectUtil.getActivityThread(this.mContext);

        // 用自定义的 instrumentation 替换掉 ActivityThread 里面的 instrumentation
        ReflectUtil.setInstrumentation(activityThread, instrumentation);
        ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
        this.mInstrumentation = instrumentation;
     catch (Exception e) 
        e.printStackTrace();
    

既然 Activity 的启动,中间走了 Instrumentation.execStartActivity 这个方法,那么我们大概可以知道,Hook 出一个 Instrumentation 对象用来做什么了,实际上就是用来帮助启动插件的 Activity

启动插件Activity

我们 Hook 了一个 VAInstrumentation 以替代系统的 Instrumentation,这样当系统通过 ActivityThread 调用 它的的成员变量 mInstrumentation 的 newActivity() 等方法的时候,实际是调用我们 VAInstrumentationnewActivity()

实际上对于插件 Activity 启动,采用的是宿主 manifest 中占坑的方式来绕过系统校验,然后再加载真正的activity。

什么是占坑?就是构造一系列假的 Activity 替身,在 AndroidMainfest.xml 里面进行注册,以绕过检测,然后到了真正启动 Activity 的时候,再把它变回,去启动真正的目标 Activity。那么这一步是怎么做的呢?

我们可以打开核心库里面的 AndroidMainfest.xml 看看:

<application>
    <!-- Stub Activities -->
    <activity android:name=".A$1" android:launchMode="standard"/>
    <activity android:name=".A$2" android:launchMode="standard"
        android:theme="@android:style/Theme.Translucent" />

    <!-- Stub Activities -->
    <activity android:name=".B$1" android:launchMode="singleTop"/>
    <activity android:name=".B$2" android:launchMode="singleTop"/>
    <activity android:name=".B$3" android:launchMode="singleTop"/>
    <activity android:name=".B$4" android:launchMode="singleTop"/>
    <activity android:name=".B$5" android:launchMode="singleTop"/>
    <activity android:name=".B$6" android:launchMode="singleTop"/>
    <activity android:name=".B$7" android:launchMode="singleTop"/>
    <activity android:name=".B$8" android:launchMode="singleTop"/>

    <!-- Stub Activities -->
    <activity android:name=".C$1" android:launchMode="singleTask"/>
    <activity android:name=".C$2" android:launchMode="singleTask"/>
    <activity android:name=".C$3" android:launchMode="singleTask"/>
    <activity android:name=".C$4" android:launchMode="singleTask"/>
    <activity android:name=".C$5" android:launchMode="singleTask"/>
    <activity android:name=".C$6" android:launchMode="singleTask"/>
    <activity android:name=".C$7" android:launchMode="singleTask"/>
    <activity android:name=".C$8" android:launchMode="singleTask"/>

    <!-- Stub Activities -->
    <activity android:name=".D$1" android:launchMode="singleInstance"/>
    <activity android:name=".D$2" android:launchMode="singleInstance"/>
    <activity android:name=".D$3" android:launchMode="singleInstance"/>
    <activity android:name=".D$4" android:launchMode="singleInstance"/>
    <activity android:name=".D$5" android:launchMode="singleInstance"/>
    <activity android:name=".D$6" android:launchMode="singleInstance"/>
    <activity android:name=".D$7" android:launchMode="singleInstance"/>
    <activity android:name=".D$8" android:launchMode="singleInstance"/>

</application>

可以发现,在清单里面注册了一堆假的 StubActivity。 ABCD分别对应不同的启动模式,那么,我们启动插件的 Activity 的时候,是如何把它改为清单里面已注册的这些假的 Activity 名呢?

VAInstrumentation 里面,重写了 startActivity 的必经之路,就是 execStartActivity() 方法:

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) 

    // 这里面做了一系列操作,实际上就是查找插件里面第一个符合隐式条件的第一个ResolveInfo,并设置进intent
    mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
    // null component is an implicitly intent
    if (intent.getComponent() != null) 
        Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
                intent.getComponent().getClassName()));
        // !!! 重头戏在这里,用那些注册的假的StubActivity来替换真实的Activity,以绕过检测 !!!
        this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
    

    ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                intent, requestCode, options);

    return result;



private ActivityResult realExecStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) 
    ActivityResult result = null;
    try 
        Class[] parameterTypes = Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class,
        int.class, Bundle.class;
        result = (ActivityResult)ReflectUtil.invoke(Instrumentation.class, mBase,
                "execStartActivity", parameterTypes,
                who, contextThread, token, target, intent, requestCode, options);
     catch (Exception e) 
        e.printStackTrace();
    

    return result;

那么,是如何替换 StubActivity 的呢? 跟进代码:

public void markIntentIfNeeded(Intent intent) 
    if (intent.getComponent() == null) 
        return;
    

    String targetPackageName = intent.getComponent().getPackageName();
    String targetClassName = intent.getComponent().getClassName();
    // 判断是否是启动插件的Activity
    if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) 
        // 做标记
        intent.putExtra(Constants.KEY_IS_PLUGIN, true);
        // 保存真实的意图
        intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
        intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
        dispatchStubActivity(intent);
    


/**
 * 真正的转换就在这里。根据启动模式,转换对应的 StubActivity
 */
private void dispatchStubActivity(Intent intent) 
    ComponentName component = intent.getComponent();
    String targetClassName = intent.getComponent().getClassName();
    LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
    ActivityInfo info = loadedPlugin.getActivityInfo(component);
    if (info == null) 
        throw new RuntimeException("can not find " + component);
    
    int launchMode = info.launchMode;
    // 临时替换主题
    Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
    themeObj.applyStyle(info.theme, true);

    // 实际上就是这一句,完成转换
    String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
    Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
    intent.setClassName(mContext, stubActivity);

继续跟进代码:

class StubActivityInfo 
    public static final int MAX_COUNT_STANDARD = 1;
    public static final int MAX_COUNT_SINGLETOP = 8;
    public static final int MAX_COUNT_SINGLETASK = 8;
    public static final int MAX_COUNT_SINGLEINSTANCE = 8;

    public static final String corePackage = "com.didi.virtualapk.core";

    // 这个格式,就是那些假的Activity的名字
    public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d";
    public static final String STUB_ACTIVITY_SINGLETOP = "%s.B$%d";
    public static final String STUB_ACTIVITY_SINGLETASK = "%s.C$%d";
    public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D$%d";

    public final int usedStandardStubActivity = 1;
    public int usedSingleTopStubActivity = 0;
    public int usedSingleTaskStubActivity = 0;
    public int usedSingleInstanceStubActivity = 0;

    private HashMap<String, String> mCachedStubActivity = new HashMap<>();

    /**
     * 在这里根据启动模式及主题构造 StubActivity 
     */
    public String getStubActivity(String className, int launchMode, Theme theme) 
        String stubActivity= mCachedStubActivity.get(className);
        if (stubActivity != null) 
            return stubActivity;
        

        TypedArray array = theme.obtainStyledAttributes(new int[]
                android.R.attr.windowIsTranslucent,
                android.R.attr.windowBackground
        );
        boolean windowIsTranslucent = array.getBoolean(0, false);
        array.recycle();
        if (Constants.DEBUG) 
            Log.d("StubActivityInfo", "getStubActivity, is transparent theme ? " + windowIsTranslucent);
        
        stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
        switch (launchMode) 
            case ActivityInfo.LAUNCH_MULTIPLE: 
                stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
                if (windowIsTranslucent) 
                    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
                
                break;
            
            case ActivityInfo.LAUNCH_SINGLE_TOP: 
                usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
                break;
            
            case ActivityInfo.LAUNCH_SINGLE_TASK: 
                usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
                break;
            
            case ActivityInfo.LAUNCH_SINGLE_INSTANCE: 
                usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
                break;
            

            default:break;
        

        mCachedStubActivity.put(className, stubActivity);
        return stubActivity;
    

到这一步,就基本清晰了。同样的,既然变为了 StubActivity,那么真正启动的时候还得变回来才行。来看一下重写后的 newActivity() 方法:

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException 
    try 
        cl.loadClass(className);
     catch (ClassNotFoundException e) 
        // 根据 intent 类型,去获取相应的插件
        LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
        // 这里就是从Intent中取出我们刚才保存的真正的意图
        String targetClassName = PluginUtil.getTargetActivity(intent);

        Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));

        if (targetClassName != null) 
            // mBase 是未替换之前的 Instrumentation 对象,所以这个实际上是交给系统原先的 Instrumentation 对象去执行,所以这个模式其实也可以理解为与动态代理等同
            // plugin.getClassLoader() 是自己构造的一个 DexClassLoader,专门用于加载对应的apk里面的类
            Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
            activity.setIntent(intent);

            try 
                // for 4.1+
                ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
             catch (Exception ignored) 
                // ignored.
            

            return activity;
        
    

    return mBase.newActivity(cl, className, intent);

到这里,插件的 Activity 启动流程分析,就基本结束了。细节方面,没法一步到位,还需要大家边看源码边理解,这样才能看得更透彻。

Service 支持

对于 Service 的支持,采用动态代理AMS,拦截 Service 相关的请求,将其中转给Service Runtime去处理,Service Runtime会接管系统的所有操作。

对于我们动态代理AMS,在上一节 Activity支持 中已经介绍过了,那么,简单的来看一下 ActivityManagerProxy 是如何启动一个Service的。

在执行 startService 等方法的时候,AMS 代理对象会相应的来执行以下这些方法:

private Object startService(Object proxy, Method method, Object[] args) throws Throwable 
    IApplicationThread appThread = (IApplicationThread) args[0];
    Intent target = (Intent) args[1];
    ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
    if (null == resolveInfo || null == resolveInfo.serviceInfo) 
        // is host service
        return method.invoke(this.mActivityManager, args);
    

    return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);


private ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) 
    Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
    return mPluginManager.getHostContext().startService(wrapperIntent);


private Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) 
    // fill in service with ComponentName
    target.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
    String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();

    // 这里进行判断,看是交给 LocalService,还是 RemoteService 处理
    // LocalService 和 RemoteService 分别对应是否在新的进程中启动Activity
    boolean local = PluginUtil.isLocalService(serviceInfo);
    Class<? extends Service> delegate = local ? LocalService.class : RemoteService.class;
    Intent intent = new Intent();
    intent.setClass(mPluginManager.getHostContext(), delegate);
    intent.putExtra(RemoteService.EXTRA_TARGET, target);

    // 保存一下这个的Command,对应执行不同操作
    intent.putExtra(RemoteService.EXTRA_COMMAND, command);
    intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
    if (extras != null) 
        intent.putExtras(extras);
    

    return intent;

实际上包括我们调用 stopService(),AMS 代理对象最后变换后的意图,同样也是上面代码的最后两个个方法 startDelegateServiceForTargetwrapperTargetIntent(),只不过 command 不一样。

所以本质上 AMS 作为代理,不管你执行启动或者关闭插件里面的 Service,他都是调用 LocalService 或者 RemoteService 的 startService 方法,在 LocalService 或者 RemoteService 的 onStartCommand() 下,根据 command 进行相应的操作。那么我们来看一下 LocalService 的 onStartCommand() 方法:

@Override
public int onStartCommand(Intent intent, int flags, int startId) 
    if (null == intent || !intent.hasExtra(EXTRA_TARGET) || !intent.hasExtra(EXTRA_COMMAND)) 
        return START_STICKY;
    

    Intent target = intent.getParcelableExtra(EXTRA_TARGET);
    int command = intent.getIntExtra(EXTRA_COMMAND, 0);
    if (null == target || command <= 0) 
        return START_STICKY;
    

    ComponentName component = target.getComponent();
    LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);

    switch (command) 
        case EXTRA_COMMAND_START_SERVICE: 
            ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
            IApplicationThread appThread = mainThread.getApplicationThread();
            Service service;

            if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) 
                service = this.mPluginManager.getComponentsHandler().getService(component);
             else 
                try 
                    service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                    Application app = plugin.getApplication();
                    IBinder token = appThread.asBinder();
                    Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                    IActivityManager am = mPluginManager.getActivityManager();

                    attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                    service.onCreate();
                    this.mPluginManager.getComponentsHandler().rememberService(component, service);
                 catch (Throwable t) 
                    return START_STICKY;
                
            

            service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
            break;
        
        case EXTRA_COMMAND_BIND_SERVICE: 
            ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
            IApplicationThread appThread = mainThread.getApplicationThread();
            Service service = null;

            if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) 
                service = this.mPluginManager.getComponentsHandler().getService(component);
             else 
                try 
                    service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                    Application app = plugin.getApplication();
                    IBinder token = appThread.asBinder();
                    Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                    IActivityManager am = mPluginManager.getActivityManager();

                    attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                    service.onCreate();
                    this.mPluginManager.getComponentsHandler().rememberService(component, service);
                 catch (Throwable t) 
                    t.printStackTrace();
                
            
            try 
                IBinder binder = service.onBind(target);
                IBinder serviceConnection = PluginUtil.getBinder(intent.getExtras(), "sc");
                IServiceConnection iServiceConnection = IServiceConnection.Stub.asInterface(serviceConnection);
                iServiceConnection.connected(component, binder);
             catch (Exception e) 
                e.printStackTrace();
            
            break;
        
        case EXTRA_COMMAND_STOP_SERVICE: 
            Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
            if (null != service) 
                try 
                    service.onDestroy();
                 catch (Exception e) 
                    Log.e(TAG, "Unable to stop service " + service + ": " + e.toString());
                
             else 
                Log.i(TAG, component + " not found");
            
            break;
        
        case EXTRA_COMMAND_UNBIND_SERVICE: 
            Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
            if (null != service) 
                try 
                    service.onUnbind(target);
                    service.onDestroy();
                 catch (Exception e) 
                    Log.e(TAG, "Unable to unbind service " + service + ": " + e.toString());
                
             else 
                Log.i(TAG, component + " not found");
            
            break;
        
    

    return START_STICKY;

很显然,在这里面才对应去控制了插件Service的生命周期。具体代码就留给大家分析吧~~

ContentProvider 支持

动态代理 IContentProvider,拦截provider相关的请求,将其中转给Provider Runtime去处理,Provider Runtime会接管系统的所有操作。

我们来看一下 com.didi.virtualapk.internal.PluginContentResolver 这个类:

public class PluginContentResolver extends ContentResolver 
    private ContentResolver mBase;
    private PluginManager mPluginManager;
    private static Method sAcquireProvider;
    private static Method sAcquireExistingProvider;
    private static Method sAcquireUnstableProvider;

    static 
        try 
            sAcquireProvider = ContentResolver.class.getDeclaredMethod("acquireProvider",
                    new Class[]Context.class, String.class);
            sAcquireProvider.setAccessible(true);
            sAcquireExistingProvider = ContentResolver.class.getDeclaredMethod("acquireExistingProvider",
                    new Class[]Context.class, String.class);
            sAcquireExistingProvider.setAccessible(true);
            sAcquireUnstableProvider = ContentResolver.class.getDeclaredMethod("acquireUnstableProvider",
                    new Class[]Context.class, String.class);
            sAcquireUnstableProvider.setAccessible(true);
         catch (Exception e) 
            //ignored
        
    

    public PluginContentResolver(Context context) 
        super(context);
        mBase = context.getContentResolver();
        mPluginManager = PluginManager.getInstance(context);
    

    protected IContentProvider acquireProvider(Context context, String auth) 
        try 
            if (mPluginManager.resolveContentProvider(auth, 0) != null) 
                // 在这里,去 hook 一个 IContentProvider 代理对象
                return mPluginManager.getIContentProvider();
            

            return (IContentProvider) sAcquireProvider.invoke(mBase, context, auth);
         catch (Exception e) 
            e.printStackTrace();
        

        return null;
    

    // ...

这个类是在构造 LoadedPlugin 的时候创建的 PluginContext 对象里面的 getContentResolver() 里面创建的。

class PluginContext extends ContextWrapper 

    private final LoadedPlugin mPlugin;

    public PluginContext(LoadedPlugin plugin) 
        super(plugin.getPluginManager().getHostContext());
        this.mPlugin = plugin;
    

    @Override
    public ContentResolver getContentResolver() 
        // 创建代理支持
        return new PluginContentResolver(getHostContext());
    

那么,上面Hook 的 IContentProvider 代理对象,实际上是在 PluginManager 做的。

private void hookIContentProviderAsNeeded() 
    Uri uri = Uri.parse(PluginContentResolver.getUri(mContext));
    mContext.getContentResolver().call(uri, "wakeup", null, null);
    try 
        Field authority = null;
        Field mProvider = null;
        ActivityThread activityThread = (ActivityThread) ReflectUtil.getActivityThread(mContext);
        Map mProviderMap = (Map) ReflectUtil.getField(activityThread.getClass(), activityThread, "mProviderMap");
        Iterator iter = mProviderMap.entrySet().iterator();
        while (iter.hasNext()) 
            Map.Entry entry = (Map.Entry) iter.next();
            Object key = entry.getKey();
            Object val = entry.getValue();
            String auth;
            if (key instanceof String) 
                auth = (String) key;
             else 
                if (authority == null) 
                    authority = key.getClass().getDeclaredField("authority");
                    authority.setAccessible(true);
              

以上是关于Android 插件化小结的主要内容,如果未能解决你的问题,请参考以下文章

滴滴开源Android插件化框架VirtualAPK原理分析

滴滴开源Android插件化框架VirtualAPK原理分析

Android插件化开发之Hook StartActivity方法

Android插件化原理解析——Hook机制之动态代理

Android插件化原理解析——Hook机制之动态代理

Android 插件化插件化原理 ( JVM 内存数据 | 类加载流程 )