动态加载技术研究 - 由浅入深

动态加载技术

介绍

在程序运行的时候,加载一些程序自身原本不存在的可执行文件并运行这些文件里的代码逻辑。

动态调用外部的Dex文件则是完全没有问题的。在APK文件中往往有一个或者多个Dex文件,我们写的每一句代码都会被编译到这些文件里面,Android应用运行的时候就是通过执行这些Dex文件完成应用的功能的。虽然一个APK一旦构建出来,我们是无法更换里面的Dex文件的,但是我们可以通过加载外部的Dex文件来实现动态加载,这个外部文件可以放在外部存储,或者从网络下载。

真正的动态加载应该是
  1. 应用在运行的时候通过加载一些本地不存在的可执行文件实现一些特定的功能;
  2. 这些可执行文件是可以替换的;
  3. 更换静态资源(比如换启动图、换主题、或者用服务器参数开关控制广告的隐藏现实等)不属于 动态加载;
  4. Android中动态加载的核心思想是动态调用外部的 dex文件,极端的情况下,Android APK自身带有的Dex文件只是一个程序的入口(或者说空壳),所有的功能都通过从服务器下载最新的Dex文件完成;

类型

Android项目中,动态加载技术按照加载的可执行文件的不同大致可以分为两种:

动态加载so库

Android中NDK中其实就使用了动态加载,动态加载.so库并通过JNI调用其封装好的方法。后者一般是由C/C++编译而成,运行在Native层,效率会比执行在虚拟机层的Java代码高很多,所以Android中经常通过动态加载.so库来完成一些对性能比较有需求的工作(比如T9搜索、或者Bitmap的解码、图片高斯模糊处理等)。此外,由于so库是由C/C++编译而来的,只能被反编译成汇编代码,相比中dex文件反编译得到的Smali代码更难被破解,因此so库也可以被用于安全领域

动态加载dex/jar/apk文件(现在动态加载普遍说的是这种);

“基于ClassLoader的动态加载dex/jar/apk文件”,就是我们上面提到的“在Android中动态加载由Java代码编译而来的dex包并执行其中的代码逻辑”,这是常规Android开发比较少用到的一种技术,目前网络上大多文章说到的动态加载指的就是这种

Android项目中,所有Java代码都会被编译成dex文件,Android应用运行时,就是通过执行dex文件里的业务代码逻辑来工作的。使用动态加载技术可以在Android应用运行时加载外部的dex文件,而通过网络下载新的dex文件并替换原有的dex文件就可以达到不安装新APK文件就升级应用(改变代码逻辑)的目的。同时,使用动态加载技术,一般来说会使得Android开发工作变得更加复杂,这中开发方式不是官方推荐的,不是目前主流的Android开发方式,Github 和 StackOverflow 上面外国的开发者也对此不是很感兴趣,外国相关的教程更是少得可怜,目前只有在大天朝才有比较深入的研究和应用,特别是一些SDK组件项目和 BAT家族 的项目上,Github上的相关开源项目基本是国人在维护,偶尔有几个外国人请求更新英文文档。

原理

无论上面的哪种动态加载,其实基本原理都是在程序运行时加载一些外部的可执行的文件,然后调用这些文件的某个方法执行业务逻辑。需要说明的是,因为文件是可执行的(so库或者dex包,也就是一种动态链接库),出于安全问题,Android并不允许直接加载手机外部存储这类noexec(不可执行)存储路径上的可执行文件。
对于这些外部的可执行文件,在Android应用中调用它们前,都要先把他们拷贝到data/packagename/内部储存文件路径,确保库不会被第三方应用恶意修改或拦截,然后再将他们加载到当前的运行环境并调用需要的方法执行相应的逻辑,从而实现动态调用。
动态加载的大致过程就是:

  1. 把可执行文件(.so/dex/jar/apk)拷贝到应用APP内部存储;
  2. 加载可执行文件;
  3. 调用具体的方法执行业务逻辑;

动态加载技术就是使用类加载器加载相应的apk、dex、jar(必须含有dex文件),再通过反射获得该apk、dex、jar内部的资源(class、图片、color等等)进而供宿主app使用。

好处

在Android App中,一个应用程序dex文件的方法数最大不能超过65536个,否则,你的app将出异常了,那么如果越大的项目那肯定超过了,像美团、支付宝等都是使用动态加载技术,支付宝在去年的一个技术分享类会议上就推崇让应用程序插件化,而美团也公布了他们的解决方案:Dex自动拆包和动态加载技术。所以使用动态加载技术解决此类问题。而它的优点可以让应用程序实现插件化、插拔式结构,对后期维护作用那不用说了,比如在不发布新版本的情况下更新某些模块。

实现方式

基础
类加载器
  • PathClassLoader - 只能加载已经安装的apk,即/data/app目录下的apk。
  • DexClassLoader - 能加载手机中未安装的apk、jar、dex,只要能找到对应的路径。
原理
  1. 在AndroidManifest.xml文件中的<manifest>标签中定义shareUserId属性,应用从一开始安装在Android系统上时,系统都会给它分配一个linux user id,之后该应用在今后都将运行在独立的一个进程中,其它应用程序不能访问它的资源,那么如果两个应用的sharedUserId相同,那么它们将共同运行在相同的linux进程中,从而便可以数据共享、资源访问了。所以我们在宿主app和插件app的manifest上都定义一个相同的sharedUserId。
  2. 通过shareUserId获取到宿主app的所有插件。
  3. 通过类加载器加载对应的插件。
  4. 使用反射获取插件中的资源,比如R类下的color,drawable等。
简单动态加载模式
  • 获取能够加载的.dex文件,无论加载.jar,还是.apk,其实都和加载.dex是等价的,Android能加载.jar和.apk,是因为它们都包含有.dex,直接加载.apk文件时,ClassLoader也会自动把.apk里的.dex解压出来。
    • 通过JDK的编译命令javac把Java代码编译成.class文件,再使用jar命令把.class文件封装成.jar文件,这与编译普通Java程序的时候完全一样。
      之后再用Android SDK的DX工具把.jar文件优化成.dex文件(在“android-sdk\build-tools\具体版本\”路径下)
    dx --dex --output=target.dex origin.jar // target.dex
    
  • 加载并调用.dex里面的方法,通过DexClassLoader加载后使用反射或者接口方式调用里面的方法
    实例使用DexClassLoader的代码
  File optimizedDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "test_dexloader.jar");// 外部路径
  File dexOutputDir = this.getDir("dex", 0);// 无法直接从外部路径加载.dex文件,需要指定APP内部路径作为缓存目录(.dex文件会被解压到此目录)
  DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(), null, getClassLoader());
使用反射的方式

使用DexClassLoader加载进来的类,我们本地并没有这些类的源码,所以无法直接调用,不过可以通过反射的方法调用,简单粗暴。

DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(), dexOutputDir.getAbsolutePath(), null, getClassLoader());
            Class libProviderClazz = null;
            try {
                libProviderClazz = dexClassLoader.loadClass("me.kaede.dexclassloader.MyLoader");
                // 遍历类里所有方法
                Method[] methods = libProviderClazz.getDeclaredMethods();
                for (int i = 0; i < methods.length; i++) {
                    Log.e(TAG, methods[i].toString());
                }
                Method start = libProviderClazz.getDeclaredMethod("func");// 获取方法
                start.setAccessible(true);// 把方法设为public,让外部可以调用
                String string = (String) start.invoke(libProviderClazz.newInstance());// 调用方法并获取返回值
                Toast.makeText(this, string, Toast.LENGTH_LONG).show();
            } catch (Exception exception) {
                // Handle exception gracefully here.
                exception.printStackTrace();
            }
使用接口的方式

毕竟.dex文件也是我们自己维护的,所以可以把方法抽象成公共接口,把这些接口也复制到主项目里面去,就可以通过这些接口调用动态加载得到的实例的方法了。

pulic interface IFunc{
    public String func();
}

// 调用
IFunc ifunc = (IFunc)libProviderClazz;
String string = ifunc.func();
Toast.makeText(this, string, Toast.LENGTH_LONG).show();
代理Activity模式

使用插件APK里的Activity需要解决两个问题:

如何使插件APK里的Activity具有生命周期

主项目APK注册一个代理Activity(命名为ProxyActivity),ProxyActivity是一个普通的Activity,但只是一个空壳,自身并没有什么业务逻辑。每次打开插件APK里的某一个Activity的时候,都是在主项目里使用标准的方式启动ProxyActivity,再在ProxyActivity的生命周期里同步调用插件中的Activity实例的生命周期方法,从而执行插件APK的业务逻辑。
ProxyActivity + 没注册的Activity = 标准的Activity

如何使插件APK里的Activity具有上下文环境(使用R资源)
  1. 插件里需要用到的新资源都通过纯Java代码的方式创建(包括XML布局、动画、点九图等)
  2. 获取一个AssetManager实例,使用其“addAssetPath”方法加载APK(里的资源),再使用DisplayMetrics、Configuration、CompatibilityInfo实例一起创建我们想要的Resources实例
    try {  
        AssetManager assetManager = AssetManager.class.newInstance();  
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
        addAssetPath.invoke(assetManager, mDexPath);  
        mAssetManager = assetManager;  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
    Resources superRes = super.getResources();  
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),  
            superRes.getConfiguration()); 

资源冲突问题

其实这种方式加载进来的res资源并不是融入到主项目里面来,主项目里的res资源是保存在ContextImpl里面的Resources实例,整个项目共有,而新加进来的res资源是保存在新创建的Resources实例的,也就是说ProxyActivity其实有两套res资源,并不是把新的res资源和原有的res资源合并了(所以不怕R.id重复),对两个res资源的访问都需要用对应的Resources实例,这也是开发时要处理的问题。(其实应该有3套,Android系统会加载一套framework-res.apk资源,里面存放系统默认Theme等资源)

限制

  1. 实际运行的Activity实例其实都是ProxyActivity,并不是真正想要启动的Activity;
  2. ProxyActivity只能指定一种LaunchMode,所以插件里的Activity无法自定义LaunchMode;
  3. 不支持静态注册的BroadcastReceiver;
  4. 往往不是所有的apk都可作为插件被加载,插件项目需要依赖特定的框架,还有需要遵循一定的"开发规范";

实际应用中可能要处理的问题

插件APK的管理后台

使用动态加载的目的,就是希望可以绕过APK的安装过程升级应用的功能,如果插件APK是打包在主项目内部的那动态加载纯粹是多次一举。更多的时候我们希望可以在线下载插件APK,并且在插件APK有新版本的时候,主项目要从服务器下载最新的插件替换本地已经存在的旧插件。为此,我们应该有一个管理后台,它大概有以下功能:

  1. 上传不同版本的插件APK,并向APP主项目提供插件APK信息查询功能和下载功能;
  2. 管理在线的插件APK,并能向不同版本号的APP主项目提供最合适的插件APK;
  3. 万一最新的插件APK出现紧急BUG,要提供旧版本回滚功能;
  4. 出于安全考虑应该对APP项目的请求信息做一些安全性校验;

插件APK合法性校验

加载外部的可执行代码,一个逃不开的问题就是要确保外部代码的安全性,我们可不希望加载一些来历不明的插件APK,因为这些插件有的时候能访问主项目的关键数据。

最简单可靠的做法就是校验插件APK的MD5值,如果插件APK的MD5与我们服务器预置的数值不同,就认为插件被改动过,弃用。

是热部署,还是插件化?

动态加载方式,可以在“项目层级”做到代码分离,按道理我们希望是主项目和插件项目不要有任何交互行为,实际上也应该如此!这样做不仅能确保项目的安全性,也能简化开发工作,所以一般的做法是

只有在用户使用到的时候才加载插件

主项目还是像常规Android项目那样开发,只有用户使用插件APK的功能时才动态加载插件并运行,插件一旦运行后,与主项目没有任何交互逻辑,只有在主项目启动插件的时候才触发一次调用插件的行为。比如,我们的主项目里有几款推广的游戏,平时在用户使用主项目的功能时,可以先静默把游戏(其实就是一个插件APK)下载好,当用户点击游戏入口时,以动态加载的方式启动游戏,游戏只运行插件APK里的代码逻辑,结束后返回主项目界面。

一启动主项目就加载插件

另外一种完全相反的情形是,主项目只提供一个启动的入口,以及从服务器下载最新插件的更新逻辑,这两部分的代码都是长期保持不变的,应用一启动就动态加载插件,所有业务逻辑的代码都在插件里实现。比如现在一些游戏市场都要求开发者接入其SDK项目,如果SDK项目采用这种开发方式,先提供一个空壳的SDK给开发者,空壳SDK能从服务器下载最新的插件再运行插件里的逻辑,就能保证开发者开发的游戏每次启动的时候都能运行最新的代码逻辑,而不用让开发者在SDK有新版本的时候重新更换SDK并构建新的游戏APK。

让插件使用主项目的功能

有些时候,比如,主项目里有一个成熟的图片加载框架ImageLoader,而插件里也有一个ImageLoader。如果一个应用同时运行两套ImageLoader,那会有许多额外的性能开销,如果能让插件也用主项目的ImageLoader就好了。另外,如果在插件里需要用到用户登录功能,我们总不希望用户使用主项目时进行一次登录,进入插件时由来一次登录,如果能在插件里使用主项目的登录状态就好了。

因此,有些时候我们希望插件项目能调用主项目的功能。怎么处理好呢,由于插件项目与主项目是分开的,我们在开发插件的时候,怎么调用主项目的代码啊?这里需要稍微了解一下Android项目间的依赖方式。

想想一个普通的APK是怎么构建和运行的,Android SDK提供了许多系统类(如Activity、Fragment等,一般我们也喜欢在这里查看源码),我们的Android项目依赖Android SDK项目并使用这些类进行开发,那构建APK的时候会把这些类打包进来吗?不会,要是每个APK都打包一份,那得有多少冗余啊。所以Android项目至少有两种依赖的方式,一种构建时会把被依赖的项目(Library)的类打包进来,一种不会。

在Android Studio打开项目的Project Structure,找到具体Module的Dependencies选项卡

image.png

可以看到Library项目有个Scope属性,这里的Compile模式就是会把Library的类打包进来,而Provided模式就不会。

注意,使用Provided模式的Library只能是jar文件,而不能是一个Android Library项目,因为后者可能自带了一些res资源,这些资源无法一并塞进标准的jar文件里面。到这里我们明白,Android SDK的代码其实是打包进系统ROM(俗称Framework层级)里面的,我们开发Android项目的时候,只是以Provided模式引用android.jar,从这个角度也佐证了上面谈到的“为什么APP实际运行时AssetManager类的逻辑会与Android SDK里的源码不一样”。

现在好办了,如果要在插件里使用主项目的ImageLoader,我们可以把ImageLoader的相关代码抽离成一个Android Libary项目,主项目以Compile模式引用这个Libary,而插件项目以Provided模式引用这个Library(编译出来的jar),这样能实现两者之间的交互了,当然代价也是明显的。

我们应该只给插件开放一些必要的接口,不然会有安全性问题;
作为通用模块的Library应该保持不变(起码接口不变),不然主项目与插件项目的版本同步会复杂许多;
因为插件项目已经严重依赖主项目了,所以插件项目不能独立运行,因为缺少必要的环境;
最后我们再说说“热部署”和“插件化”的区别,一般我们把独立运行的插件APK叫热部署,而需要依赖主项目的环境运行的插件APK叫做插件化。

动态创建Activity模式
特点
  1. 主APK可以启动一个未安装的插件APK;
  2. 插件APK可以是任意第三方APK,无需接入指定的接口,理所当然也可以独立运行;

需要启动插件的某一个Activity(比如PlugActivity)的时候,动态创建一个TargetActivity,新创建的TargetActivity会继承PlugActivity的所有共有行为,而这个TargetActivity的包名与类名刚好与我们事先注册的TargetActivity一致,我们就能以标准的方式启动这个Activity。运行时动态创建并编译一个Activity类,动态创建类的工具有dexmaker和asmdex,二者均能实现动态字节码操作,最大的区别是前者是创建dex文件,而后者是创建class文件。

使用dexmaker动态创建一个类

运行时创建一个编译好并能运行的类叫做“动态字节码操作(runtime bytecode manipulation)”,使用dexmaker工具能创建一个dex文件。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void onMakeDex(View view){
        try {
            DexMaker dexMaker = new DexMaker();
            // Generate a HelloWorld class.
            TypeId<?> helloWorld = TypeId.get("LHelloWorld;");
            dexMaker.declare(helloWorld, "HelloWorld.generated", Modifier.PUBLIC, TypeId.OBJECT);
            generateHelloMethod(dexMaker, helloWorld);
            // Create the dex file and load it.
            File outputDir = new File(Environment.getExternalStorageDirectory() + File.separator + "dexmaker");
            if (!outputDir.exists())outputDir.mkdir();
            ClassLoader loader = dexMaker.generateAndLoad(this.getClassLoader(), outputDir);
            Class<?> helloWorldClass = loader.loadClass("HelloWorld");
            // Execute our newly-generated code in-process.
            helloWorldClass.getMethod("hello").invoke(null);
        } catch (Exception e) {
            Log.e("MainActivity","[onMakeDex]",e);
        }
    }

    /**
     * Generates Dalvik bytecode equivalent to the following method.
     *    public static void hello() {
     *        int a = 0xabcd;
     *        int b = 0xaaaa;
     *        int c = a - b;
     *        String s = Integer.toHexString(c);
     *        System.out.println(s);
     *        return;
     *    }
     */
    private static void generateHelloMethod(DexMaker dexMaker, TypeId<?> declaringType) {
        // Lookup some types we'll need along the way.
        TypeId<System> systemType = TypeId.get(System.class);
        TypeId<PrintStream> printStreamType = TypeId.get(PrintStream.class);

        // Identify the 'hello()' method on declaringType.
        MethodId hello = declaringType.getMethod(TypeId.VOID, "hello");

        // Declare that method on the dexMaker. Use the returned Code instance
        // as a builder that we can append instructions to.
        Code code = dexMaker.declare(hello, Modifier.STATIC | Modifier.PUBLIC);

        // Declare all the locals we'll need up front. The API requires this.
        Local<Integer> a = code.newLocal(TypeId.INT);
        Local<Integer> b = code.newLocal(TypeId.INT);
        Local<Integer> c = code.newLocal(TypeId.INT);
        Local<String> s = code.newLocal(TypeId.STRING);
        Local<PrintStream> localSystemOut = code.newLocal(printStreamType);

        // int a = 0xabcd;
        code.loadConstant(a, 0xabcd);

        // int b = 0xaaaa;
        code.loadConstant(b, 0xaaaa);

        // int c = a - b;
        code.op(BinaryOp.SUBTRACT, c, a, b);

        // String s = Integer.toHexString(c);
        MethodId<Integer, String> toHexString
                = TypeId.get(Integer.class).getMethod(TypeId.STRING, "toHexString", TypeId.INT);
        code.invokeStatic(toHexString, s, c);

        // System.out.println(s);
        FieldId<System, PrintStream> systemOutField = systemType.getField(printStreamType, "out");
        code.sget(systemOutField, localSystemOut);
        MethodId<PrintStream, Void> printlnMethod = printStreamType.getMethod(
                TypeId.VOID, "println", TypeId.STRING);
        code.invokeVirtual(printlnMethod, null, localSystemOut, s);

        // return;
        code.returnVoid();
    }
    
}

运行后在SD卡的dexmaker目录下找到刚创建的文件“Generated1532509318.jar”,把里面的“classes.dex”解压出来,然后再用“dex2jar”工具转化成jar文件,最后再用“jd-gui”工具反编译jar的源码。

修改需要启动的目标Activity

在Android,虚拟机加载类的时候,是通过ClassLoader的loadClass方法,而loadClass方法并不是final类型的,这意味着我们可以创建自己的类去继承ClassLoader,以重载loadClass方法并改写类的加载逻辑,在需要加载PlugActivity的时候,偷偷把其换成TargetActivity。

public class CJClassLoader extends ClassLoader{

@override
    public Class loadClass(String className){
      if(当前上下文插件不为空) {
        if( className 是 TargetActivity){
             找到当前实际要加载的原始PlugActivity,动态创建类(TargetActivity extends PlugActivity )的dex文件
             return  从dex文件中加载的TargetActivity
        }else{
             return  使用对应的PluginClassLoader加载普通类
        }  
     }else{
         return super.loadClass() //使用原来的类加载方法
     }   
    } 
}
image.png
存在的问题

动态类创建的方式,使得注册一个通用的Activity就能给多给Activity使用,对这种做法存在的问题也是明显的

  1. 使用同一个注册的Activity,所以一些需要在Manifest注册的属性无法做到每个Activity都自定义配置;
  2. 插件中的权限,无法动态注册,插件需要的权限都得在宿主中注册,无法动态添加权限;
  3. 插件的Activity无法开启独立进程,因为这需要在Manifest里面注册;
  4. 动态字节码操作涉及到Hack开发,所以相比代理模式起来不稳定;
    其中不稳定的问题出现在对Service的支持上,使用动态创建类的方式可以搞定Activity和Broadcast Receiver,但是使用类似的方式处理Service却不行,因为“ContextImpl.getApplicationContext” 期待得到一个非ContextWrapper的context,如果不是则继续下次循环,目前的Context实例都是wrapper,所以会进入死循环。
代理Activity模式与动态创建Activity模式的区别

简单地说,最大的不同是代理模式使用了一个代理的Activity,而动态创建Activity模式使用了一个通用的Activity。

代理模式中,使用一个代理Activity去完成本应该由插件Activity完成的工作,这个代理Activity是一个标准的Android Activity组件,具有生命周期和上下文环境(ContextWrapper和ContextCompl),但是它自身只是一个空壳,并没有承担什么业务逻辑;而插件Activity其实只是一个普通的Java对象,它没有上下文环境,但是却能正常执行业务逻辑的代码。代理Activity和不同的插件Activity配合起来,就能完成不同的业务逻辑了。所以代理模式其实还是使用常规的Android开发技术,只是在处理插件资源的时候强制调用了系统的隐藏API,因此这种模式还是可以稳定工作和升级的。

动态创建Activity模式,被动态创建出来的Activity类是有在主项目里面注册的,它是一个标准的Activity,它有自己的Context和生命周期,不需要代理的Activity。

作用与代价

作用
  1. 规避APK覆盖安装的升级过程,提高用户体验,顺便能 规避 一些安卓市场的限制;
  2. 动态修复应用的一些 紧急BUG,做好最后一道保障;
  3. 当应用体积太庞大的时候,可以把一些模块通过动态加载以插件的形式分割出去,这样可以减少主项目的体积,提高项目的编译速度,也能让主项目和插件项目并行开发;
  4. 插件模块可以用懒加载的方式在需要的时候才初始化,从而 提高应用的启动速度;
  5. 从项目管理上来看,分割插件模块的方式做到了 项目级别的代码分离,大大降低模块之间的耦合度,同一个项目能够分割出不同模块在多个开发团队之间 并行开发,如果出现BUG也容易定位问题;
  6. 在Android应用上 推广 其他应用的时候,可以使用动态加载技术让用户优先体验新应用的功能,而不用下载并安装全新的APK;
  7. 减少主项目DEX的方法数,65535问题 彻底成为历史(虽然现在在Android Studio中很容易开启MultiDex,这个问题也不难解决);
代价
  1. 开发方式可能变得比较诡异、繁琐,与常规开发方式不同;
  2. 随着动态加载框架复杂程度的加深,项目的构建过程也变得复杂,有可能要主项目和插件项目分别构建,再整合到一起;
  3. 由于插件项目是独立开发的,当主项目加载插件运行时,插件的运行环境已经完全不同,代码逻辑容易出现BUG,而且在主项目中调试插件十分繁琐;
  4. 非常规的开发方式,有些框架使用反射强行调用了部分Android系统Framework层的代码,部分Android ROM可能已经改动了这些代码,所以有存在兼容性问题的风险,特别是在一些古老Android设备和部分三星的手机上;
  5. 采用动态加载的插件在使用系统资源(特别是Theme)时经常有一些兼容性问题,特别是部分三星的手机;

开源项目

对比

  • DyLA : Dynamic-load-apk @singwhatiwanna, 百度
  • DiLA : Direct-Load-apk @melbcat
  • APF : Android-Plugin-Framework @limpoxe
  • ACDD : ACDD @bunnyblue
  • DyAPK : DynamicAPK @TediWang, 携程
  • DPG : DroidPlugin @cmzy, 360
\ DyLA DiLA ACDD DyAPK DPG APF Small
加载非独立插件[1] × × ×
加载.so插件 × × ![2] × × ×
Activity生命周期 ×
Service动态注册 × × × × ×[3]
资源分包共享[4] × × ![5] ![5] × ![6]
公共插件打包共享[7] × × × × × ×
支持AppCompat[8] × × × × × ×
支持本地网页组件 × × × × × ×
支持联调插件[9] × × × × × ×
  • [1] 独立插件:一个完整的apk包,可以独立运行。比如从你的程序跑起淘宝、QQ。
    非独立插件:依赖于宿主,宿主是个壳,插件可使用其资源代码并分离之以最小化。 -- “所有不能加载非独立插件的插件化框架都是耍流氓”。
  • [2] ACDD加载.so用了Native方法(libdexopt.so),不是Java层,源码似乎未共享。
  • [3] Service更新频度低,可预先注册在宿主的manifest中,现不支持。
  • [4] 要实现宿主、各个插件资源可互相访问,需要对他们的资源进行分段处理以避免冲突。
  • [5] 这些框架修改aapt源码、重编、覆盖SDK Manager下载的aapt,Small使用gradle-small-plugin,在后期修改二进制文件,实现了PP段分区。
  • [6] 使用public-padding对资源id的TT段进行分区,分开了宿主和插件。但是插件之间无法分段。
  • [7] 除了宿主提供一些公共资源与代码外,我们仍需封装一些业务层面的公共库,这些库被其他插件所依赖。
    公共插件打包的目的就是可以单独更新公共库插件,并且相关插件不需要动到。
  • [8] AppCompat: Android Studio默认添加的主题包,Google主推的Metrial Design包也依赖于此。大势所趋。
  • [9] 联调插件:使用Android Studio调试宿主时,可直接在插件代码中添加断点调试
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,566评论 25 707
  • 如果我们是孩子,该如何被对待? 读陈岚《我们为什么被霸凌》一书,两章,心情沉重,读不下去。有时间还是需要读一读。 ...
    拐子凤阅读 308评论 0 0
  • 昨晚凌晨,微信终于发布酝酿已久的小程序,首批百余家小程序集体亮相,一时间朋友圈讨论纷纷。 微信亦于今日发布小程序的...
    w_jeff阅读 15,825评论 45 245
  • 我年轻的时候我的老师已年老。 为磨炼自己我放弃炉火直到很冷。 我忍耐着像正在铸造的金属。 我去学校里长大并在那学习...
    东丰林波阅读 272评论 0 1