1.插件化
插件可以理解为免安装的Apk,而支持插件的app称为宿主。
一个Apk会包含如下几个部分:
classes.dex:Java代码字节码
res:资源文件
lib:so 文件
assets:静态资产文件
AndroidManifest.xml:清单文件
Android系统在打开应用之后,只是开辟进程,然后使用ClassLoader加载classes.dex至进程中,执行对应的组件而已。
插件化让Apk中的代码(主要是指Android组件)能够免安装运行,这样带来很多好处:
①减少安装Apk的体积、按需下载模块
②动态更新插件
③宿主和插件分开编译,提升开发效率
④解决方法数超过65535的问题
插件化和热修复不是同一个概念,虽然在技术实现的角度来说,它们都是从系统加载器的角度出发,通过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件/补丁中的内容,但是它们的出发点是不同的。插件化是把需要实现的模块或功能独立出来,减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块。热修复则是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。
插件化与组件化的区别:
组件化是将一个App分成多个模块,每个模块都是一个组件(module),开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。组件化开发有一个弊端 ,就是多个模块必须是并发开发 , 模块之间相互依赖 , 如果修改了一个模块 ,那就必须重新打包 。
插件化是将整个app拆分成若干模块,其中有1个宿主模块、若干插件模块 。宿主模块和插件模块可以分开进行编译,二者之间互不影响,各个模块可以并发进行开发。打包时,将宿主模块和插件模块分开进行打包,即宿主模块和插件模块都各自是一个单独apk安装文件 。只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk,实现宿主模块动态更新插件。
现在大型Android项目基本都是组件化+插件化开发。项目架构上都是组件化的框架,某些修改频繁的Module模块设置成插件模块,编译成独立的APK文件,以插件的形式进行部署,供宿主模块调用。应用运行时,点击启动某个插件APK中的界面,首先下载对应的插件APK文件,将其放在内置存储区中,然后加载该APK文件,主要是类加载器加载dex文件中的Class字节码数据。
插件化的思路是:在宿主模块应用中提前预留好插件组件对应的代理组件接口,将插件模块apk插入到程序中,就可以调用插件apk安装包中的功能。将插件模块编译打包成apk文件,该文件包含classes.dex文件,被打包在了apk压缩包中。因此只要拿到了apk文件,就可以将其解压,获取到其中的classes.dex文件。可以自己实现一个DexClassLoader加载该dex文件,进而调用其中封装的字节码类对象。
2.插件化实现步骤
①加载并执行插件Apk中的代码
要让插件Apk真正运行起来,首先要找到插件Apk的存放位置,然后解析加载Apk里面的代码。
由于插件是未安装的apk,系统不会处理其中的类,所以需要使用ClassLoader加载Apk,然后反射里面的代码。
在Android系统中,ClassLoader用来加载dex文件。dex文件是一种对class文件优化的产物,在Android中应用打包时会把所有class文件进行合并、优化(把不同的class文件重复的东西只保留一份),然后生成一个最终的class.dex文件。
PathClassLoader:用来加载系统类和应用程序类,可以加载已经安装的apk目录下的dex文件。
DexClassLoader:用来加载dex文件,可以从存储空间加载dex文件。
在插件化中一般使用的是DexClassLoader。创建出DexClassLaoder实例以后,调用其loadClass( className) 方法就可以加载插件中的类了。然后通过反射执行类的方法:
Class loadClass = pluginClassLoader.loadClass( activityName);
loadClass.getMethod("test",null).invoke(loadClass);
这个过程叫做ClassLoader注入。完成注入后,所有来自宿主的类使用宿主的ClassLoader进行加载,所有来自插件Apk的类使用插件ClassLoader 进行加载。由于ClassLoader的双亲委派机制,实际上系统类会不受ClassLoader的类隔离机制影响,这样宿主Apk就可以在宿主进程中使用来自于插件的组件类了。
②让系统能调用插件Apk中的组件
Android系统中四大组件是需要在系统中注册的,并且由系统管理生命周期,具体是在Android系统的AMS和PMS中注册,而四大组件的解析和启动都需要依赖AMS和PMS。而插件是动态加载的,所以插件的四大组件不可能注册到宿主的Manifest文件中,因此不能和系统直接进行交互的。 而且仅仅构造出四大组件的实例是没用的,还需要管理组件的生命周期。
插件化如何支持组件生命周期的管理,大致分为两种方式:运行时容器技术(ProxyActivity代理)和预埋StubActivity,hook系统启动Activity的过程。
这里使用运行时容器技术,就是在宿主Apk中预埋一些空的Android组件。以Activity为例,可以在宿主中预置一个ContainerActivity extends Activity ,并且在AndroidManifest.xml中注册它。
ContainerActivity作为插件Activity的容器,它从Intent接收插件的不同信息,如pluginName、pluginApkPath、pluginActivityName等。当ContainerActivity启动时,就加载插件的 ClassLoader、Resource,并反射pluginActivityName对应的Activity类。当完成加载后,ContainerActivity要做两件事:
①转发所有来自系统的生命周期回调至插件Activity
②接受Activity方法的系统调用,并转发回系统
第一步可以通过复写ContainerActivity的生命周期方法来完成,而第二步需要定义一个 PluginActivity,然后在编写插件Apk中的Activity 组件时,需要继承自PluginActivity。
public class ContainerActivity extends Activity {
private PluginActivity pluginActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
String pluginActivityName = getIntent().getString("pluginActivityName", "");
pluginActivity = PluginLoader.loadActivity( pluginActivityName, this);
if (pluginActivity == null) {
super.onCreate(savedInstanceState);
return;
}
pluginActivity.onCreate();
}
@Override
protected void onResume() {
if (pluginActivity == null) {
super.onResume();
return;
}
pluginActivity.onResume();
}
@Override
protected void onPause() {
if (pluginActivity == null) {
super.onPause();
return;
}
pluginActivity.onPause();
}
// ...
}
PluginActivity如下:
public class PluginActivity {
private ContainerActivity containerActivity;
public PluginActivity(ContainerActivity containerActivity) {
this.containerActivity = containerActivity;
}
@Override
public
return containerActivity.findViewById(id);
}
// ...
}
// 插件Apk中真正写的组件
public class TestActivity extends PluginActivity {
// ......
}
大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。
该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,插件中的Activity必须继承PluginActivity,如果想把之前的模块改造成插件需要很多额外的工作。
class TestActivity extends Activity {}
改为
class TestActivity extends PluginActivity {}
为了让插件组件的编写与原来没有任何差别,可以使用字节码插桩。Android中Transform Api可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换。也就是用户配置Gradle插件后,正常开发,依然编写:
class TestActivity extends Activity {}
然后完成编译后,最后的字节码中,显示的却是:
class TestActivity extends PluginActivity {}
③正确识别插件Apk中的资源
最后一步就是资源注入,Android应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layout、values 等)都会被打包到Apk中,然后生成一个对应的R类,其中包含对所有资源的引用 id。
资源的注入主要依靠两个接口:
①PackageManager#getPackageArchiveInfo:根据Apk路径解析一个未安装的Apk的PackageInfo
②PackageManager#getResourcesForApplication:根据ApplicationInfo创建一个Resources实例
因此可以在上面ContainerActivity#onCreate中加载插件Apk的时候,用这两个方法创建出一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo拿到插件Apk的PackageInfo,有了PacakgeInfo之后就可以组装一份ApplicationInfo,然后通过 PackageManager#getResourcesForApplication来创建资源实例,大概代码像这样:
PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
pluginApkPath,
PackageManager.GET_ACTIVITIES
| PackageManager.GET_META_DATA
| PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS
| PackageManager.GET_SIGNATURES
);
packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;
Resources injectResources = null;
try {
injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
} catch (PackageManager.NameNotFoundExcep tion e) {
// ...
}
拿到资源实例后,需要将宿主的资源和插件资源Merge一下,编写一个新的Resources类,用这样的方式完成自动代理:
public class PluginResources extends Resources {
private Resources hostResources;
private Resources injectResources;
public PluginResources(Resources hostResources, Resources injectResources) {
super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
this.hostResources = hostResources;
this.injectResources = injectResources;
}
@Override
public String getString(int id, Object... formatArgs) throws NotFoundException {
try {
return injectResources.getString(id, formatArgs);
} catch (NotFoundException e) {
return hostResources.getString(id, formatArgs);
}
}
// ...
}
然后在ContainerActivity完成插件组件加载后,创建一份Merge资源,再复写ContainerActivity# getResources,将获取到的资源替换掉:
public class ContainerActivity extends Activity {
private Resources pluginResources;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
// ...
}
@Override
public Resources getResources() {
if (pluginActivity == null) {
return super.getResources();
}
return pluginResources;
}
}
这样就完成了资源的注入。