Android 插件化原理(一),通过dex文件调用插件app代码 您所在的位置:网站首页 classesdex里的代码 Android 插件化原理(一),通过dex文件调用插件app代码

Android 插件化原理(一),通过dex文件调用插件app代码

2024-07-10 12:39| 来源: 网络整理| 查看: 265

Android插件化原理,从以下三个问题切入:

什么是插件化如何实现插件类的加载如何实现插件资源的加载 什么是插件化

插件化技术最初是源于免安装运行APK的想法,这个免安装的APK就可以理解为插件,而支持插件的app,则称之为宿主;一方面减小了安装包的大小,另一方面可以实现 app 功能的动态扩展

插件化解决的问题 APP的功能越来越多,体积越来越大模块之间的耦合度高,协同开发的沟通成本越来越大APP功能变多之后,导致方法数可能会超过65535,APP占用内存过大应用之间的相互调用 插件化与组件化的区别 组件化开发就是将一个APP拆分成多个模块,每一个模块都是一个组件,在开发的过程中可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并为一个APK,这就是组件化开发。插件化和组件化略有不同,插件化是将整个APP拆分成多个模块,这些模块包括一个宿主和多个插件,每个模块都是一个单独的APK,最终打包的时候,宿主APK和插件APK分开打包。(最终online部署发布时,可以根据客户需求选择只发布宿主APK,或者是发布宿主APK和其中需要用到的插件APK) 各大插件化对比

特性

 

DynamicAPK

dynamic- load-apk

 

Small

 

DroidPlugin

 

RePlugin

 

VirtualAPK

作者

携程

任玉刚

wequick

360

360

滴滴

支持四大组件

只支持Activity

只支持Activity

只支持

Activity

全支持

全支持

全支持

组件无需在宿主manifest 中预注册

 

×

 

 

 

 

 

插件可以依赖宿主

×

支持PendingIntent

×

×

×

Android特性支持

大部分

大部分

大部分

几乎全部

几乎全部

几乎全部

兼容性适配

一般

一般

中等

插件构建

部署aapt

Gradle插件

Gradle插件

Gradle插件

 

在选择开源框架的时候,需要根据自身的需求来,如果加载的插件不需要和宿主有任何耦合,也无须和宿主进 行通信,比如加载第三方 App,那么推荐使用 RePlugin,其他的情况推荐使用 VirtualApk

插件化的实现

如何去实现插件化呢?插件是免安装的,那么要思考几个问题:

插件也是APK,APK里面有资源和类,那么需要考虑的问题无非就是两个 如何加载插件资源如何加载插件类要加载Android类,as we all konw,四大组件是需要在AndroidManifest.xml中注册的,要在宿主中调用插件的四大组件,显然插件APK中的用到的四大组件相关类,是没有在宿主的AndroidMainfest.xml中注册过的,那么怎么通过宿主去调用插件的四大组件 类加载

Java中会通过javac命令将.java文件编译生成.class文件,jvm会加载class文件,解析之后初始化,然后就可以运行了;Android中会将代码编译成一个APK,解压APK可以看到一个或者多个.dex文件,dex文件是DVM的可执行文件,dex就是把class合并优化后生成的(odex);下面我们来学习DVM如何加载dex文件。

ClassLoader

Android中是通过ClassLoader加载dex文件的,ClassLoader是一个抽象类,实现主要分为两种类型:系统类加载器和自定义加载器,其实按照我的理解,系统类加载器主要就是BootClassLoader,DexClassLoader和PathClassLoader

BootClassLoader,用于加载Android Framework层class文件。PathClassLoader,用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dexDexClassLoader,用于加载指定的dex,以及jar、zip、apk中的classes.dex

类继承关系如下图:

我们先来看下 PathClassLoader 和 DexClassLoader。

注意:有些帖子可能会有如下说法:

DexClassLoader:能够加载未安装的jar、apk、dex等;而PathClassLoader只能够加载系统中已经安装的apk;这个说法是有问题的,或者说针对Android8.0之后的系统,这个说法是错误的

在8.0(API 26)之前,它们二者的唯一区别是第二个参数 optimizedDirectory,这个参数的意思是生成的 odex(优化的dex)存放的路径。在8.0(API 26)及之后,二者就完全一样了。

看Android源码

// API26之后和API26之前的PathClassLoader的源码是相同的 // /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java public class PathClassLoader extends BaseDexClassLoader { // optimizedDirectory 直接为 null public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); } // optimizedDirectory 直接为 null public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) { super(dexPath, null, librarySearchPath, parent); } } // API 小于等于26 /libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java public class DexClassLoader extends BaseDexClassLoader { //从API26开始,super的构造方法,也就是BaseDexClassLoader的构造方法有变动 public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), librarySearchPath, parent); } } // API 大于26 /libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java public class DexClassLoader extends BaseDexClassLoader { /** * @param optimizedDirectory this parameter is deprecated and has no effect since API level 26. */ public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { //可以看到第二个参数optimizedDirectory直接为null,已经和PathClassLoader的构造方法一样了 super(dexPath, null, librarySearchPath, parent); } } // API 26/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java public BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent) { super(parent); // DexPathList 的第四个参数是 optimizedDirectory,可以看到这儿为 null this.pathList = new DexPathList(this, dexPath, librarySearchPath, null); } // API 25/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory); }

上述源码可以看出,PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader的,而且他们两个都只有构造函数,其类加载相关逻辑全都是在父类BaseDexClassLoader中。

唯一的区别就是第二个参数optimizedDirectory有没有使用,从API26,也就是8.0开始,他们两个的构造函数完全一样,已经没有任何区别,故DexClassLoader能干的事,PathClassLoader也能干

在搞清楚了PathClassLoader、DexClassLoader和BaseDexClassLoader之间的关系之后,我们写一段代码,研究一下PathClassLoader和BootClassLoader之间的关系。

在AS中创建一个Android项目,创建完成之后,再New 一个Module,项目的Project结构视图如下:

在宿主的MainActivity的onCreate方法中加入下代码:

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Example of a call to a native method TextView tv = findViewById(R.id.sample_text); tv.setText("xxxxxxx"); printClassloader(); //invokePluginMethod(); //invokePluginByLoadAPK(); } private void printlassCloader() { ClassLoader classLoader = getClassLoader(); while (classLoader != null) { Log.d(TAG, "printlassCloader classloader = " + classLoader); classLoader = classLoader.getParent(); } }

输出结果如下:

01-13 11:44:15.513 13212-13212/com.enjoy.myplugin D/Rayman: printClassloader classloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-1/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-1/lib/arm64, /vendor/lib64, /system/lib64]]] 01-13 11:44:15.513 13212-13212/com.enjoy.myplugin D/Rayman: printClassloader classloader = java.lang.BootClassLoader@15bc8d9c

从打印结果来看,当前应用程序的类加载器,或者说MainActivity的ClassLoader就是PathClassLoader,而PathClassLoader的parent属性的值是BootClassLoader,需要说明的是,这儿的parent并不是父类,parent是ClassLoader类本身的一个成员属性。

那么在修改一下printClassLoader方法,看看Activity和AppCompactActivity的类加载器具体都是什么:

private void printClassloader() { ClassLoader classLoader = getClassLoader(); while (classLoader != null) { Log.d(TAG, "printClassloader classloader = " + classLoader); classLoader = classLoader.getParent(); } Log.d(TAG,"activity clsloader = "+ Activity.class.getClassLoader()); Log.d(TAG,"appcompactactivity clsloader = "+ AppCompatActivity.class.getClassLoader()); }

打印结果如下:

01-13 11:55:37.838 13435-13435/? D/Rayman: printClassloader classloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-2/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-2/lib/arm64, /vendor/lib64, /system/lib64]]] 01-13 11:55:37.838 13435-13435/? D/Rayman: printClassloader classloader = java.lang.BootClassLoader@15bc8d9c 01-13 11:55:37.838 13435-13435/? D/Rayman: activity clsloader = java.lang.BootClassLoader@15bc8d9c 01-13 11:55:37.838 13435-13435/? D/Rayman: appcompactactivity clsloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-2/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-2/lib/arm64, /vendor/lib64, /system/lib64]]]

从打印结果可知:

正好印证了,前面谈到的BootClassLoader和PathClassLoader各自的作用范围

BootClassLoader,用于加载Android Framework层class文件。PathClassLoader,用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex

说到这儿,可能会有些疑问:

为什么Activity和AppCompactActivity的类加载器不同,需要说明的是BootClassLoader用于加载Android Framework中的类,或者说是BootClassLoader加载的类一定是在Android SDK中的,而AppCompactActivity并不是在SDK中,而是我们在创建项目的时候添加的依赖库,在build.gradle中添加的dependencies,

implementation 'androidx.appcompat:appcompat:1.1.0' 如何实现类加载

如何利用ClassLoader去加载一个类呢?其实非常简单,在之前创建的项目中有个Module是plugin,在plugin中添加一个java类,叫做PluginTest,如下图:

PluginTest的代码如下:

package com.enjoy.plugin; import android.util.Log; public class PluginTest { private final static String TAG = "Rayman"; private void ShowPluginMsg(String msg){ Log.d(TAG,"PluginTest ShowPluginMsg msg = "+msg); } }

现在我们有一个apk文件,路径是 apkPath,然后里面有个类 com.enjoy.plugin.PluginTest,可以参照如下代码,实现对PluginTest类的加载,在宿主APP的MainActivity中实现如下方法:

private void invokePluginMethod() { //在这里我们需要先生成PluginTest.dex文件 DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/PluginTest.dex", getCacheDir().getAbsolutePath(), null, getClassLoader()); //注意此处如果替换为PathClassLoader也是可以正常执行的 PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/PluginTest.dex", null,getClassLoader()); try { //通过DexClassLoader加载PluginTest类 Class clz = dexClassLoader.loadClass("com.enjoy.plugin.PluginTest"); //用DexClassLoader或者PathClassLoader都是可以的 //Class clz = pathClassLoader.loadClass("com.enjoy.plugin.PluginTest"); //通过反射调用PluginTest类中的方法 Object obj = clz.newInstance(); Method method = clz.getDeclaredMethod("ShowPluginMsg",String.class); method.setAccessible(true); method.invoke(obj,"This is invoke Plugin method..."); } catch (Exception e) { e.printStackTrace(); } }

生成dex文件的步骤:

首先在AS中编译plugin这个Module,生成相应的.class文件,即生成PuginTest.class文件,.class的路径如下:在Android SDK路径下找到dx.bat(Windows系统)这个文件,也就是找到自己的SDK路径,dx.bat在Android\Sdk\build-tools\28.0.3(这儿是Android SDK版本,也有可能是27.0.3或者25.0.2等),可参考如下截图:

将这个路径配置到环境变量,打开cmd,然后用如下命令生成dex文件:

dx --dex --output=output.dex input.class

使用这个命令需要注意,一定要在包名的外层目录执行,比如要生成前面提到的PluginTest.dex,需要按照下面方式执行,在cmd中先进入PluginTest.class文件的包名外层目录,如下:

进入到这儿之后,执行dx --dex --output=PluginTest.dex com\enjoy\plugin\PluginTest.class

执行完上述命令,就会在MyPlugin\plugin\build\intermediates\javac\debug\classes这个目录下生成PluginTest.dex文件

注意:

执行dx命令,只能在包名的外层目录执行,比如说在cmd里面直接进入到.class的目录MyPlugin\plugin\build\intermediates\javac\debug\classes\com\enjoy\plugin,那么在执行dx命令是会报错,无法生成相应的dex文件。

OK,到这儿dex文件就生成了,也就是生成了ClassLoader可以加载的dex文件,亦即DVM可以执行的dex文件。

接下来只需将PluginTest.dex拷贝或者push到手机的/sdcard中,或者是Android虚拟机的/sdcard目录下(/sdcard/PuginTest.dex),然后执行上面已经写好的invokePluginMethod方法,这里我直接在MainActivity的OnCreate方法中调用:

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Example of a call to a native method TextView tv = findViewById(R.id.sample_text); tv.setText(stringFromJNI()); printClassloader(); invokePluginMethod(); }

运行结果如下:

01-13 14:43:48.050 13645-13645/? D/Rayman: printClassloader classloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-1/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-1/lib/arm64, /vendor/lib64, /system/lib64]]] 01-13 14:43:48.050 13645-13645/? D/Rayman: printClassloader classloader = java.lang.BootClassLoader@15bc8d9c 01-13 14:43:48.050 13645-13645/? D/Rayman: activity clsloader = java.lang.BootClassLoader@15bc8d9c 01-13 14:43:48.050 13645-13645/? D/Rayman: appcompactactivity clsloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-1/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-1/lib/arm64, /vendor/lib64, /system/lib64]]] 01-13 14:43:48.054 13645-13645/? D/Rayman: PluginTest ShowPluginMsg msg = This is invoke Plugin method...

LOG中的最后一句,就是我们在PluginTest.java中打印出来的,到这儿说明在宿主APP中成功的调用了插件APP中的代码。

需要知道插件代码的包名,类名,方法名,以及参数,并不需要安装插件APP,就可以通过ClassLoader加载dex文件,再利用反射调用插件代码,只需要dex文件就实现了免安装调用app代码的功能。

本章节讲述的是通过dex文件加载调用插件APP的代码,到这儿就暂时结束了,下一章节会讲述ClassLoader类加载机制,双亲委托机制,不用生成dex文件,只需要插件APK就可以通过ClassLoader和反射调用插件APP的代码。

下一章节链接:https://blog.csdn.net/qq_31429205/article/details/103959814



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有