VirtualApk 插件化原理分析

插件化需要解决的几个问题:

  • 插件的类是如何加载的
  • 插件的资源文件如何获取
  • 插件中的四大组件如何启动(Activity、Service、BroadCastReceiver、ContentProvider)

本问以VirtualApk为例 分析插件化实现的几个关键点:

一、VirualApk中的几个重要的类

1、LoadedPlugin 代表一个被加载的插件

LoadedPlugin 中持有插件Apk的保存位置,插件自己独有的ClassLoader、Resources、PluginContext(上下文)和PluginPakageManager(插件包管理器)

public class LoadedPlugin {

    //PluginManager实例
    protected PluginManager mPluginManager;
     //宿主的Context
     protected Context mHostContext;

     //保存被加载的apk的位置
     protected final String mLocation;
     //插件自己的Context
     protected Context mPluginContext;
     //插件自己的Resource
     protected Resources mResources;
     //插件自己的ClassLoader
     protected ClassLoader mClassLoader;
     //插件自己的PackageManager
     protected PluginPackageManager mPackageManager;
     //插件的Application
     protected Application mApplication;

    //插件中注册的Activity、Service、Receiver、ContentProvider
    protected Map<ComponentName, ActivityInfo> mActivityInfos;
    protected Map<ComponentName, ServiceInfo> mServiceInfos;
    protected Map<ComponentName, ActivityInfo> mReceiverInfos;
    protected Map<ComponentName, ProviderInfo> mProviderInfos;
    protected Map<String, ProviderInfo> mProviders; // key is authorities of provider
}

2、PluginContext

PluginContext 插件中使用的上下文,通过复写 getApplication()、getClassLoader()、getResources()、getAssets()、getContentResolver()、getPackageManager() 等方法,返回插件的Application、ClassLoader、Resoures、AssetManager、ContentProvider、PackageManager。

class PluginContext extends ContextWrapper {

    private final LoadedPlugin mPlugin;

    public PluginContext(LoadedPlugin plugin) {
        super(plugin.getPluginManager().getHostContext());
        this.mPlugin = plugin;
    }
    
    public PluginContext(LoadedPlugin plugin, Context base) {
        super(base);
        this.mPlugin = plugin;
    }

    @Override
    public Context getApplicationContext() {
        return this.mPlugin.getApplication();
    }



    private Context getHostContext() {
        return getBaseContext();
    }

    @Override
    public ContentResolver getContentResolver() {
        return new PluginContentResolver(getHostContext());
    }

    @Override
    public ClassLoader getClassLoader() {
        return this.mPlugin.getClassLoader();
    }


    @Override
    public PackageManager getPackageManager() {
        return this.mPlugin.getPackageManager();
    }

  
    @Override
    public Resources getResources() {
        return this.mPlugin.getResources();
    }

    @Override
    public AssetManager getAssets() {
        return this.mPlugin.getAssets();
    }

    @Override
    public Resources.Theme getTheme() {
        return this.mPlugin.getTheme();
    }

   
 }


3、PluginManager

PlugInManager是VirtualApk的大管家,持有宿主的Content、application,维护着所有加载过的插件LoadedApk,hook的系统的Instrumentation、ActivityManager、ContentProvider

public class PluginManager {
    
    //HostApp的Content和Application
    protected final Context mContext;
    protected final Application mApplication;

    //维护所有加载过的LoadedPlugin插件
    protected final Map<String, LoadedPlugin> mPlugins = new ConcurrentHashMap<>();

    //hook的系统的Instumentation
    protected VAInstrumentation mInstrumentation; // Hooked instrumentation
    //hook系统的ActivityManager
    protected IActivityManager mActivityManager; // Hooked IActivityManager binder
    //hook系统的ContentProvider
    protected IContentProvider mIContentProvider; // Hooked IContentProvider binder

}

二、插件中ClassLoader、Resources 是如何创建的,so是如何处理的。

2.1、插件的ClassLoader

 public class LoadedPlugin {
    protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception {
        File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
        String dexOutputPath = dexOutputDir.getAbsolutePath();
        DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);

        if (Constants.COMBINE_CLASSLOADER) {
            DexUtil.insertDex(loader, parent, libsDir);
        }

        return loader;
    }
}

LoadedPlugin中createClassLoader()方法创建自己的ClassLoader();

  • 以宿主的classLoader为父classLoader,创建了一个DexClassLoader作为插件的classLoader,并指定了dexPath为插件apk的路径。
  • 如何指定了Constants.COMBINE_CLASSLOADER,则会修改宿主classLoader,将插件classLoader的dexElements和宿主ClassLoader的dexElements的合并(宿主在前、插件在后),使宿主也可以加载插件中的类。

2.2、插件的Resources是如何创建的。

  protected Resources createResources(Context context, String packageName, File apk) throws Exception {
        if (Constants.COMBINE_RESOURCES) {
            //(3)COMBINE_RESOURCES模式下 resource处理
            return
            ResourcesManager.createResources(context, packageName, apk);
        } else {
            //(1)利用插件的assetManager 创建一个Resources对象
            Resources hostResources = context.getResources();
            AssetManager assetManager = createAssetManager(context, apk);
            return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
        }
    }
    
   
  • 插件中创建Resources核心操作:创建一个属于插件的AssetManager,利用assetManager和宿主的DisplayMetric和configuration 创建一个独立的Resources实例作为插件的Resources
  • 创建中创建AssetManager实例:创建一个空的AssetManager,利用反射调用addAssetPath方法,加载插件中的资源(包括res和assets)
 //创建插件的AssetManager实例
     protected AssetManager createAssetManager(Context context, File apk) throws Exception {
        AssetManager am = AssetManager.class.newInstance();
        Reflector.with(am).method("addAssetPath", String.class).call(apk.getAbsolutePath());
        return am;
    }
  • 如果开启了COMBINE_RESOURCES合并资源模式,处理会稍微复杂一些。

首先在创建插件AssetManager时,会通过addAssetPath,将宿主的资源目录,正在被加载的apk,已经加载过的所有插件的apk资源 都添加到新创建的AssetManager

其次利用符合的AssetManager创建的Resources作为插件的Resources,可以访问访问宿主和所有插件的Resource资源

再次 还有hook 替换掉宿主Context中的Resrouces

这样,宿主和插件使用同一个Resources对象,可以访问宿主和插件中的资源

2.3、插件的so是如何处理的

VirtualApk 会在宿主的内部路径创建一个“valibs”文件夹,来保存插件的so文件。

安装插件时,会将插件apk中的so,选择一个合适的arm-abi拷贝到valibs下。

 protected void tryToCopyNativeLib(File apk) throws Exception {
        PluginUtil.copyNativeLib(apk, mHostContext, mPackageInfo, mNativeLibDir);
    }
    
     
public static void copyNativeLib(File apk, Context context, PackageInfo packageInfo, File nativeLibDir) throws Exception {
    long startTime = System.currentTimeMillis();
    ZipFile zipfile = new ZipFile(apk.getAbsolutePath());

    try {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            for (String cpuArch : Build.SUPPORTED_ABIS) {
                if (findAndCopyNativeLib(zipfile, context, cpuArch, packageInfo, nativeLibDir)) {
                    return;
                }
            }
            
        } else {
            if (findAndCopyNativeLib(zipfile, context, Build.CPU_ABI, packageInfo, nativeLibDir)) {
                return;
            }
        }
        
        findAndCopyNativeLib(zipfile, context, "armeabi", packageInfo, nativeLibDir);

    } finally {
        zipfile.close();
        Log.d(TAG, "Done! +" + (System.currentTimeMillis() - startTime) + "ms");
    }
}

三、 四大组件是如何启动的。

3.1、Activity的启动

VirtualApk也是采用占坑位的方式,在宿主的Manifest中预先注册了多个Activity。
利用VmInstrumentation类Hook了系统的Instrumenation类。
为什么要hookInstrumentation类呢?因为Activity的创建、启动、以及各个声明周期的回调 都会经过Instrumentation,所以在Instrumentation中对Activity进行替换是最佳的一个hook点。

public class VAInstrumentation extends Instrumentation implements Handler.Callback {
     @Override
    public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode) {
        injectIntent(intent);
        return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode);
    }

   protected void injectIntent(Intent intent) {

        //(1)将隐式的Intent 转换为显式的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()));
            // resolve intent with Stub Activity if needed
            //(2)将Intent中启动的activity 替换成宿主中占位的Activity,并将真正要启动的Activity信息 以参数的形式 保存在原Intent中
            this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
        }
    }
}

Activity启动前 会调用execStartActivity()方法,
在此方法中将待启动的插件中的Activity 替换为宿主中占位的Activity
-(1)将隐式的Intent 转换为显式的Intent

  • (2)将Intent中启动的activity 替换成宿主中占位的Activity,并将真正要启动的Activity信息 以参数的形式 保存在原Intent中

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

        String targetPackageName = intent.getComponent().getPackageName();
        String targetClassName = intent.getComponent().getClassName();
        // search map and return specific launchmode stub 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);
        }
    }

执行execStartActivity()后,启动Activity的信息会通过IPC 传递给ActivityManagerService,ActivityManagerService 对Activity进行校验通过之后,会通知原app进程来创建Activity实例,最终会调用Instrumentation.newActivity()

public class VAInstrumentation extends Instrumentation implements Handler.Callback {
    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        try {
            cl.loadClass(className);
            Log.i(TAG, String.format("newActivity[%s]", className));
            
            //(1) 创建占位Activity时,因为宿主中 只对StubActivity进行了声明,而并没有对应的Class,所以此时会异常,异常时 尝试创建插件的Activity
        } catch (ClassNotFoundException e) {
            ComponentName component = PluginUtil.getComponent(intent);
            
            if (component == null) {
                return newActivity(mBase.newActivity(cl, className, intent));
            }
    
            String targetClassName = component.getClassName();
            Log.i(TAG, String.format("newActivity[%s : %s/%s]", className, component.getPackageName(), targetClassName));
    
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);
    
            if (plugin == null) {
                // Not found then goto stub activity.
             
                Log.i(TAG, "Not found. starting the stub activity: " + StubActivity.class);
                return newActivity(mBase.newActivity(cl, StubActivity.class.getName(), intent));
            }

            //(2) 创建 利用插件的ClassLoader 创建插件的Activity实例
            
            Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
            activity.setIntent(intent);

            //(3)替换插件Activity中的Resources
            // for 4.1+
            Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());
    
            return newActivity(activity);
        }

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

在Instrumentation的newActivity方法中,找出要启动的真正的Activity类 创建实例,并返回给ActivityManagerService.

  • (1) 创建占位Activity时,因为宿主中 只对StubActivity进行了声明,而并没有对应的Class,所以此时会异常,异常时 尝试创建插件的Activity
  • (2) 创建 利用插件的ClassLoader 创建插件的Activity实例
  • (3)替换插件Activity中的Resources

3.2、Service的启动

VirtualApk中 Mainifest中预留了两个Service(LocalService和RemoteService),作为占位的本进程Service和独立进程Service。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.didi.virtualapk.core">
    <application>
        <!-- Local Service running in main process -->
        <service android:exported="false" android:name="com.didi.virtualapk.delegate.LocalService" />

        <!-- Daemon Service running in child process -->
        <service android:exported="false" android:name="com.didi.virtualapk.delegate.RemoteService" android:process=":daemon">
            <intent-filter>
                <action android:name="${applicationId}.intent.ACTION_DAEMON_SERVICE" />
            </intent-filter>
        </service>
    </application>
</manifest>

VirtualApk hook了系统的ActivityManagerProxy类,利用动态代理技术,接管了Service的启动。

 /**
     * hookSystemServices, but need to compatible with Android O in future.
     */
    protected void hookSystemServices() {
        try {
            Singleton<IActivityManager> defaultSingleton;
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                defaultSingleton = Reflector.on(ActivityManager.class).field("IActivityManagerSingleton").get();
            } else {
                defaultSingleton = Reflector.on(ActivityManagerNative.class).field("gDefault").get();
            }
            IActivityManager origin = defaultSingleton.get();
            IActivityManager activityManagerProxy = (IActivityManager) Proxy.newProxyInstance(mContext.getClassLoader(), new Class[] { IActivityManager.class },
                createActivityManagerProxy(origin));

            // Hook IActivityManager from ActivityManagerNative
            Reflector.with(defaultSingleton).field("mInstance").set(activityManagerProxy);

            if (defaultSingleton.get() == activityManagerProxy) {
                this.mActivityManager = activityManagerProxy;
                Log.d(TAG, "hookSystemServices succeed : " + mActivityManager);
            }
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
  • 启动插件中的Service时,会将intent中的Component替换为宿主中占位的LocalServie或者RemoteService
  • 真正的插件Service和Service的操作 都保存在Intent的参数中
  • 待LocalService或者RemoteService启动之后,onStartCommand()方法中取出插件Service相关的参数,然后在LocalService或者RemoteService中 创建插件Service的实例,并调用相应的回调方法。
public class LocalService extends Service {
    private static final String TAG = Constants.TAG_PREFIX + "LocalService";

    /**
     * The target service, usually it's a plugin service intent
     */
    public static final String EXTRA_TARGET = "target";
    public static final String EXTRA_COMMAND = "command";
    public static final String EXTRA_PLUGIN_LOCATION = "plugin_location";

    public static final int EXTRA_COMMAND_START_SERVICE = 1;
    public static final int EXTRA_COMMAND_STOP_SERVICE = 2;
    public static final int EXTRA_COMMAND_BIND_SERVICE = 3;
    public static final int EXTRA_COMMAND_UNBIND_SERVICE = 4;


    private PluginManager mPluginManager;

    @Override
    public IBinder onBind(Intent intent) {
        return new Binder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mPluginManager = PluginManager.getInstance(this);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        

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

        switch (command) {
            case EXTRA_COMMAND_START_SERVICE: {
                ActivityThread mainThread = ActivityThread.currentActivityThread();
                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.currentActivityThread();
                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) {
                        Log.w(TAG, t);
                    }
                }
                try {
                    IBinder binder = service.onBind(target);
                    IBinder serviceConnection = PluginUtil.getBinder(intent.getExtras(), "sc");
                    IServiceConnection iServiceConnection = IServiceConnection.Stub.asInterface(serviceConnection);
                    if (Build.VERSION.SDK_INT >= 26) {
                        iServiceConnection.connected(component, binder, false);
                    } else {
                        Reflector.QuietReflector.with(iServiceConnection).method("connected", ComponentName.class, IBinder.class).call(component, binder);
                    }
                } catch (Exception e) {
                    Log.w(TAG, e);
                }
                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;
    }

}

3.3、ContentProvider的启动

VirtualApk中预留了RemoteContentProvider的坑位ContentProivder

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.didi.virtualapk.core">
    
    <application>
        <provider
            android:exported="false"
            android:name="com.didi.virtualapk.delegate.RemoteContentProvider"
            android:authorities="${applicationId}.VirtualAPK.Provider"
            android:process=":daemon" />

    </application>

</manifest>

ContentProvider的支持依然是通过代理分发。

Cursor bookCursor = getContentResolvewr().query(bookUri,new String[]{"_id","name"},null,null,null)

这里用到了PluginContext,在生成Activity、Service的时候,为其设置的Context都为PluginContext对象。

所以当你调用getContentResolver时,调用的为PluginContext的getContentResolver。

  @Override
    public ContentResolver getContentResolver() {
        return new PluginContentResolver(getHostContext());
    }

返回的是一个PluginContentResolver对象,当我们调用query方法时,会辗转调用到
ContentResolver.acquireUnstableProvider方法。该方法被PluginContentResolver中复写:

  @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    @Override
    protected IContentProvider acquireUnstableProvider(Context context, String auth) {
        if (mPluginManager.resolveContentProvider(auth, 0) != null) {
            return mPluginManager.getIContentProvider();
        }
        return super.acquireUnstableProvider(context, auth);
    }

PluginManager.getIContentProvider()返回的又是什么呢?

PluginManager.getIContentProvider() 返回的其实是RemoteContentProvider的代理对象IContentProviderProxy。

VirtualApk hook了宿主的RemoteContentProvider,利用动态代理接管了RemoteContentProvider的操作。

protected void hookIContentProviderAsNeeded() {
        Uri uri = Uri.parse(RemoteContentProvider.getUri(mContext));
        //(1)提前请求一些RemoteContentProvider 创建RemoteContentProvider的实例
        mContext.getContentResolver().call(uri, "wakeup", null, null);
        try {
            Field authority = null;
            Field provider = null;
            ActivityThread activityThread = ActivityThread.currentActivityThread();
            Map providerMap = Reflector.with(activityThread).field("mProviderMap").get();
            Iterator iter = providerMap.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);
                    }
                    auth = (String) authority.get(key);
                }
                if (auth.equals(RemoteContentProvider.getAuthority(mContext))) {
                    if (provider == null) {
                        provider = val.getClass().getDeclaredField("mProvider");
                        provider.setAccessible(true);
                    }
        
                    IContentProvider rawProvider = (IContentProvider) provider.get(val);
                    
                    //(2)利用动态代理 接管RemoteContentProvider的操作
                    IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);
                    mIContentProvider = proxy;
                    Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);
                    break;
                }
            }
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
  • (1)提前请求一些RemoteContentProvider 创建RemoteContentProvider的实例
  • (2)利用动态代理 接管RemoteContentProvider的操作

有了IContentProviderProxy对象 就可以拦截query、insert、update、delete操作,把用户调用的uri,替换为占坑provider(RemouteContentPrpvider)的uri,再把原本的uri作为参数拼接在占坑provider的uri后面。

RemouteContentProvider中收到请求后,
取出原本的uri,拿到auth,在通过加载plugin得到providerInfo,反射生成provider对象,在调用其attachInfo方法,即完成了ContentProvider的代理分发。

3.4、BroadCastReciever的注册

BroadcastReceiver的处理比较简单,只需将插件中静态注册的BroadCastReceiver转化成动态注册即可。

 // Register broadcast receivers dynamically
        Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
        for (PackageParser.Activity receiver : this.mPackage.receivers) {
            receivers.put(receiver.getComponentName(), receiver.info);
    
            BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
            for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
                this.mHostContext.registerReceiver(br, aii);
            }
        }

3.5、小结:

  • Activity:在宿主apk中提前占几个坑,然后通过“欺上瞒下”(这个词好像是360之前的ppt中提到)的方式,启动插件apk的Activity;因为要支持不同的launchMode以及一些特殊的属性,需要占多个坑。
  • Service:通过代理Service的方式去分发;主进程和其他进程,VirtualAPK使用了两个代理Service。
  • BroadcastReceiver:静态转动态。
  • ContentProvider:通过一个代理Provider进行分发。

四、插件中的资源如何被加载

4.1、首先明确什么地方会用到Resouces

Resources是context的行为能力,所有Context的子类 才有可能取到Resources,即 Application、Activity、Service。
另外 ContentProvider 会在attach()方法时 传递进去一个Context对象。

所以天生持有Resource(或者Context)的类 有四个:Application、Activity、Service、ContentProvider

4.2 Application、Activity、Service、ContentProvider中Resources的替换

PluginContext中保存了插件对应的Resource

  • Application 是如何替换Resouces的
    创建Application对象时,传递的是PluginContext
  • Activity 中 是如何替换Resouces资源的
    VAInstrumentation中 创建Activity 之后, 立即为Activity设置了mResources(PluginContext.Resouces)
  • Service 中是如何替换资源的
    Service.attach() 传递的是PluginContext
  • ContentProvider 是如何替换Resources资源的
    contentProvider.attachInfo() 传递的是PluginContenxt

五、 插件的ClassLoader是如何生效的?

5.1、四大组件创建时ClassLoader的选择

  • 创建Application时 用的是Plugin.classLoader()
  • 创建Activity时 classLoader 用的是Plugin.getClassLoader()
  • 创建Service时 用的Plugin.ClassLoader()
  • 创建BroadCastReceiver时, 用的是Plugin.getClassLoader()
  • 创建ContentProvider时, 用的是??? 没有用插件的ClassLoader?

5.2、四大组件之外的类如何加载

对于宿主apk,插件是一个相对独立的整体,宿主调用插件,只能通过四大组件来调用(启动插件activity、service、broadcast、contentProvider)。

我们知道,通常情况下

在A类中加载B类,那么加载B类用到的ClassLoadert通常情况下就是A的classLoader

由此得出,我们只需要启动插件Activity时 选用插件的ClassLoader,那么Activity内用到的类,默认都是插件ClassLoader来加载的。

六、其他

6.1、插件apk 是如何解析的?

利用系统自带的PackageParser.parsePackage(),来解析一个APK

6.2、坑位中的Activity是如何匹配的。

Manifest中预埋了 各种模式的Activity,分为ABCD四种:

  • A 代表 stand模式的Activity, 2个
  • B 代表 singleTop模式的Activity ,8个
  • C 代表 singleTask模式的Activity,8个
  • D 代表 singleInstance模式的Activity,8个

StubActivityInfo.getStubActivity() 根据launchMode 来选择未使用的预留Activity坑位。


 <application>
        <activity android:exported="false" android:name="com.didi.virtualapk.delegate.StubActivity" android:launchMode="standard"/>
        <!-- Stub Activities -->
        <activity android:exported="false" android:name=".A$1" android:launchMode="standard"/>
        <activity android:exported="false" android:name=".A$2" android:launchMode="standard"
            android:theme="@android:style/Theme.Translucent" />

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

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

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

6.3、virtualapk-gradle-plugin 做了什么事情

virtualapk-gradle-plugin 分为host和plugIn两部分

  • 宿主-plugin
apply plugin: 'com.didi.virtualapk.host'
1、generateDependencies()记录速录依赖库信息,生成version.txt、allVersions.txt两个文件
2、backupHostR 备份R.txt文件 生成Host_R.txt
3、backupProguardMapping 备份混淆文件,生成mapping.txt
生成四个文件 version.txt、allVersions.txt、Host_R.txt、mapping.txt
  • 插件-plugin
插件工程引入
apply plugin: 'com.didi.virtualapk.plugin'
插件app moudle的build.gradle中配置
virtualApk {
    packageId = 0x6f             // The package id of Resources.
    targetHost='source/host/app' // The path of application module in host project.
    applyHostMapping = true      // [Optional] Default value is true. 
}

virtualapk.plugin 根据宿主生成的 version.txt、Host_R.txt、mapping.txt 对插件的依赖、资源进行了去重操作。重复的依赖或资源以宿主依赖和资源为主,去除插件的对应项。

七、参考资料

virtualAPK 四大组件启动

virualApk资源的替换
virtualApk-gradle-plugin

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容