【Android】插件化框架Virtual APK實現(xiàn)原理解析

1 . 前言

本文主要針對Virtual APK的實現(xiàn)做講解。

2 . 重要的知識點

Activity啟動流程(AMS)
DexClassLoader
動態(tài)代理
反射
廣播的動態(tài)注冊

3 . 宿主App的實現(xiàn)

中心思想:

對插件APK進行解析,獲取插件APK的信息
在框架初始化時,對一系列系統(tǒng)組件和接口進行替換,從而對Activity、Service、ContentProvider的啟動和生命周期進行修改和監(jiān)控,達到欺瞞系統(tǒng)或者劫持系統(tǒng)的目的來啟動插件Apk的對應(yīng)組件。

3.1 插件Apk的解析和加載

插件Apk的加載在PluginManager#loadPlugin方法,在加載完成后,會生成一個LoadedPlugin對象并保存在Map中。LoadedPlugin里保存里插件Apk里絕大多數(shù)的重要信息和一個DexClassLoader,這個DexClassLoader是作為插件Apk的類加載器使用。

看下LoadedPlugin的具體實現(xiàn),注釋標明了各個屬性的含義:

public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
        // PluginManager
        this.mPluginManager = pluginManager;
        // 宿主Context
        this.mHostContext = context;
        // 插件apk路徑
        this.mLocation = apk.getAbsolutePath();
        this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
        // 插件apk metadata
        this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
        // 插件apk package信息
        this.mPackageInfo = new PackageInfo();
        this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
        this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
        // 插件apk 簽名信息
        if (Build.VERSION.SDK_INT >= 28
            || (Build.VERSION.SDK_INT == 27 && Build.VERSION.PREVIEW_SDK_INT != 0)) { // Android P Preview
            try {
                this.mPackageInfo.signatures = this.mPackage.mSigningDetails.signatures;
            } catch (Throwable e) {
                PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
                this.mPackageInfo.signatures = info.signatures;
            }
        } else {
            this.mPackageInfo.signatures = this.mPackage.mSignatures;
        }
        // 插件apk 包名
        this.mPackageInfo.packageName = this.mPackage.packageName;
        // 如果已經(jīng)加載過相同的apk, 拋出異常
        if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {
            throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
        }

        this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
        this.mPackageInfo.versionName = this.mPackage.mVersionName;
        this.mPackageInfo.permissions = new PermissionInfo[0];
        this.mPackageManager = createPluginPackageManager();
        this.mPluginContext = createPluginContext(null);
        this.mNativeLibDir = getDir(context, Constants.NATIVE_DIR);
        this.mPackage.applicationInfo.nativeLibraryDir = this.mNativeLibDir.getAbsolutePath();
        // 創(chuàng)建插件的資源管理器
        this.mResources = createResources(context, getPackageName(), apk);
        // 創(chuàng)建 一個dexClassLoader
        this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());

        tryToCopyNativeLib(apk);

        // Cache instrumentations
        Map<ComponentName, InstrumentationInfo> instrumentations = new HashMap<ComponentName, InstrumentationInfo>();
        for (PackageParser.Instrumentation instrumentation : this.mPackage.instrumentation) {
            instrumentations.put(instrumentation.getComponentName(), instrumentation.info);
        }
        this.mInstrumentationInfos = Collections.unmodifiableMap(instrumentations);
        this.mPackageInfo.instrumentation = instrumentations.values().toArray(new InstrumentationInfo[instrumentations.size()]);

        // Cache activities
        // 保存插件apk的Activity信息
        Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
        for (PackageParser.Activity activity : this.mPackage.activities) {
            activity.info.metaData = activity.metaData;
            activityInfos.put(activity.getComponentName(), activity.info);
        }
        this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
        this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);

        // Cache services
        // 保存插件apk的Service信息
        Map<ComponentName, ServiceInfo> serviceInfos = new HashMap<ComponentName, ServiceInfo>();
        for (PackageParser.Service service : this.mPackage.services) {
            serviceInfos.put(service.getComponentName(), service.info);
        }
        this.mServiceInfos = Collections.unmodifiableMap(serviceInfos);
        this.mPackageInfo.services = serviceInfos.values().toArray(new ServiceInfo[serviceInfos.size()]);

        // Cache providers
        // 保存插件apk的ContentProvider信息
        Map<String, ProviderInfo> providers = new HashMap<String, ProviderInfo>();
        Map<ComponentName, ProviderInfo> providerInfos = new HashMap<ComponentName, ProviderInfo>();
        for (PackageParser.Provider provider : this.mPackage.providers) {
            providers.put(provider.info.authority, provider.info);
            providerInfos.put(provider.getComponentName(), provider.info);
        }
        this.mProviders = Collections.unmodifiableMap(providers);
        this.mProviderInfos = Collections.unmodifiableMap(providerInfos);
        this.mPackageInfo.providers = providerInfos.values().toArray(new ProviderInfo[providerInfos.size()]);

        // 將所有靜態(tài)注冊的廣播全部改為動態(tài)注冊
        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);
            }
        }
        this.mReceiverInfos = Collections.unmodifiableMap(receivers);
        this.mPackageInfo.receivers = receivers.values().toArray(new ActivityInfo[receivers.size()]);

        // try to invoke plugin's application
        // 創(chuàng)建插件apk的Application對象
        invokeApplication();
    }

3.2 Activity的啟動處理及生命周期管理

Virtual APK啟動插件APK中Activity的整體方案:

Hook Instrumentaion 和主線程Halder的callback,在重要啟動過程節(jié)點對Intent或Activity進行替換
在宿主APP中預(yù)先設(shè)置一些插樁Activity,這些插樁Activity并不會真正的啟動,而是對AMS進行欺騙。如果啟動的Activity是插件APK中的,則根據(jù)該Actiivty的啟動模式選擇合適的插樁Activity, AMS在啟動階段對插樁Activity處理后,在創(chuàng)建Activity實例階段,實際創(chuàng)建插件APK中要啟動的Activity。

3.2.1 插樁Activity的聲明:

插樁Activity有很多個,挑一些看一下:

 <!-- 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" 

3.2.2 hook Instrumentation

將系統(tǒng)提供的Instrumentation替換為自定義的VAInstrumentation,將主線程Handler的Callback也替換為VAInstrumentation(VAInstrumentation 實現(xiàn)了Handler.Callback接口)

     protected void hookInstrumentationAndHandler() {
        try {
            // 獲取當(dāng)前進程的activityThread
            ActivityThread activityThread = ActivityThread.currentActivityThread();
            // 獲取當(dāng)前進程的Instrumentation
            Instrumentation baseInstrumentation = activityThread.getInstrumentation();
            // 創(chuàng)建自定義Instrumentation
            final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
            // 將當(dāng)前進程原有的Instrumentation對象替換為自定義的
            Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
            // 將當(dāng)前進程原有的主線程Hander的callback替換為自定義的
            Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
            Reflector.with(mainHandler).field("mCallback").set(instrumentation);
            this.mInstrumentation = instrumentation;
            Log.d(TAG, "hookInstrumentationAndHandler succeed : " + mInstrumentation);
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }

3.2.3 啟動Activity時對AMS進行欺騙

如果我們熟悉Activity啟動流程的話,我們一定知道Activity的啟動和生命周期管理,都間接通過Instrumentation進行管理的。--如果不熟悉也沒關(guān)系,可以看我之前寫的AMS系列文章,看完保證秒懂(霧)。VAInstrumentation重寫了這個類的一些重要方法,我們根據(jù)Activity啟動流程一個一個說

3.2.3.1 execStartActivity

這個方法有很多個重載,挑其中一個:

    public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode) {
        // 對原始Intent進行處理
        injectIntent(intent);
        return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode);
    }

injectIntent方法對Intent的處理在ComponentsHandler#markIntentIfNeeded方法,對原始Intent進行解析,獲取目標Actiivty的包名和類名,如果目標Activity的包名和當(dāng)前進程不同且該包名對應(yīng)的LoadedPlugin對象存在,則說明它是我們加載過的插件APK中的Activity,則對該Intent的目標進行替換:

   public void markIntentIfNeeded(Intent intent) {
        ...
        String targetPackageName = intent.getComponent().getPackageName();
        String targetClassName = intent.getComponent().getClassName();
        // 判斷是否需要啟動的是插件Apk的Activity
        if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
            ...
            // 將原始Intent的目標Acitivy替換為預(yù)設(shè)的插樁Activity中的一個
            dispatchStubActivity(intent);
        }
    }

dispatchStubActivity方法根據(jù)原始Intent的啟動模式選擇合適的插樁Activity,將原始Intent中的類名修改為插樁Activity的類名,示例代碼:

 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;
            }

3.2.3.2 newActivity

如果只是對原始Intent進行替換,那么最終啟動的會是插樁Activity,這顯然達不到啟動插件Apk中Acitivty的目的,在Activity實例創(chuàng)建階段,還需要對實際創(chuàng)建的Actiivty進行替換,方法在VAInstrumentation#newActivity:

@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));

        } catch (ClassNotFoundException e) {
            ComponentName component = PluginUtil.getComponent(intent);

            String targetClassName = component.getClassName();
            Log.i(TAG, String.format("newActivity[%s : %s/%s]", className, component.getPackageName(), targetClassName));

            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);
            // 使用在LoadedPlugin對象中創(chuàng)建的DexClassLoader進行類加載,該ClassLoader指向插件APK所在路徑
            Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
            activity.setIntent(intent);
            // 插件Activity實例創(chuàng)建后,將Resource替換為插件APK的資源
            Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());

            return newActivity(activity);
        }

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

如果我們啟動的是插件APK里的Activity,這個方法的Catch語句塊是一定會被執(zhí)行的,因為入?yún)lassName已經(jīng)被替換為插樁Activity的,但是我們只是在宿主App的AndroidManifest.xml中定義了這些Actiivty,并沒有真正的實現(xiàn)。在進入Catch語句塊后,使用LoadedPlugin中保存的DexClassloader進行Activity的創(chuàng)建。

3.2.3.3 AMS對插件APK中的Activity管理

看到這里,可能就會有同學(xué)有問題了,你把要啟動的Activity給替換了,但是AMS中不是還記錄的是插樁Actiivty么,那么這個Activity實例后續(xù)跟AMS的交互怎么辦?那豈不是在AMS中的記錄找不到了?放心,不會出現(xiàn)這個問題的。復(fù)習(xí)之前AMS系列文章我們就會知道,AMS中對Activity管理的依據(jù)是一個叫appToken的Binder實例,在客戶端對應(yīng)的token會在Instrumentation#newActivity執(zhí)行完成后調(diào)用Activity#attach方法傳遞給Actiivty。

這也是為什么對AMS進行欺騙這種插件化方案可行的原因,因為后續(xù)管理是使用的token,如果Android使用className之類的來管理的話,恐怕這種方案就不太好實現(xiàn)了。

3.2.3.4 替換Context、applicaiton、Resources

在系統(tǒng)創(chuàng)建插件Activity的Context創(chuàng)建完成之后,需要將其替換為PluginContext,PluginContext和Context的區(qū)別是其內(nèi)部保存有一個LoadedPlugin對象,方便對Context中的資源進行替換。代碼在VAInstrumentaiton#injectActivity,調(diào)用處在VAInstrumentaiton#callActivityOnCreate

protected void injectActivity(Activity activity) {
        final Intent intent = activity.getIntent();
        if (PluginUtil.isIntentFromPlugin(intent)) {
            Context base = activity.getBaseContext();
            try {
                LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
                Reflector.with(base).field("mResources").set(plugin.getResources());
                Reflector reflector = Reflector.with(activity);
                reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
                reflector.field("mApplication").set(plugin.getApplication());

                // set screenOrientation
                ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
                if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                    activity.setRequestedOrientation(activityInfo.screenOrientation);
                }

                // for native activity
                ComponentName component = PluginUtil.getComponent(intent);
                Intent wrapperIntent = new Intent(intent);
                wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
                wrapperIntent.setExtrasClassLoader(activity.getClassLoader());
                activity.setIntent(wrapperIntent);

            } catch (Exception e) {
                Log.w(TAG, e);
            }
        }
    }

3.3 Service的處理

Virtual APK啟動插件APK中Activity的整體方案:

使用動態(tài)代理代理宿主APP中所有關(guān)于Service的請求
判斷是否為插件APK中的Service,如果不是,則說明為宿主 APP中的,直接打開即可
如果是插件APK中的Service,則判斷是否為遠端Service,如果是遠端Service,則啟動RemoteService,并在其StartCommand方法中根據(jù)所代理的生命周期方法進行處理
如果是本地Service,則啟動LocalService,并在其StartCommand方法中根據(jù)所代理的生命周期方法進行處理

3.3.1 插件化框架初始化時代理系統(tǒng)的IActivityManager

IActivityManager是AMS的實現(xiàn)接口,它的實現(xiàn)類分別是ActivityManagerService和其proxy
這里我們需要代理的是Proxy,實現(xiàn)方法在PluginManager#hookSystemServices

 protected void hookSystemServices() {
        try {
            Singleton<IActivityManager對象> defaultSingleton;
            // 獲取IActivityManager對象
            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();
            // 創(chuàng)建activityManager對象的動態(tài)代理
            IActivityManager activityManager對象的動態(tài)代理 = (IActivityManager) Proxy.newProxyInstance(mContext.getClassLoader(), new Class[] { IActivityManager.class },
                createActivityManagerProxy(origin));

            // 使用動態(tài)代理替換之前的IActivityManager對象實例
            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);
        }
    }

通過將動態(tài)代理對系統(tǒng)創(chuàng)建的ActivityManager的proxy進行替換,這樣,調(diào)用AMS方法時,會轉(zhuǎn)到ActivityManagerProxy的invoke方法,并根據(jù)方法名對Service的生命周期進行管理,生命周期方法較多,挑選其中一個:

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("startService".equals(method.getName())) {
            try {
                return startService(proxy, method, args);
            } catch (Throwable e) {
                Log.e(TAG, "Start service error", e);
            }
        }
startService:

 protected 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) {
            //  插件中沒找到,說明是宿主APP自己的Service
            return method.invoke(this.mActivityManager, args);
        }
        // 啟動插件APK中的Service
        return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
    }

startDelegateServiceForTarget中會調(diào)用wrapperTargetIntent處理,最終在RemoteService或者LocalService的onStartCommand中對Service的各生命周期處理。

需要注意的是,在RemoteService中需要重新對APK進行解析和裝載,生成LoadedPlugin,因為它運行在另一個進程中。

這也說明插件APK的Service進程如果聲明了多個是無效的,因為他們最終都會運行在宿主RemoteService所在進程。

3.4 ContentProvider的處理

ContentProvicer的處理和Service是類似的,不多說了。

4 . 插件App的實現(xiàn)

插件APP理論上并不需要做什么特殊處理,唯一需要注意的是資源文件的沖突問題,因此,需要在插件工程app目錄下的build.gradle中添加如下代碼:

virtualApk {
    packageId = 0x6f // the package id of Resources.
    targetHost = '../../VirtualAPK/app' // the path of application module in host project.
    applyHostMapping = true //optional, default value: true.
}

它的作用是在插件APK編譯時對資源ID進行重寫,處理方法在ResourceCollector.groovy文件的collect方法:

 def collect() {

        //1、First, collect all resources by parsing the R symbol file.
        parseResEntries(allRSymbolFile, allResources, allStyleables)

        //2、Then, collect host resources by parsing the host apk R symbol file, should be stripped.
        parseResEntries(hostRSymbolFile, hostResources, hostStyleables)

        //3、Compute the resources that should be retained in the plugin apk.
        filterPluginResources()

        //4、Reassign the resource ID. If the resource entry exists in host apk, the reassign ID
        //   should be same with value in host apk; If the resource entry is owned by plugin project,
        //   then we should recalculate the ID value.
        reassignPluginResourceId()

        //5、Collect all the resources in the retained AARs, to regenerate the R java file that uses the new resource ID
        vaContext.retainedAarLibs.each {
            gatherReservedAarResources(it)
        }
    }

首先獲取插件app和宿主app的資源集合,然后尋找其中沖突的資源id進行修改,修改id是 reassignPluginResourceId方法:

private void reassignPluginResourceId() {
        // 對資源ID根據(jù)typeId進行排序
        resourceIdList.sort { t1, t2 ->
            t1.typeId - t2.typeId
        }

        int lastType = 1
        // 重寫資源ID
        resourceIdList.each {
            if (it.typeId < 0) {
                return
            }
            def typeId = 0
            def entryId = 0
            typeId = lastType++
            pluginResources.get(it.resType).each {
                it.setNewResourceId(virtualApk.packageId, typeId, entryId++)
            }
        }
    }

資源ID是一個32位的16進制整數(shù),前8位代表app, 接下來8位代表typeId(string、layout、id等),從01開始累加,后面四位為資源id,從0000開始累加。

對資源ID的遍歷使用了雙重循環(huán),外層循環(huán)從01開始對typeId進行遍歷,內(nèi)層循環(huán)從0000開始對typeId對應(yīng)的資源ID進行遍歷,并且在內(nèi)層循環(huán)調(diào)用setNewResourceId進行重寫:

  public void setNewResourceId(packageId, typeId, entryId) {
        newResourceId = packageId << 24 | typeId << 16 | entryId
    }

packageId是我們在build.gradle中定義的virtualApk.packageId,將其左移24位,與資源id的前8位對應(yīng),typeId與第9-16位對應(yīng),后面是資源id

這樣,在插件app編譯過程中就完成了沖突資源id的替換,后面也不會有沖突的問題了

5 . 總結(jié)

回顧整個Virtual APK的實現(xiàn),其實邏輯并不是特別復(fù)雜,但是可以看到作者們對AMS以及資源加載、類加載器等API的熟悉程度,如果不是對這些知識體系特別精通的話,是很難實現(xiàn)的,甚至連思路都不可能有,這也是我們學(xué)習(xí)源碼的意義所在。

來自:https://www.androidos.net.cn/doc/2021/10/13/1024.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容