FrameWork源碼解析(9)-插件化框架VirtualApk之插件加載

主目錄見:Android高級進階知識(這是總目錄索引)
框架地址:VirtualApk
在線源碼查看:AndroidXRef

上一篇文章插件化框架VirtualApk之初始化我們已經講了框架初始化的內容,主要就是hook Instrumentation類和hook AMS系統服務。今天這篇就是加載插件apk的內容,是插件化非常重要的一步,廢話不多說,直接開寫。。。

一.插件加載

在宿主工程初始化之后,我們就要加載插件工程了,還是從用法下手:

  PluginManager pluginManager = PluginManager.getInstance(base);
        File apk = new File(Environment.getExternalStorageDirectory(), "Test.apk");
        if (apk.exists()) {
            try {
                pluginManager.loadPlugin(apk);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

在前面我們已經實例化過PluginManager實例了,因為單例模式,所以直接會返回PluginManager實例,所以我們跟進PluginManager#loadPlugin()方法:

 public void loadPlugin(File apk) throws Exception {
        if (null == apk) {
            throw new IllegalArgumentException("error : apk is null.");
        }

        if (!apk.exists()) {
            throw new FileNotFoundException(apk.getAbsolutePath());
        }

        LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk);
        if (null != plugin) {
            this.mPlugins.put(plugin.getPackageName(), plugin);
            // try to invoke plugin's application
            plugin.invokeApplication();
        } else {
            throw  new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
        }
    }

這個方法主要做了兩件事,一個是根據apk構建LoadedPlugin類實例,然后就是調用插件的Application,我們一個一個來看,首先我們看LoadedPlugin#create()方法做了啥:

 public static LoadedPlugin create(PluginManager pluginManager, Context host, File apk) throws Exception {
        return new LoadedPlugin(pluginManager, host, apk);
    }

這個方法并沒有做什么復雜的操作,就是實例化了LoadedPlugin類,參數分別是PluginManager實例,宿主工程的上下文對象Context,最后就是apk文件,接著我們看LoadedPlugin類的構造函數寫了啥:

    LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws PackageParser.PackageParserException {
        this.mPluginManager = pluginManager;
        this.mHostContext = context;
        this.mLocation = apk.getAbsolutePath();
        this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
        this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
        this.mPackageInfo = new PackageInfo();
        this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
        this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
        this.mPackageInfo.signatures = this.mPackage.mSignatures;
        this.mPackageInfo.packageName = this.mPackage.packageName;
        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 = new PluginPackageManager();
        this.mPluginContext = new PluginContext(this);
        this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
        this.mResources = createResources(context, apk);
        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
        Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
        for (PackageParser.Activity activity : this.mPackage.activities) {
            activityInfos.put(activity.getComponentName(), activity.info);
        }
        this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
        this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);

        // Cache services
        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
        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()]);

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

            try {
                BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
                for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
                    this.mHostContext.registerReceiver(br, aii);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        this.mReceiverInfos = Collections.unmodifiableMap(receivers);
        this.mPackageInfo.receivers = receivers.values().toArray(new ActivityInfo[receivers.size()]);
    }

我們看到這個方法比較長,但是都很重要,所以我們這里不省略,一部分一部分來進行說明,首先我們看下面這部分:

        this.mPluginManager = pluginManager;
        this.mHostContext = context;
        this.mLocation = apk.getAbsolutePath();
        this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
        this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;

這部分首先是將pluginManagercontext賦值給LoadedPlugin類的相應屬性,接著獲取apk的絕對路徑進行保存,然后調用PackageParserCompat#parsePackage()方法解析apk包中的AndroidManifest.xml文件(如果對PackageParser解析APK不是很清楚的話那么可以參考文章PackageParser解析APK(上),PackageParser解析APK(下))將解析的結果保存在PackageParser.Package類中,最后保存application meta-data。代碼往下:

        this.mPackageInfo = new PackageInfo();
        this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
        this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
        this.mPackageInfo.signatures = this.mPackage.mSignatures;
        this.mPackageInfo.packageName = this.mPackage.packageName;
        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];

這部分代碼比較簡單,主要是實例化一個PackageInfo類,然后往里面屬性放進相應的值,大家應該一看就知道,不做贅述,我們直接來看下一部分代碼:

       this.mPackageManager = new PluginPackageManager();
        this.mPluginContext = new PluginContext(this);
        this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
        this.mResources = createResources(context, apk);
        this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());

這里面有很多重要的方法,首先我們來說第一個實例化PluginPackageManager類,這里主要要將插件中的PackageManager替換成宿主程序的PackageManager,這樣的話,就可以用宿主的PackageManager來全權接管獲取插件中包apk的信息,因為解析插件apk用的是宿主的上下文對象。接著就是實例化PluginContext類,這個類繼承ContextWrapper,主要作用也是將插件中的上下文對象替換成這個PluginContext類實例,然后里面的方法進行重寫,替換成我們自己構建的對象。然后就是獲取so文件的路徑。我們往下看這部分會看到createResources()createClassLoader()方法,這兩個方法特別重要,我們這邊重點講解,首先來看createResources()方法。

二.資源的加載

  @WorkerThread
    private static Resources createResources(Context context, File apk) {
        if (Constants.COMBINE_RESOURCES) {
//如果插件資源合并到宿主里面去的情況,插件可以訪問宿主的資源
            Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
            ResourcesManager.hookResources(context, resources);
            return resources;
        } else {
  //插件使用獨立的Resources,不與宿主有關系,無法訪問到宿主的資源
            Resources hostResources = context.getResources();
            AssetManager assetManager = createAssetManager(context, apk);
            return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
        }
    }

加載資源這塊,如果不清楚的話可以先看AssetManager加載資源過程,我們知道如果插件不調用宿主的資源的話,那么我們這里只要創建一個單獨的Resources實例給插件即可,但是如果要想插件能訪問到宿主的資源的話,那么我們這里就得將資源添加到同一個AssetManager中,我們跟進ResourcesManager#createResources()方法:

 public static synchronized Resources createResources(Context hostContext, String apk) {
        Resources hostResources = hostContext.getResources();
        Resources newResources = null;
        AssetManager assetManager;
        try {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                assetManager = AssetManager.class.newInstance();
                ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", hostContext.getApplicationInfo().sourceDir);
            } else {
                assetManager = hostResources.getAssets();
            }
            ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", apk);
            List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
            for (LoadedPlugin plugin : pluginList) {
                ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", plugin.getLocation());
            }
            if (isMiUi(hostResources)) {
                newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
            } else if (isVivo(hostResources)) {
                newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
            } else if (isNubia(hostResources)) {
                newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
            } else if (isNotRawResources(hostResources)) {
                newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
            } else {
                // is raw android resources
                newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
            }
            // lastly, sync all LoadedPlugin to newResources
            for (LoadedPlugin plugin : pluginList) {
                plugin.updateResources(newResources);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return newResources;

    }

這部分代碼也會長一點,而且涉及到了兼容性方面的問題,我們也拆開來看,首先我們來看下面這段代碼:

  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                assetManager = AssetManager.class.newInstance();
                ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", hostContext.getApplicationInfo().sourceDir);
            } else {
                assetManager = hostResources.getAssets();
            }

這里為啥要區別Android L之前的版本和之后的版本呢?這里主要是在Resources對象最終是通過AssetManager對象來獲取資源的,不過會先通過資源id查找到資源文件名。resources.arsc包含了資源的id索引,但是在Android L之前,資源resources.arsc的解析在前面已經解析完了,這樣的話,在addAssetPath方法將插件資源加入到資源路徑列表里后,但是在resources.arsc中并沒有插件資源的id索引,這樣會導致Resources查找不到資源。所以滴滴這個插件化框架想出一個方法,就是構造一個新的 AssetManager,將宿主和加載過的插件的所有 apk 全都添加一遍,然后再調用hookResources方法將新的 Resources替換回原來的(這個地方其實還有其他做法,但是這個地方不展開了)。

           ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", apk);
            List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
            for (LoadedPlugin plugin : pluginList) {
                ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", plugin.getLocation());
            }

這段代碼就是將當前的插件已經加載過的插件的資源重新添加一遍。接著就是很蛋疼的代碼了,這應該也是寫這個框架當初疼了不少時間的問題:

     if (isMiUi(hostResources)) {
                newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
            } else if (isVivo(hostResources)) {
                newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
            } else if (isNubia(hostResources)) {
                newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
            } else if (isNotRawResources(hostResources)) {
                newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
            } else {
                // is raw android resources
                newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
            }

因為不同廠商都重寫了Resources類,所以這個地方做一個適配。最后將各個插件的Resources類進行更新即可。這樣我們的資源就加載完成了。

三.類加載

 private static ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) {
        File dexOutputDir = context.getDir(Constants.OPTIMIZE_DIR, Context.MODE_PRIVATE);
        String dexOutputPath = dexOutputDir.getAbsolutePath();
        DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);

        if (Constants.COMBINE_CLASSLOADER) {
            try {
                DexUtil.insertDex(loader);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return loader;
    }

如果對類加載不是非常清楚的,可以先看看ClassLoader及dex加載過程,這樣我們就知道為什么用DexClassLoader這個類加載器來加載類了,這里也是一樣,如果要讓插件工程能調用宿主工程的類的話,那么就要將插件的dex和宿主的dex進行合并放在dexElements中。我們來看看DexUtil#insertDex()方法:

 public static void insertDex(DexClassLoader dexClassLoader) throws Exception {
        Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
        Object newDexElements = getDexElements(getPathList(dexClassLoader));
        Object allDexElements = combineArray(baseDexElements, newDexElements);
        Object pathList = getPathList(getPathClassLoader());
        ReflectUtil.setField(pathList.getClass(), pathList, "dexElements", allDexElements);

        insertNativeLibrary(dexClassLoader);

    }

這個類的作用就是將宿主的 DexPathList中的dexElements屬性值取出來,然后調用combineArray將兩個數組進行合并,然后設置到宿主的dexElements中去。最后我們看到又調用了insertNativeLibrary()方法,這個方法是做什么的呢?

 private static synchronized void insertNativeLibrary(DexClassLoader dexClassLoader) throws Exception {
        if (sHasInsertedNativeLibrary) {
            return;
        }
        sHasInsertedNativeLibrary = true;

        Object basePathList = getPathList(getPathClassLoader());
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
            List<File> nativeLibraryDirectories = (List<File>) ReflectUtil.getField(basePathList.getClass(),
                    basePathList, "nativeLibraryDirectories");
            nativeLibraryDirectories.add(Systems.getContext().getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE));

            Object baseNativeLibraryPathElements = ReflectUtil.getField(basePathList.getClass(), basePathList, "nativeLibraryPathElements");
            final int baseArrayLength = Array.getLength(baseNativeLibraryPathElements);

            Object newPathList = getPathList(dexClassLoader);
            Object newNativeLibraryPathElements = ReflectUtil.getField(newPathList.getClass(), newPathList, "nativeLibraryPathElements");
            Class<?> elementClass = newNativeLibraryPathElements.getClass().getComponentType();
            Object allNativeLibraryPathElements = Array.newInstance(elementClass, baseArrayLength + 1);
            System.arraycopy(baseNativeLibraryPathElements, 0, allNativeLibraryPathElements, 0, baseArrayLength);

            Field soPathField;
            if (Build.VERSION.SDK_INT >= 26) {
                soPathField = elementClass.getDeclaredField("path");
            } else {
                soPathField = elementClass.getDeclaredField("dir");
            }
            soPathField.setAccessible(true);
            final int newArrayLength = Array.getLength(newNativeLibraryPathElements);
            for (int i = 0; i < newArrayLength; i++) {
                Object element = Array.get(newNativeLibraryPathElements, i);
                String dir = ((File)soPathField.get(element)).getAbsolutePath();
                if (dir.contains(Constants.NATIVE_DIR)) {
                    Array.set(allNativeLibraryPathElements, baseArrayLength, element);
                    break;
                }
            }

            ReflectUtil.setField(basePathList.getClass(), basePathList, "nativeLibraryPathElements", allNativeLibraryPathElements);
        } else {
            File[] nativeLibraryDirectories = (File[]) ReflectUtil.getFieldNoException(basePathList.getClass(),
                    basePathList, "nativeLibraryDirectories");
            final int N = nativeLibraryDirectories.length;
            File[] newNativeLibraryDirectories = new File[N + 1];
            System.arraycopy(nativeLibraryDirectories, 0, newNativeLibraryDirectories, 0, N);
            newNativeLibraryDirectories[N] = Systems.getContext().getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
            ReflectUtil.setField(basePathList.getClass(), basePathList, "nativeLibraryDirectories", newNativeLibraryDirectories);
        }
    }

這個方法也比較長,我們看到這邊也有一個版本兼容的判斷,我們先來看看Android L之前的情況:

  File[] nativeLibraryDirectories = (File[]) ReflectUtil.getFieldNoException(basePathList.getClass(),
                    basePathList, "nativeLibraryDirectories");
  final int N = nativeLibraryDirectories.length;
  File[] newNativeLibraryDirectories = new File[N + 1];
  System.arraycopy(nativeLibraryDirectories, 0, newNativeLibraryDirectories, 0, N);
  newNativeLibraryDirectories[N] = Systems.getContext().getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
  ReflectUtil.setField(basePathList.getClass(), basePathList, "nativeLibraryDirectories", newNativeLibraryDirectories);

這段代碼邏輯比較清晰,首先是獲取到DexPathList類中的nativeLibraryDirectories屬性,這個屬性主要是存儲so文件的目錄,因為有可能會有多個,所以是個數組,這個方法主要是將插件的so文件路徑添加進nativeLibraryDirectories,然后重新設置給DexPathList中。同樣的如果Android L之后,前面是一樣的只不過有一段代碼不同:

 this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, 140 suppressedExceptions);

DexPathList類中增加了一個屬性nativeLibraryPathElements,具體的細節這里也就不做說明了,如果這塊有不明白的可以給私信。程序緊接著會調用 tryToCopyNativeLib(apk)方法,就是將apk中的so文件拷貝到指定的位置。

四.動態注冊廣播

我們知道LoadedPlugin構造函數會緊接著緩存instrumentations,activities,services,providers,receivers。這部分代碼比較簡單,我就直接跳過了。我們直接來講這段動態注冊廣播的代碼:

   Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
        for (PackageParser.Activity receiver : this.mPackage.receivers) {
            receivers.put(receiver.getComponentName(), receiver.info);

            try {
                BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
                for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
                    this.mHostContext.registerReceiver(br, aii);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

我們知道,四大組件中的廣播有靜態注冊廣播和動態注冊廣播之分,我們這里可以采用動態注冊廣播的方法,將插件的廣播注冊到宿主的上下文Context中,這樣的話,就可以在插件中發送廣播,然后宿主可以接收得到了。所以相對于其他組件,廣播還是比較容易處理的。

五.創建插件Application

PluginManager#loadPlugin()方法的最后會調用LoadedPlugin#invokeApplication方法來創建插件的Application,這部分代碼稍微簡單點,我們一起來瀏覽下:

public void invokeApplication() {
        if (mApplication != null) {
            return;
        }

        // make sure application's callback is run on ui thread.
        RunUtil.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mApplication = makeApplication(false, mPluginManager.getInstrumentation());
            }
        }, true);
    }

這個地方在ui線程又調用了makeApplication()方法:

private Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
        if (null != this.mApplication) {
            return this.mApplication;
        }

        String appClass = this.mPackage.applicationInfo.className;
        if (forceDefaultAppClass || null == appClass) {
            appClass = "android.app.Application";
        }

        try {
            this.mApplication = instrumentation.newApplication(this.mClassLoader, appClass, this.getPluginContext());
            instrumentation.callApplicationOnCreate(this.mApplication);
            return this.mApplication;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

這個方法是調用初始化時候hook掉的instrumentation來創建插件工程的實現了Application類的子類,然后調用它的onCreate()方法。到這里,加載插件的代碼已經解析完畢了。

總結:加載插件的代碼我們已經分析完成了,如果有分析不完整的地方,大家可以提意見,我會補充或者進行解答,或者有錯誤大家可以提出來。下幾篇就會講關于四大組件啟動流程要做的一些處理了,希望大家一起進步。

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

推薦閱讀更多精彩內容