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

96
ZJ_Rocky
2017.12.28 16:14 字数 2144

主目录见: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()方法。到这里,加载插件的代码已经解析完毕了。

总结:加载插件的代码我们已经分析完成了,如果有分析不完整的地方,大家可以提意见,我会补充或者进行解答,或者有错误大家可以提出来。下几篇就会讲关于四大组件启动流程要做的一些处理了,希望大家一起进步。

Android高级进阶
Web note ad 1