作者
大家好,我叫小鑫,也可以叫我蜡笔小鑫😊;
本人17年毕业于中山大学,于2018年7月加入37手游安卓团队,曾经就职于久邦数码担任安卓开发工程师;
目前是37手游安卓团队的海外负责人,负责相关业务开发;同时兼顾一些基础建设相关工作。
作者:37手游移动客户端团队
链接:https://juejin.cn/post/7244947219160711224
一、什么是插件化
一个运行的App作为宿主,去加载一个未安装的apk文件,并且运行起来,这就叫做插件化
插件化的使用场景:
1、线上新增功能(如淘宝、支付宝等)
2、热修复(通过下发补丁插件,完成对功能的修复)
3、当编译太慢时,可以使用插件化,对某些不改动的代码做成插件,加快编译速度
二、插件化的三种常见实现方式
1、占位式实现插件化
1、特点
插件遵从宿主的定义的标准,使用宿主的上下文环境。
2、优点:只使用了少量反射,无hook,实现简单
3、缺点:在插件中只能使用宿主提供的上下文环境,如插件Activity中,不能使用this当上下文,也就是说有一定侵入性,需要修改插件Activity的实现。
2、实现步骤
1、定义宿主的标准,下面以Activity为例
public interface ActivityInterface {
/**
* 把宿主(app)的环境 给 插件
* @param appActivity
*/
void insertAppContext(Activity appActivity);
// 生命周期方法
void onCreate(Bundle savedInstanceState);
void onStart();
void onResume();
void onDestroy();
//此处省略了其他声明周期,只做演示使用
}
2、在插件模块中,根据标准实现插件Activity
//根据标准实现的插件模块中的BaseActivity
public class BaseActivity implements ActivityInterface {
//宿主传递过来的上下文
public Activity appActivity; // 宿主的环境
@Override
public void insertAppContext(Activity appActivity) {
this.appActivity = appActivity;
}
@SuppressLint("MissingSuperCall")
@Override
public void onCreate(Bundle savedInstanceState) {
}
@SuppressLint("MissingSuperCall")
@Override
public void onStart() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onResume() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onDestroy() {
}
//实际上走的是宿主的setContentView方法
public void setContentView(int resId) {
appActivity.setContentView(resId);
}
public View findViewById(int layoutId) {
return appActivity.findViewById(layoutId);
}
@Override
public void startActivity(Intent intent) {
Intent intentNew = new Intent();
intentNew.putExtra("className", intent.getComponent().getClassName()); // TestActivity 全类名
appActivity.startActivity(intentNew);
}
}
//BaseActivity是关键
public class PluginActivity extends BaseActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.plugin_main);
// this 会报错,因为插件没有安装,也没有组件的环境,所以必须使用宿主环境
Toast.makeText(appActivity, "我是插件", Toast.LENGTH_SHORT).show();
findViewById(R.id.bt_start_activity).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//这个startActivity走的是BaseActivity的startActivity,也是被代理过的
startActivity(new Intent(appActivity, TestActivity.class));
}
});
}
}
3、将插件模块打包apk,在宿主中加载该apk
对插件apk的加载主要分为两个步骤,即加载类和加载资源。加载类使用的是自定义的DexClassLoader,加载资源使用的是反射调用AssetManager的addAssetPath方法。
具体的代码如下:
public class PluginManager {
private static final String TAG = PluginManager.class.getSimpleName();
private static PluginManager pluginManager;
private Context context;
public static PluginManager getInstance(Context context) {
if (pluginManager == null) {
synchronized (PluginManager.class) {
if (pluginManager == null) {
pluginManager = new PluginManager(context);
}
}
}
return pluginManager;
}
public PluginManager(Context context) {
this.context = context;
}
private DexClassLoader dexClassLoader;
private Resources resources;
/**
* 1、加载类
* 2、加载资源
*/
public void loadPlugin() {
try {
File file = AssetUtils.copyAssetPlugin(context, "p.apk", "plugin");
if (!file.exists()) {
Log.d(TAG, "插件包 不存在...");
return;
}
String pluginPath = file.getAbsolutePath();
File fileDir = context.getDir("pDir", Context.MODE_PRIVATE);
dexClassLoader = new DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, context.getClassLoader());
// 加载资源
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, pluginPath); // 插件包的路径 pluginPath
Resources r = context.getResources(); // 宿主的资源配置信息
// 特殊的 Resources,加载插件里面的资源的 Resources
resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
public ClassLoader getClassLoader() {
return dexClassLoader;
}
public Resources getResources() {
return resources;
}
}
4、在宿主中定义占位的Activity
这里最重要的步骤为:
1、重写getResources和getClassLoader方法,使用插件的ClassLoader和插件的Resources
2、实例化出来插件Activity
3、给插件Activity注入上下文
4、调用插件Activity的onCreate方法
代码如下:
public class ProxyActivity extends Activity {
//这里使用的是插件中的资源
@Override
public Resources getResources() {
return PluginManager.getInstance(this).getResources();
}
//这里使用的是插件中的类加载器
@Override
public ClassLoader getClassLoader() {
return PluginManager.getInstance(this).getClassLoader();
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 真正的加载 插件里面的 Activity
String className = getIntent().getStringExtra("className");
try {
Class mPluginActivityClass = getClassLoader().loadClass(className);
// 实例化 插件包里面的 Activity
Constructor constructor = mPluginActivityClass.getConstructor(new Class[]{});
Object mPluginActivity = constructor.newInstance(new Object[]{});
ActivityInterface activityInterface = (ActivityInterface) mPluginActivity;
// 给插件注入上下文
activityInterface.insertAppContext(this);
Bundle bundle = new Bundle();
bundle.putString("appName", "我是宿主传递过来的信息");
// 执行插件里面的onCreate方法
activityInterface.onCreate(bundle);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void startActivity(Intent intent) {
String className = intent.getStringExtra("className");
Intent proxyIntent = new Intent(this, ProxyActivity.class);
proxyIntent.putExtra("className", className); // 包名+TestActivity
// 要给TestActivity 进栈
super.startActivity(proxyIntent);
}
}
3、小结
至此,可以实现简单的占位式插件化了,我们来总结下步骤:
1、定义宿主和插件之间的标准,如Activity的标准为IActivityInterface
2、根据标准实现插件模块,打成apk文件(这里最重要的是插件中使用的上下文是宿主中传递过来的)
3、宿主中加载插件模块apk
4、定义占位Activity,在OnCreate方法中,根据Intent携带的插件Activity信息,反射插件Activity实例,为插件Activity注入占位Activity的上下文,调用插件Activity实例的onCreate方法启动
这种实现方式的优点是:全程仅有少量的反射,并无hook系统操作,适配工作简单。
缺点也很明显,在插件Activity中,需要遵守宿主规则,如果要做成框架,侵入性这个问题难以解决
2、hook实现插件化
学完了占位式插件化后,下面我们来介绍一种在插件中可以使用this的方式,采用hook系统api的方式实现插件化
1、特点
插件中的Activity可以使用this,与常规写法无异,无需像占位式那样遵从标准
hook操作较多,主要有两个环节需要hook,一个是hook欺骗AMS,启动没有再AndroidManifest中注册的Activity。另一个是hook实现将插件的dex和宿主的dex合并,替换掉原先的dexElements
2、原理
1、startActivity的过程
从图中看出,App告诉AMS启动Activity时,是携带了Intent的,我们日常看到的have you declared this activity in your AndroidManifest.xml
这个错误,就是在调用了startActivity后,AMS对要启动的Activity进行检查时触发的
也就是说,如果我们要欺骗AMS,在startActivity时,携带的Intent中的Activity就必须是一个在AndroidManifest中注册的Activity,而不能是我们插件中的Activity。
这里怎么办?偷龙转凤,偷梁换柱,狸猫换太子~
解决方法是,将Intent中的Component暂时替换成一个占位Activity(在AndroidManifest中声明过的),并且将真正要启动的插件Activity以参数的形式存放到Intent中。
在AMS向App发送LAUNCH_ACTIVITY事件时,把真正要启动的Activity启动
基本原理就是这样了,那么要解决的重点问题如下:
1、在调用Activity时,替换掉启动插件Activity的Intent为启动占位Activity的Intent,并将启动插件Activity的Intent以参数存放到启动占位Activity的Intent中
2、在AMS发送LAUNCH_ACTIVITY事件时,拦截,将Intent换回启动插件Activity的Intent
3、启动Activity时,是用的默认的ClassLoader加载Activity类,反射实例化的,所以需要把插件Activity加入到默认的ClassLoader中
下面我们挨个问题来解决
3、实现步骤
1、hook AMS,偷龙转凤
private void hookAmsAction() throws Exception {
Class mIActivityManagerClass = Class.forName("android.app.IActivityManager");
// 我们要拿到IActivityManager对象,才能让动态代理里面的 invoke 正常执行下
// 执行此方法 static public IActivityManager getDefault(),就能拿到 IActivityManager
Class mActivityManagerNativeClass2 = Class.forName("android.app.ActivityManagerNative");
final Object mIActivityManager = mActivityManagerNativeClass2.getMethod("getDefault").invoke(null);
// 动态代理IActivityManager
Object mIActivityManagerProxy = Proxy.newProxyInstance(
HookApplication.class.getClassLoader(),
new Class[]{mIActivityManagerClass}, // 要监听的接口
new InvocationHandler() { // IActivityManager 接口的回调方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
// 用ProxyActivity 绕过了 AMS检查
Intent intent = new Intent(HookApplication.this, ProxyActivity.class);
// 把要启动插件Activity的Intent当做参数存进去
intent.putExtra("actionIntent", ((Intent) args[2]));
args[2] = intent;
}
Log.d("hook", "拦截到了IActivityManager里面的方法" + method.getName());
// 让系统继续正常往下执行
return method.invoke(mIActivityManager, args);
}
});
/**
* 为了拿到 gDefault
* 通过 ActivityManagerNative 拿到 gDefault变量(对象)
*/
Class mActivityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
Field gDefaultField = mActivityManagerNativeClass.getDeclaredField("gDefault");
gDefaultField.setAccessible(true); // 授权
Object gDefault = gDefaultField.get(null);
// 替换点
Class mSingletonClass = Class.forName("android.util.Singleton");
// 获取此字段 mInstance
Field mInstanceField = mSingletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
// 替换
mInstanceField.set(gDefault, mIActivityManagerProxy);
}
2、hook LAUNCH_ACTIVITY事件,将要启动的Activity换回来
/**
* Hook LuanchActivity,即将要实例化Activity,要把ProxyActivity 给 换回来 ---》 TestActivity
*/
private void hookLuanchActivity() throws Exception {
Field mCallbackFiled = Handler.class.getDeclaredField("mCallback");
mCallbackFiled.setAccessible(true); // 授权
/**
* handler对象怎么来
* 1.寻找H,先寻找ActivityThread
*
* 执行此方法 public static ActivityThread currentActivityThread()
*
* 通过ActivityThread 找到 H
*
*/
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
// 获得ActivityThrea对象
Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);
Field mHField = mActivityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
// 获取真正对象
Handler mH = (Handler) mHField.get(mActivityThread);
mCallbackFiled.set(mH, new MyCallback(mH)); // 替换 增加我们自己的实现代码
}
public static final int LAUNCH_ACTIVITY = 100;
class MyCallback implements Handler.Callback {
private Handler mH;
public MyCallback(Handler mH) {
this.mH = mH;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
// 做我们在自己的业务逻辑(把ProxyActivity 换成 TestActivity)
Object obj = msg.obj;
try {
// 我们要获取之前Hook携带过来的 TestActivity
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
// 获取 intent 对象,才能取出携带过来的 actionIntent
Intent intent = (Intent) intentField.get(obj);
// actionIntent == 插件Activity的Intent
Intent actionIntent = intent.getParcelableExtra("actionIntent");
if (actionIntent != null) {
// 把ProxyActivity换成真正的插件Activity
intentField.set(obj, actionIntent);
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
//事件正常往下执行
mH.handleMessage(msg);
return true; // 系统不会往下执行
}
}
3、将插件dex和宿主dex合并
private void pluginToAppAction() throws Exception {
// 第一步:找到宿主 dexElements 得到此对象 PathClassLoader代表是宿主
PathClassLoader pathClassLoader = (PathClassLoader) this.getClassLoader(); // 本质就是PathClassLoader
Class mBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
// private final DexPathList pathList;
Field pathListField = mBaseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object mDexPathList = pathListField.get(pathClassLoader);
Field dexElementsField = mDexPathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
// 本质就是 Element[] dexElements
Object dexElements = dexElementsField.get(mDexPathList);
/*** ---------------------- ***/
// 第二步:找到插件 dexElements 得到此对象,代表插件 DexClassLoader--代表插件
File pluginDirFile = getDir("plugin", Context.MODE_PRIVATE);
File file = new File(pluginDirFile.getAbsoluteFile() + File.separator + "p.apk");
if (!file.exists()) {
throw new FileNotFoundException("没有找到插件包!!: " + file.getAbsolutePath());
} else {
Log.i("ZXX", "找到插件: " + file.getAbsolutePath());
}
String pluginPath = file.getAbsolutePath();
File fileDir = this.getDir("pluginDir", Context.MODE_PRIVATE); // data/data/包名/pluginDir/
DexClassLoader dexClassLoader = new
DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, getClassLoader());
Class mBaseDexClassLoaderClassPlugin = Class.forName("dalvik.system.BaseDexClassLoader");
// private final DexPathList pathList;
Field pathListFieldPlugin = mBaseDexClassLoaderClassPlugin.getDeclaredField("pathList");
pathListFieldPlugin.setAccessible(true);
Object mDexPathListPlugin = pathListFieldPlugin.get(dexClassLoader);
Field dexElementsFieldPlugin = mDexPathListPlugin.getClass().getDeclaredField("dexElements");
dexElementsFieldPlugin.setAccessible(true);
// 本质就是 Element[] dexElements
Object dexElementsPlugin = dexElementsFieldPlugin.get(mDexPathListPlugin);
// 第三步:创建出 新的 dexElements []
int mainDexLeng = Array.getLength(dexElements);
int pluginDexLeng = Array.getLength(dexElementsPlugin);
int sumDexLeng = mainDexLeng + pluginDexLeng;
// 参数一:int[] String[] ... 我们需要Element[]
// 参数二:数组对象的长度
// 本质就是 Element[] newDexElements
Object newDexElements = Array.newInstance(dexElements.getClass().getComponentType(),sumDexLeng); // 创建数组对象
// 第四步:宿主dexElements + 插件dexElements =----> 融合 新的 newDexElements
for (int i = 0; i < sumDexLeng; i++) {
// 先融合宿主
if (i < mainDexLeng) {
// 参数一:新要融合的容器 -- newDexElements
Array.set(newDexElements, i, Array.get(dexElements, i));
} else { // 再融合插件的
Array.set(newDexElements, i, Array.get(dexElementsPlugin, i - mainDexLeng));
}
}
// 第五步:把新的 newDexElements,设置到宿主中去
// 宿主
dexElementsField.set(mDexPathList, newDexElements);
// 处理加载插件中的布局,这里和占位式一致
doPluginLayoutLoad();
}
4、小结
hook插件化主要有三个重点步骤:
欺骗AMS,绕过AMS对插件Activity的检测,主要是通过偷龙转凤的方式实现
hook AMS启动Activity的LAUNCH_ACTIVITY事件,启动插件Activity
将插件Dex和宿主Dex合并
这种方式实现的插件化,插件Activity中可以使用this,侵入性低。但由于用了许多hook操作,系统适配需要做的工作较多。重点的三个操作尤其需要根据系统源码做一定的适配
3、LoadedApk式实现插件化
在hook实现插件化中,由于是将所有的插件都加入到dexElements中,宿主和插件用的还是同一个ClassLoader。下面我们来介绍LoadedApk式实现插件化,这事一种使用多个ClassLoader实现的插件化
1、特点
宿主和插件用的ClassLoader不是同一个
2、原理
欺骗AMS和偷龙转凤的实现和hook式实现插件化是一样的,不同的是hook式实现插件化是在BaseDexClassLoader中的dexElements中加入插件的dex,来达到成功加载插件类的目的。而LoadedApk式则不是。下面来分析LoadedApk式实现插件化的原理
查看ActivityThread中启动Activity的代码
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
//1、获取LoadedApk
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
。。。省略
Activity activity = null;
try {
//2、从LoadedApk中获取ClassLoader,用于加载Activity类
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
//实例化Activity
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
。。。省略
获取PackageInfo的代码如下:
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
//主要是这里,根据包名从mPackages中获取,那么只要构造插件的LoadedApk对象,放入到mPackeges中就能实现
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
3、实现步骤
欺骗AMS的操作和偷龙转风环节和hook一致,不再赘述。
1、下面介绍如何构建LoadedApk对象,加入到ActivityThread中的mPackages中
/**
* 自己创造一个LoadedApk.ClassLoader 添加到 mPackages,此LoadedApk 专门用来加载插件里面的 class
*/
private void customLoadedApkAction() throws Exception {
File pluginDirFile = getDir("plugin", Context.MODE_PRIVATE);
File file = new File( pluginDirFile.getAbsoluteFile() + File.separator + "p.apk");
if (!file.exists()) {
throw new FileNotFoundException("插件包不存在..." + file.getAbsolutePath());
}
String pulginPath = file.getAbsolutePath();
// mPackages 添加 自定义的LoadedApk
// final ArrayMap<String, WeakReference<LoadedApk>> mPackages 添加自定义LoadedApk
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
// 执行此方法 public static ActivityThread currentActivityThread() 拿到 ActivityThread对象
Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);
Field mPackagesField = mActivityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
// 拿到mPackages对象
Object mPackagesObj = mPackagesField.get(mActivityThread);
Map mPackages = (Map) mPackagesObj;
// 如何自定义一个 LoadedApk,系统是如何创造LoadedApk的,我们就怎么去创造LoadedApk
// 执行此 public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo)
Class mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Field defaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
defaultField.setAccessible(true);
Object defaultObj = defaultField.get(null);
/**
* ApplicationInfo 如何获取,我们之前学习 APK解析源码分析
*/
ApplicationInfo applicationInfo = getApplicationInfoAction();
Method mLoadedApkMethod = mActivityThreadClass.getMethod("getPackageInfoNoCheck", ApplicationInfo.class, mCompatibilityInfoClass); // 类类型
// 执行 才能拿到 LoedApk 对象
Object mLoadedApk = mLoadedApkMethod.invoke(mActivityThread, applicationInfo, defaultObj);
// 自定义加载器 加载插件
// String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent
File fileDir = getDir("pulginPathDir", Context.MODE_PRIVATE);
// 自定义 加载插件的 ClassLoader
ClassLoader classLoader = new PluginClassLoader(pulginPath,fileDir.getAbsolutePath(), null, getClassLoader());
Field mClassLoaderField = mLoadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(mLoadedApk, classLoader); // 替换 LoadedApk 里面的 ClassLoader
// 添加自定义的 LoadedApk 专门加载 插件里面的 class
// 最终的目标 mPackages.put(插件的包名,插件的LoadedApk);
WeakReference weakReference = new WeakReference(mLoadedApk); // 放入 自定义的LoadedApk --》 插件的
mPackages.put(applicationInfo.packageName, weakReference); // 增加了我们自己的LoadedApk
}
/**
* 获取 ApplicationInfo 为插件服务的
* @return
* @throws
*/
private ApplicationInfo getApplicationInfoAction() throws Exception {
// 执行此public static ApplicationInfo generateApplicationInfo方法,拿到ApplicationInfo
Class mPackageParserClass = Class.forName("android.content.pm.PackageParser");
Object mPackageParser = mPackageParserClass.newInstance();
// generateApplicationInfo方法的类类型
Class $PackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Method mApplicationInfoMethod = mPackageParserClass.getMethod("generateApplicationInfo",$PackageClass,
int.class, mPackageUserStateClass);
File dirFile = getDir("plugin", Context.MODE_PRIVATE);
File file = new File(dirFile.getAbsoluteFile() + File.separator + "p.apk");
String pulginPath = file.getAbsolutePath();
// 执行此public Package parsePackage(File packageFile, int flags)方法,拿到 Package
// 获得执行方法的对象
Method mPackageMethod = mPackageParserClass.getMethod("parsePackage", File.class, int.class);
Object mPackage = mPackageMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);
// 参数 Package p, int flags, PackageUserState state
ApplicationInfo applicationInfo = (ApplicationInfo)
mApplicationInfoMethod.invoke(mPackageParser, mPackage, 0, mPackageUserStateClass.newInstance());
// 获得的 ApplicationInfo 就是插件的 ApplicationInfo
// 我们这里获取的 ApplicationInfo
// applicationInfo.publicSourceDir = 插件的路径;
// applicationInfo.sourceDir = 插件的路径;
applicationInfo.publicSourceDir = pulginPath;
applicationInfo.sourceDir = pulginPath;
return applicationInfo;
}
2、hook AMS启动Activity的回调
class MyCallback implements Handler.Callback {
private Handler mH;
public MyCallback(Handler mH) {
this.mH = mH;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
// 做我们在自己的业务逻辑(把ProxyActivity 换成 TestActivity)
Object obj = msg.obj; // 本质 ActivityClientRecord
try {
// 我们要获取之前Hook携带过来的 TestActivity
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
// 获取 intent 对象,才能取出携带过来的 actionIntent
Intent intent = (Intent) intentField.get(obj);
Intent actionIntent = intent.getParcelableExtra("actionIntent");
if (actionIntent != null) {
intentField.set(obj, actionIntent); // 把ProxyActivity 换成 插件Activity
/***
* 我们在以下代码中,对插件 和 宿主 进行区分
*/
Field activityInfoField = obj.getClass().getDeclaredField("activityInfo");
activityInfoField.setAccessible(true); //授权
ActivityInfo activityInfo = (ActivityInfo) activityInfoField.get(obj);
// 宿主的Intent的getPackage会拿到包名,插件的会是空,用来判断是否是插件Intent
if (actionIntent.getPackage() == null) {
//将applicationInfo的包名改为插件的包名,这样拿到的LoadedApk才是我们自定义的
activityInfo.applicationInfo.packageName = actionIntent.getComponent().getPackageName();
// 这个是下个步骤,hook PMS,绕过PMS的检测
hookGetPackageInfo();
} else { // 宿主
activityInfo.applicationInfo.packageName = actionIntent.getPackage();
}
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
mH.handleMessage(msg);
// 让系统继续正常往下执行
// return false; // 系统就会往下执行
return true; // 系统不会往下执行
}
3、hook PMS,绕过检测
光这样操作还不行,在Activity启动时,PMS会检测包名对应的Apk是否有安装(LoadedApk中的initializeJavaContextClassLoader方法),没有安装会报错。
调用流程:performLaunchActivity->makeApplication->initializeJavaContextClassLoader。
initializeJavaContextClassLoader()方法代码如下:
IPackageManager pm = ActivityThread.getPackageManager();
android.content.pm.PackageInfo pi;
try {
pi = pm.getPackageInfo(mPackageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
UserHandle.myUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
if (pi == null) {
throw new IllegalStateException("Unable to get package info for "
+ mPackageName + "; is package not installed?");
}
因此还需要hook PMS,绕过检测,实现代码如下:
// Hook 拦截此 getPackageInfo 做自己的逻辑
private void hookGetPackageInfo() {
try {
// sPackageManager 替换 我们自己的动态代理
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
Field sCurrentActivityThreadField = mActivityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
Field sPackageManagerField = mActivityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
final Object packageManager = sPackageManagerField.get(null);
/**
* 动态代理
*/
Class mIPackageManagerClass = Class.forName("android.content.pm.IPackageManager");
Object mIPackageManagerProxy = Proxy.newProxyInstance(getClassLoader(),
new Class[]{mIPackageManagerClass}, // 要监听的接口
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("getPackageInfo".equals(method.getName())) {
// 如何才能绕过 PMS, 欺骗系统
// pi != null
return new PackageInfo(); // 成功绕过 PMS检测
}
// 让系统正常继续执行下去
return method.invoke(packageManager, args);
}
});
// 替换 狸猫换太子 换成我们自己的 动态代理
sPackageManagerField.set(null, mIPackageManagerProxy);
} catch (Exception e) {
e.printStackTrace();
}
}
4、小结
LoadedApk方式实现插件化,主要在启动Activity时,加载这个环节和hook方式不同。
主要步骤是:
模仿系统源码,实现插件apk的LoadedApk实例。并放置到ActivityThread中的mPackages对象中
在ActivityThread的mH回调中,偷龙转凤插件Activity的同时,将activityInfo中的applicationInfo中的包名替换为插件的包名,从而让后续逻辑使用插件的LoadedApk
最后,hook PMS,绕过PMS对插件是否安装的检测
4、三种实现插件化方式的小结
占位式实现插件化是比较稳定的,兼容性较好,因为没有hook系统的api。但由于要时刻注意使用宿主的上下文,编写插件是比较难受的
hook方式实现插件化,不用考虑宿主环境,但是对系统api进行了hook,兼容性较差。
LoadedApk方式实现插件化,和hook方式接近,不用考虑宿主环境,但对系统api进行了hook,兼容性较差
三、市面上的插件化框架
1、美团Robust
美团Robust插件化的实现方式不像上述讲的三种方式,而是借鉴的instant run的实现,在在编译打包阶段对每个函数都插入一段控制逻辑代码
简单看下修复流程:
下面的方法,在编译期间会加入一段控制逻辑,判断是走补丁操作,还是原操作
public long getIndex() {
return 100;
}
编译后的getIndex
public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
}
}
return 100L;
}
加载补丁时,会反射给changeQuickRedirect设置实例,当该实例不为空,则走插件补丁的逻辑
当然不是这么简单就能完美实现,具体可参考:
https://tech.meituan.com/2016/09/14/android-robust.html
2、腾讯QZone和Tinker
腾讯的QZone和Tinker本质上使用的是hook方式实现的插件化,操作的是dexElements
这里我们介绍下类的verify:
在apk安装时,虚拟机会将classes.dex优化成odex文件,然后才会执行。在这个过程中,会进行类的verify操作,如果调用关系的类都在同一个dex,就会被打上CLASS_ISPREVERIFIED
标志,然后写入odex文件。
而在运行时,如果是被打上了标志的类引用了其他dex的类,则会报错。
因此要解决打上标志这个问题。
QZone的做法是:在每个类的构造方法中,去引用一个其他Dex中的类,从而避免被打上标志
而Tinker的做法是:将宿主dex和插件dex进行合并,然后将dexElements中的就dex删除,将合并后的dex加入。所有的代码都在同一个dex中,也就不会有CLASS_ISPREVERIFIED
的问题了
四、SDK插件化
1、建议选择占位式实现
SDK插件化本质上和app的插件化区别不大,不过SDK如果四大组件不多,非常建议使用占位式插件化,因为兼容性问题较少。
2、自定义插件Context
但由于sdk通常依附于宿主Activity调用,但最好不要对宿主Activity的getClassLoader和getResources进行处理,这样可以避免影响宿主的逻辑。于是可以实现一个具备插件ClassLoader和Resouces的Context给Sdk使用
public class SQwanCore implements ISQwanCore {
@Override
public void init(Context context) {
//构造一个带有插件classLoader和Resources的Context
SdkContextProxy sdkContext = new SdkContextProxy(context);
try {
ISQwanCore sdkObj = (ISQwanCore) sdkContext.getClassLoader().loadClass("com.sq.plugin.PluginSQwanCore").newInstance();
sdkObj.init(sdkContext);
} catch (Exception e) {
e.printStackTrace();
}
}
}
SdkContextProxy代码如下:
public class SdkContextProxy extends ContextWrapper {
private Context baseContext;
public SdkContextProxy(Context base) {
super(base);
baseContext = base;
}
@Override
public ClassLoader getClassLoader() {
return PluginManager.getInstance(baseContext).getClassLoader();
}
@Override
public Resources getResources() {
return PluginManager.getInstance(baseContext).getResources();
}
//启动Activity时,做特殊操作,引导到ProxyActivity,参照占位式的ProxyActivity
@Override
public void startActivity(Intent intent) {
String className = intent.getComponent().getClassName();
Intent proxyIntent = new Intent(this, ProxyActivity.class);
proxyIntent.putExtra("className", className); // 包名+插件Activity
// 要给插件Activity进栈
super.startActivity(proxyIntent);
}
}
Sdk的插件实现:
public class PluginSQwanCore implements ISQwanCore {
@Override
public void init(Context context) {
//PluginActivity要根据标准IActivityInterface实现
context.startActivity(new Intent(context, PluginActivity.class));
}
}
其他逻辑参照占位式插件化实现即可
至于SDK插件化的CLASS_ISPREVERIFIED
最简单的处理方式是:将插件和宿主中共有的类,在插件中删除即可,如本案例中ISQwanCore类在插件中删除。
3、资源处理
常见问题:
AssetManager的适配(19以上和以下不同)
能代理的和不能代理的(xml文件中的资源引用)
隔离宿主和插件的Resources(利用ContextWrapper处理)
资源ID冲突怎么处理(使用gradle修改资源ID)
getIdentifier冲突怎么处理(插件内优先插件,ResourceWrapper使用)
将宿主和插件资源合并出一个大的Resources,为什么?
1、宿主的资源中是包含系统资源的,这块需要用
2、sdk有些资源放在宿主中,方便切包(如闪屏图)
public class SuperHostResources {
private Context mContext;
private Resources mResources;
public SuperHostResources(Context context, String pluginPath) {
mContext = context;
mResources = buildHostResources(pluginPath);
}
private Resources buildHostResources(String pluginPath) {
Resources hostResources = mContext.getResources();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
try {
AssetManager assetManager = mContext.getResources().getAssets();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, pluginPath);
hostResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
hostResources = mContext.getResources();
}
} else {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, pluginPath);
//冲突了不行的,要改id后才可以这么做
String baseApkPath = mContext.getApplicationInfo().sourceDir;
addAssetPathMethod.invoke(assetManager, baseApkPath);
hostResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
hostResources = mContext.getResources();
}
}
return hostResources;
}
public Resources get() {
return mResources;
}
}
使用ResourceWrapper,为什么?
1、如getIdentifier,可以在这里操作出优先加载插件,还是优先加载宿主
public class MixResources extends ResourcesWrapper {
private Resources mPluginResources;
private String mPluginPkgName;
public MixResources(Resources hostResources, Context context, String pluginPath) {
super(hostResources);
PluginResources pluginResourcesBuilder = new PluginResources(context, pluginPath);
mPluginResources = pluginResourcesBuilder.get();
mPluginPkgName = pluginResourcesBuilder.getPkgName();
}
public MixResources(Resources hostResources, Resources pluginResources, String pluginPkgName) {
super(hostResources);
mPluginResources = pluginResources;
mPluginPkgName = pluginPkgName;
}
public String getPluginPkgName() {
return mPluginPkgName;
}
@Override
public CharSequence getText(int id) throws Resources.NotFoundException {
try {
return super.getText(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getText(id);
}
}
@Override
public String getString(int id) throws Resources.NotFoundException {
try {
return super.getString(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getString(id);
}
}
@Override
public String getString(int id, Object... formatArgs) throws Resources.NotFoundException {
try {
return super.getString(id,formatArgs);
} catch (Resources.NotFoundException e) {
return mPluginResources.getString(id,formatArgs);
}
}
@Override
public float getDimension(int id) throws Resources.NotFoundException {
try {
return super.getDimension(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getDimension(id);
}
}
@Override
public int getDimensionPixelOffset(int id) throws Resources.NotFoundException {
try {
return super.getDimensionPixelOffset(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getDimensionPixelOffset(id);
}
}
@Override
public int getDimensionPixelSize(int id) throws Resources.NotFoundException {
try {
return super.getDimensionPixelSize(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getDimensionPixelSize(id);
}
}
@Override
public Drawable getDrawable(int id) throws Resources.NotFoundException {
try {
return super.getDrawable(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getDrawable(id);
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawable(int id, Resources.Theme theme) throws Resources.NotFoundException {
try {
return super.getDrawable(id, theme);
} catch (Resources.NotFoundException e) {
return mPluginResources.getDrawable(id,theme);
}
}
@Override
public Drawable getDrawableForDensity(int id, int density) throws Resources.NotFoundException {
try {
return super.getDrawableForDensity(id, density);
} catch (Resources.NotFoundException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
return mPluginResources.getDrawableForDensity(id, density);
} else {
return null;
}
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawableForDensity(int id, int density, Resources.Theme theme) {
try {
return super.getDrawableForDensity(id, density, theme);
} catch (Exception e) {
return mPluginResources.getDrawableForDensity(id,density,theme);
}
}
@Override
public int getColor(int id) throws Resources.NotFoundException {
try {
return super.getColor(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getColor(id);
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public int getColor(int id, Resources.Theme theme) throws Resources.NotFoundException {
try {
return super.getColor(id,theme);
} catch (Resources.NotFoundException e) {
return mPluginResources.getColor(id,theme);
}
}
@Override
public ColorStateList getColorStateList(int id) throws Resources.NotFoundException {
try {
return super.getColorStateList(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getColorStateList(id);
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public ColorStateList getColorStateList(int id, Resources.Theme theme) throws Resources.NotFoundException {
try {
return super.getColorStateList(id,theme);
} catch (Resources.NotFoundException e) {
return mPluginResources.getColorStateList(id,theme);
}
}
@Override
public boolean getBoolean(int id) throws Resources.NotFoundException {
try {
return super.getBoolean(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getBoolean(id);
}
}
@Override
public XmlResourceParser getLayout(int id) throws Resources.NotFoundException {
try {
return super.getLayout(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getLayout(id);
}
}
@Override
public String getResourceName(int resid) throws Resources.NotFoundException {
try {
return super.getResourceName(resid);
} catch (Resources.NotFoundException e) {
return mPluginResources.getResourceName(resid);
}
}
@Override
public int getInteger(int id) throws Resources.NotFoundException {
try {
return super.getInteger(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getInteger(id);
}
}
@Override
public CharSequence getText(int id, CharSequence def) {
try {
return super.getText(id,def);
} catch (Resources.NotFoundException e) {
return mPluginResources.getText(id,def);
}
}
@Override
public InputStream openRawResource(int id) throws Resources.NotFoundException {
try {
return super.openRawResource(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.openRawResource(id);
}
}
@Override
public XmlResourceParser getXml(int id) throws Resources.NotFoundException {
try {
return super.getXml(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getXml(id);
}
}
@Override
public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws Resources.NotFoundException {
try {
super.getValue(id, outValue, resolveRefs);
} catch (Resources.NotFoundException e) {
mPluginResources.getValue(id, outValue, resolveRefs);
}
}
@Override
public Movie getMovie(int id) throws Resources.NotFoundException {
try {
return super.getMovie(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getMovie(id);
}
}
@Override
public XmlResourceParser getAnimation(int id) throws Resources.NotFoundException {
try {
return super.getAnimation(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getAnimation(id);
}
}
@Override
public InputStream openRawResource(int id, TypedValue value) throws Resources.NotFoundException {
try {
return super.openRawResource(id,value);
} catch (Resources.NotFoundException e) {
return mPluginResources.openRawResource(id,value);
}
}
@Override
public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {
try {
return super.openRawResourceFd(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.openRawResourceFd(id);
}
}
@Override
public int getIdentifier(String name, String defType, String defPackage) {
int pluginId = super.getIdentifier(name, defType, defPackage);
if (pluginId <= 0) {
return mPluginResources.getIdentifier(name, defType, mPluginPkgName);
}
return pluginId;
}
public int getIdentifierFromPlugin(String name, String defType) {
return mPluginResources.getIdentifier(name, defType, mPluginPkgName);
}
}
五、小结
1、本文介绍了三种常见的插件化实现方案,包括占位式、hook式、LoadedApk式以及他们各自的特点
2、介绍了市面上常见的插件化框架的方案,其中简要介绍了如何避免CLASS_ISPREVERIFIED
问题
3、介绍SDK的插件化,SDK的Context的实现方案
关注我获取更多知识或者投稿