1
前言
插件化技术从2015年就开始百花齐放,如: 奇虎360的replugin、任玉刚的VirtualAPK和腾讯的Shadow,插件化经历了严峻的市场考验,目前已经很成熟,今天小木箱带大家手把手学习插件化Activity,如果本文对你有所帮助,希望点赞收藏加转发。
2
插件化概念
插件化是一种动态加载四大组件的技术。最早为了解决65535限制的问题而诞生,后来Google出来了multidex专门解决65535限制问题。
目前市面的插件化一定程度上可以达到缩包目的,而且对项目组件化,工程职责颗粒化,模块低耦合有着莫大裨益。
插件化同时能实现bug热修复,由于Davilk虚拟机存在,Java支持动态加载任意类。
因为安卓系统在四大组件上做了限制,如果你尝试打开不在清单中的组件时,Android系统会让程序Crash。
插件化本质上是绕过Android系统的管控,让我们的APP自由打开、使用四大组件。
3
插件化业务价值
插件化是为了解决类加载和资源加载的问题,资源加载通过反射AssertManager,按照类加载划分,插件化分为静态代理和Hook两种方式,使用插件化是为了解决应用新版本覆盖慢问题。
四大组件可动态加载,意味着用户不需要手动安装新版本的应用,我们可以给用户提供新的功能和页面,或者在用户无感的情况下修复bug。
4
插件化项目结构
5
插件化开发流程
第一步: 创建 app 主工程作为宿主工程
第二步: 创建plugin_package作为插件工程,负责打插件包
第三步: 创建接口工程 lifecycle_manager ,负责管理四大组件的生命周期
第四步: 安装插件
4.1 把Assets里面的文件复制到/data/data/files目录下
public static void extractAssets(Context context, String sourceName) {
AssetManager am = context.getAssets();
InputStream is = null;
FileOutputStream fos = null;
try {
is = am.open(sourceName);
File extractFile = context.getFileStreamPath(sourceName);
fos = new FileOutputStream(extractFile);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
closeSilently(is);
closeSilently(fos);
}
}
4.2 通过静态代理构建DexClassLoader
因为没有上下文环境,上下文环境需要宿主提供给一个DexClassLoader包含一个插件。
// 获取插件目录下的文件
File extractFile = mContext.getFileStreamPath(mApkName);
// 获取插件包路径
String dexPath = extractFile.getPath();
// 创建Dex输出路径
File fileRelease = mContext.getDir("dex", Context.MODE_PRIVATE);
// 构建 DexClassLoader 生成目录
mPluginClassLoader = new DexClassLoader(dexPath,
fileRelease.getAbsolutePath(), null, mContext.getClassLoader());
Hook方式是把dex文件合并到宿主的DexClassLoader里面,但绕过AMS清单文件注册的Activity会抛ClassNotFuoundException,所以需要Hook startActivity和handleResumeActivity,前者实现简单,兼容性好,而且插件是分离的,后者兼容性差,开发方便,但是如果多个插件如果有相同的类,就会出现问题,这里使用静态代理来处理。
4.3 通过反射 AssertManager 实现资源加载
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method method = AssetManager.class.getMethod("addAssetPath", String.class);
method.invoke(assetManager, dexPath);
mPluginResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
mContext.getResources().getConfiguration());
} catch (Exception e) {
Toast.makeText(mContext, "加载 Plugin 失败", Toast.LENGTH_SHORT).show();
}
第五步: 解析插件
静态代理实现方式很简单,不需要熟悉 Activity 启动流程什么的,直接面向接口编程,首先需要在宿主 App 加载插件构造 DExClassCloder 和 Resource 对象,有了 DexClassLoader,就可以加载插件里面的类 Resource 是通过反射 AssertManager 的 addAssertPath 创建一个 AssertManage,再构造 Resource 对象,当然启动 Service、注册动态广播其实和启动 Activity 一样,都是通过宿主的 Context 去启动,但是 DL 框架不支持静态广播。静态广播是在应用安装的时候才会去解析并注册的,而我们插件的 Manifest 是没法注册的,所以里面的静态广播只能我们手动去解析注册,利用的是反射调用 PackageParser 的 parsePackage 方法,把静态广播都转变为动态广播,具体实现是在 PluginManager#parserApkAction 方法的实现。
public void parserApkAction() {
try {
Class packageParserClass = Class.forName("android.content.pm.PackageParser");
Object packageParser = packageParserClass.newInstance();
Method method = packageParserClass.getMethod("parsePackage", File.class, int.class);
File extractFile = mContext.getFileStreamPath(mApkName);
Object packageObject = method.invoke(packageParser, extractFile, PackageManager.GET_RECEIVERS);
Field receiversFields = packageObject.getClass().getDeclaredField("receivers");
ArrayList arrayList = (ArrayList) receiversFields.get(packageObject);
Class packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Class userHandleClass = Class.forName("android.os.UserHandle");
int userId = (int) userHandleClass.getMethod("getCallingUserId").invoke(null);
for (Object activity : arrayList) {
Class component = Class.forName("android.content.pm.PackageParser$Component");
Field intents = component.getDeclaredField("intents");
// 1.获取 Intent-Filter
ArrayList<IntentFilter> intentFilterList = (ArrayList<IntentFilter>) intents.get(activity);
// 2.需要获取到广播的全类名,通过 ActivityInfo 获取
// ActivityInfo generateActivityInfo(Activity a, int flags, PackageUserState state, int userId)
Method generateActivityInfoMethod = packageParserClass
.getMethod("generateActivityInfo", activity.getClass(), int.class,
packageUserStateClass, int.class);
ActivityInfo activityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(null, activity, 0,
packageUserStateClass.newInstance(), userId);
Class broadcastReceiverClass = getClassLoader().loadClass(activityInfo.name);
BroadcastReceiver broadcastReceiver = (BroadcastReceiver) broadcastReceiverClass.newInstance();
for (IntentFilter intentFilter : intentFilterList) {
mContext.registerReceiver(broadcastReceiver, intentFilter);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
有了 AssertManager 对象就可以访问资源文件了,但是插件是没有 Context 上下文环境的,这个上下文环境需要宿主提供给他,具体做法是通过 PackManager 获取插件入口的 Activity 注注入宿主 Context,这就完成了宿主 App 跳转插件 App 的步骤。但是插件 App 是没有上下文环境的,所以插件 App 里面是不能直接 startActivity,需要拿到宿主 Context startActivity。
第六步,代理 Activity: 在 lifecycle_mananager 构建 ActivityInterface 负责管理插件 Activity 生命周期
public interface ActivityInterface {
// 插入Activity上下文
void insertAppContext(Activity hostActivity);
// Activity各个生命周期方法
void onCreate(Bundle savedInstanceState);
void onStart();
void onResume();
void onPause();
void onStop();
void onDestroy();
}
第七步,代理 Activity: 在 plugin_package 构建 BaseActivity 实现 ActivityInterface
在 BaseActivity 提供 startActivity,丢给宿主 Activity 去启动。
public void startActivity(Intent intent) {
Intent newIntent = new Intent();
newIntent.putExtra("ext_class_name", intent.getComponent().getClassName());
mHostActivity.startActivity(newIntent);
}
第八步,代理 Activity: 在 plugin_package 构建 Activity 插件
public class PluginActivity extends BaseActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_start).setOnClickListener(
v -> startActivity(new Intent(mHostActivity, TestActivity.class))
);
}
}
// 测试插件Activity
public class TestActivity extends BaseActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
}
}
第九步: 启动插件的入口 Activity
这一步主要做的就是给插件注册一个宿主的 Context。
// PorxyActivity
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 获取到真正要启动的插件 Activity,然后执行 onCreate 方法
String className = getIntent().getStringExtra(EXT_CLASS_NAME);
try {
Class clazz = getClassLoader().loadClass(className);
ActivityInterface activityInterface = (ActivityInterface) clazz.newInstance();
// 注册宿主的 Context
activityInterface.insertAppContext(this);
activityInterface.onCreate(savedInstanceState);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void startActivity(Intent intent) {
String className = intent.getStringExtra(EXT_CLASS_NAME);
Intent proxyIntent = new Intent(this, ProxyActivity.class);
proxyIntent.putExtra(EXT_CLASS_NAME, className);
super.startActivity(proxyIntent);
}
这样其实就已经完成了 PluginActivity 的启动了,但是需要注意的是,在插件的 Activity 里面,我们不能再使用 this 了,因为插件并没有上下文环境,所以一些调用 Context 的方法都需要使用宿主的 Context 去执行,比如:
在 BaseActivity 提供 findViewById,可以查找布局 Id 文件。
public View findViewById(int layoutId) {
return mHostActivity.findViewById(layoutId);
}
在 BaseActivity 提供 setContentView,方便渲染 UI 布局。
public void setContentView(int resId) {
mHostActivity.setContentView(resId);
}
6
插件化原理介绍
-
使用 DexClassLoader 加载插件的 Apk。
-
通过代理的 Activity 去执行插件中的 Activity,加载对应的生命周期。
-
通过反射调用 AssetManager 的 addAssetPath 来加载插件中的资源。
7
插件化遇到的问题
1. 找到的 Activity 不在插件包里面
我们真正打开的却是一个在插件包中定义的 Activity,这个 Activity 需要的信息在插件包中的,而不是宿主的。
解决方案
插件Activity也同时重写了attachBaseContext方法。在这一步, 用插件的classloader和Resources实例创建一个自己的上下文,并用它替换base context传递给父类保存。如此一来,业务调用 getClassLoader()或者getResources()时,取得的就都是插件的信息了。
2. 资源 Id 类型不匹配 找不到
你需要通过一个资源ID获取一个drawable的时候,取得的是color 或者其他资源。
解决方案
主要发生在8.0以下版本。经过调查发现在 8.0 以下的插件包中,ContextThemeWrapper.mResources是宿主的Resource,而非插件的 Resource。从而导致同一个ID找到的资源不对应。
3. 插件包 leakcanary 引发的崩溃
leakcanary 会使用栈顶的 activity 的 Resource 去加载它要显示的一张图片,但这个资源有可能不在当前插件中。
解决方案
宿主和所有插件都依赖 leakcanary 即可。
8
总结
本文主要是根据我自身实际投产的 插件组件化 实践,分享一些动态加载 SDK 插件 时需要考虑的问题。内容主要包括插件化方案的共同问题、插件包 leakcanary 引发的崩溃、资源 Id 类型不匹配 、宿主 Activity 找不到问题,千言万语汇成一句话:
插件有风险,使用须谨慎!
参考链接
沐小晨曦
https://github.com/Omooo/VirtualApplication
VirtualApk 插件化
https://github.com/MicroKibaco/mk_virtual_plugin