• android 动态加载


    编译期:资源和代码的编译

    运行时:资源和代码的加载

    解决了以上2个关键问题,之后如何实现插件呢

    分析android是如何编译的

    1.aapt  资源编译依赖这个命令行 

    1)android.jar

    2)引用一个已经存在的apk包作为依赖资源参与编译

    资源编译中,对组件的类名、方法引用会导致运行期的反射调用

    3)java层面的常量ID会记录在R.java中,参与他们之后的代码编译阶段

    ,R类生成的每一个Int类型由4个字节组成,第一个字节代表package id,第二个字节为分类,三四字节为类内ID

    //android.jar中的资源,其PackageID为0x01
    public static final int cancel = 0x01040000;
    
    //用户app中的资源,PackageID总是0x7F
    public static final int zip_code = 0x7f090f2e;

    所以我们修改aapt后,可以给每个子apk中的资源分配不同头字节packageid,这样就不会产生冲突


    代码编译

    classpath:java源码编译中需要找齐所有依赖项目,用来指定去哪个目录、文件、jar包寻找依赖。
    混淆:安全起见参考手册

    实现

    两个问题需要解决:资源如何访问和代码如何访问
    两类:针对插件子工程的编译流程改造和运行时动态加载改造

    针对插件的资源编译,我们需要考虑到以下几点:

    • 使用-I参数对宿主的apk进行引用。

      据此,插件的资源、xml布局中就可以使用宿主的资源和控件、布局类了。

    • 为aapt增加--apk-module参数。

      如前所述,资源ID其实有一个PackageID的内部字段。我们为每个插件工程指定独特的PackageID字段,这样根据资源ID就很容易判明,此资源需要从哪个插件apk中去查找并加载了。在后文的资源加载部分会有进一步阐述。

    • 为aapt增加--public-R-path参数。

      按照对android.jar包中资源使用的常规手段,引用系统资源可使用它的R类的全限定名android.R来引用具体ID,以便和当前项目中的R类区分。插件对于宿主的资源引用,当然也可以使用base.package.name.R来完成。但由于历史原因,各子BU的“插件”代码是从主app中解耦独立出去的,资源引用还是直接使用当前工程的R。如果改为标准模式,则当前大量遗留代码中R都需要酌情改为base.R,工程量大并且容易出错,未来对bu开发人员的使用也有点不够“透明”。因此我们在设计上做了让步,额外增加--public-R-path参数,为aapt指明了base.R的位置,让它在编译期间把base的资源ID定义在插件的R类中完整复制一份,这样插件工程即可和之前一样,完全不用在乎资源来自于宿主或者自身,直接使用即可。当然这样做带来的副作用就是宿主和插件的资源不应有重名

    针对插件的代码编译,需要考虑以下几点:

    • classpath

      对于插件的编译来说,除了对android.jar以及自己需要的第三方库进行依赖之外,还需要依赖宿主导出的base.jar类库。同时对宿主的混淆也提出了要求:宿主的所有public/protected都可能被插件依赖,所以这些接口都不允许被混淆。

    • 混淆。

      插件工程在混淆的时候,当然也要把宿主的混淆后jar包作为参考库导入。

    自此,编译期所有重要步骤的技术方案都已经确定,剩下的工作就只是把插件apk导入到先一步生成好的base.apk中并重新进行签名对齐而已。

    万事俱备,只欠表演。接下来我们看看在运行时插件们是如何登台亮相的。

    运行时资源的加载

    平常我们使用资源,都是通过AssetManager类和Resources类来访问的。获取它们的方法位于Context类中。

    Context.java

    /** Return an AssetManager instance for your application's package. */
    public abstract AssetManager getAssets();
    
    /** Return a Resources instance for your application's package. */
    public abstract Resources getResources();

    它们是两个抽象方法,具体的实现在ContextImpl类中。ContextImpl类中初始化Resources对象后,后续Context各子类包括Activity、Service等组件就都可以通过这两个方法读取资源了。

    ContextImpl.java

    private final Resources mResources;
    
    @Override
    public AssetManager getAssets() {
       return getResources().getAssets();
    }
    
    @Override
    public Resources getResources() {
       return mResources;
    }

    既然我们已经知道一个资源ID应该从哪个apk去读取(前面在编译期我们已经在资源ID第一个字节标记了资源所属的package),那么只要我们重写这两个抽象方法,即可指导应用程序去正确的地方读取资源。

    至于读取资源,AssetManager有一个隐藏方法addAssetPath,可以为AssetManager添加资源路径。

    /**
    * Add an additional set of assets to the asset manager.  This can be
    * either a directory or ZIP file.  Not for use by applications.  Returns
    * the cookie of the added asset, or 0 on failure.
    * {@hide}
    */
    public final int addAssetPath(String path) {
       synchronized (this) {
           int res = addAssetPathNative(path);
           makeStringBlocks(mStringBlocks);
           return res;
       }
    }

    我们只需反射调用这个方法,然后把插件apk的位置告诉AssetManager类,它就会根据apk内的resources.arsc和已编译资源完成资源加载的任务了。

    以上我们已经可以做到加载插件资源了,但使用了一大堆定制类实现。要做到“无缝”体验,还需要一步:使用Instrumentation来接管所有Activity、Service等组件的创建(当然也就包含了它们使用到的Resources类)。

    话说Activity、Service等系统组件,都会经由android.app.ActivityThread类在主线程中执行。ActivityThread类有一个成员叫mInstrumentation,它会负责创建Activity等操作,这正是注入我们的修改资源类的最佳时机。通过篡改mInstrumentation为我们自己的InstrumentationHook,每次创建Activity的时候顺手把它的mResources类偷天换日为我们的DelegateResources,以后创建的每个Activity都拥有一个懂得插件、懂得委托的资源加载类啦!

    当然,上述替换都会针对Application的Context来操作。

    运行时类的加载

    类的加载相对比较简单。与Java程序的运行时classpath概念类似,Android的系统默认类加载器PathClassLoader也有一个成员pathList,顾名思义它从本质来说是一个List,运行时会从其间的每一个dex路径中查找需要加载的类。既然是个List,一定就会想到,给它追加一堆dex路径不就得了?实际上,Google官方推出的MultiDex库就是用以上原理实现的。下面代码片段展示了修改pathList路径的细节:

    MultiDex.java

    private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
         File optimizedDirectory)
                 throws IllegalArgumentException, IllegalAccessException,
                 NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
        /* The patched class loader is expected to be a descendant of
        * dalvik.system.BaseDexClassLoader. We modify its
        * dalvik.system.DexPathList pathList field to append additional DEX
        * file entries.
        */
        Field pathListField = findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
             new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
    }

    当然,针对不同Android版本,类加载方式略有不同,可以参考MultiDex源码做具体的区别处理。

    至此,之前提出的四个根本性问题,都已经有了具体的解决方案。剩下的就是编码!

    编码主要分为三部分:

    • 对aapt工具的修改。

    • gradle打包脚本的实现。

    • 运行时加载代码的实现。

    具体实现可以参考我们在GitHub上的开源项目DynamicAPK

     
  • 相关阅读:
    strpbrk函数
    memchr函数
    memset函数
    strrev函数
    strncmp函数
    strset函数
    strtok函数
    计算机经典书籍之程序设计语言
    spring自定义bean的作用域
    lucene文章
  • 原文地址:https://www.cnblogs.com/zhengtu2015/p/5823335.html
Copyright © 2020-2023  润新知