什么是插件化
以"插件"的形式,动态加载功能模块。将业务功能模块单独拆分出来打包成APK,也就是插件。主App可以在运行时动态加载插件APK,实现业务功能的动态加载。
为什么需要插件化
- 减小APK体积,可以通过插件化将App拆分成宿主APK及若干个插件APK以达到显小APK的效果。
- 快速迭代,插件化可以实现业务功能模块的动态加载,在用户不需要重新下载应用安装的情况下实现业务功能的上线
- 提高开发效率,由于插件化需要对业务功能模块进行拆分,所以业务功能模块之间必须完全解耦,使得能够同时并行开发多个业务功能模块,单独运行调试。提高了开发效率及模块的复用。
怎么实现插件化
VirtualAPK-git地址
安卓插件化VirtualAPK
Android Hook Activity 的几种姿势
Android插件化——谈谈我理解的坑位
Android 插件化原理解析——插件加载机制
源码分析 — Activity的启动流程
插件化的方案主要可以分为两种,一种是Hook+代理也称为占位式,还有一种就是下文准备介绍的纯Hook的方案
下文将基于Android SDK25以启动插件Activity为例进行阐述。需要注意的由于Hook涉及到Android系统源码,纯Hook的方案在不同的Android版本上会有差异。启动插件Activity可以总结为以下三步
具体可参考源码
源码地址
- 第一步-绕过AMS检测
由于插件Activity没有在宿主的AndroidManifest中注册,宿主直接启动会抛出异常ActivityNotFoundException。所以当宿主启动插件Activity时需要现将插件Activity替换为宿主中预先定义好的占位Activity,占位Activity需要在宿主的AndroidManifest中进行注册以绕过ActivityManagerService检测。
private void hookAMSCheck25(final Application context) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
// 动态代理
Class mIActivityManagerClass = Class.forName("android.app.IActivityManager");
Class mActivityManagerNativeClass2 = Class.forName("android.app.ActivityManagerNative");
final Object mIActivityManager = mActivityManagerNativeClass2.getMethod("getDefault").invoke(null);
Object mIActivityManagerProxy = Proxy.newProxyInstance(
context.getClassLoader(),
new Class[]{mIActivityManagerClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
Intent intentReal = (Intent) args[2];
String packageName = context.getPackageName();
String packageName1 = intentReal.getComponent().getPackageName();
if (!packageName.equals(packageName1)){
//跳转插件Activity
//将插件Activity替换成PlaceHolderActivity绕过AMS检查
Log.d(TAG, "插件Activity替换成PlaceHolderActivity绕过AMS检查");
Intent intent = new Intent(context, PlaceHolderActivity.class);
intent.putExtra("actionIntent", intentReal);
args[2] = intent;
}
}
//Log.d(TAG, "拦截到了IActivityManager里面的方法" + method.getName());
return method.invoke(mIActivityManager, args);
}
});
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");
Field mInstanceField = mSingletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true); // 让虚拟机不要检测 权限修饰符
mInstanceField.set(gDefault, mIActivityManagerProxy); // 替换是需要gDefault
}
- 第二步-还原插件Activity,由于在第一步中为了绕过AMS检测将插件Activity替换为宿主中的占位Activity,所以在启动Activity前需要将被替换的插件Activity还原回来。
private void hookLaunchActivity(Application context) throws NoSuchFieldException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Field mCallbackFiled = Handler.class.getDeclaredField("mCallback");
mCallbackFiled.setAccessible(true);
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
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, context)); // 替换 增加我们自己的实现代码
}
public final int LAUNCH_ACTIVITY = 100;
class MyCallback implements Handler.Callback {
private Handler mH;
private Application context;
public MyCallback(Handler mH, Application context) {
this.mH = mH;
this.context = context;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
//hook Activity启动将被替换的插件Activity恢复回来
Object obj = msg.obj; //ActivityClientRecord
try {
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent intent = (Intent) intentField.get(obj);
//hook点1中存入的真实的插件Intent
Intent actionIntent = intent.getParcelableExtra("actionIntent");
if (actionIntent != null) {
//把占位Activity换成插件Activity
intentField.set(obj, actionIntent);
Field activityInfoField = obj.getClass().getDeclaredField("activityInfo");
activityInfoField.setAccessible(true);
ActivityInfo activityInfo = (ActivityInfo) activityInfoField.get(obj);
String packageName = actionIntent.getComponent().getPackageName();
activityInfo.name = actionIntent.getComponent().getClassName();
activityInfo.packageName = packageName;
activityInfo.applicationInfo.packageName = packageName;
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
mH.handleMessage(msg);
return true;
}
}
- hook点3-为插件创建LoadedApk,第二步中还原了插件Activity,但是插件中的类文件与资源文件宿主不能直接访问,所以在hook点2之后直接启动Activity是会报错的。因此需要为插件创建LoadedApk来实现类文件与资源文件的加载。
/*
* SDK源码中Activity由LoadedApk创建
* LoadedApk缓存在ActivityThread的mPackages中
* 所以需要将为插件创建LoadedApk并存放进ActivityThread的mPackages中
*/
private void hookLoadedApk(Context context, String apkPath) throws FileNotFoundException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, InstantiationException {
File file = new File(apkPath);
if (!file.exists()) {
throw new FileNotFoundException("插件包不存在..." + file.getAbsolutePath());
}
String pluginPath = file.getAbsolutePath();
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);
Field mPackagesField = mActivityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
Object mPackagesObj = mPackagesField.get(mActivityThread);
Map mPackages = (Map) mPackagesObj;
Class mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Field defaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
defaultField.setAccessible(true);
/*
* 创建LoadedApk需要的参数
*/
Object defaultObj = defaultField.get(null);
/*
* 创建LoadedApk需要的参数
*/
ApplicationInfo applicationInfo = getApplicationInfoAction(apkPath);
/*
* 反射调用ActivityThread的getPackageInfoNoCheck方法创建LoadedApk
*/
Method mLoadedApkMethod = mActivityThreadClass.getMethod("getPackageInfoNoCheck", ApplicationInfo.class, mCompatibilityInfoClass);
Object mLoadedApk = mLoadedApkMethod.invoke(mActivityThread, applicationInfo, defaultObj);
File fileDir = context.getDir("pulginPathDir", Context.MODE_PRIVATE);
ClassLoader classLoader = new DexClassLoader(pluginPath,fileDir.getAbsolutePath(), null, context.getClassLoader());
Field mClassLoaderField = mLoadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(mLoadedApk, classLoader);
WeakReference weakReference = new WeakReference(mLoadedApk);
mPackages.put(applicationInfo.packageName, weakReference);
}
private ApplicationInfo getApplicationInfoAction(String apkPath) throws InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
Class mPackageParserClass = Class.forName("android.content.pm.PackageParser");
Object mPackageParser = mPackageParserClass.newInstance();
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 file = new File(apkPath);
String pulginPath = file.getAbsolutePath();
Object mPackage = null;
try {
Method mPackageMethod = mPackageParserClass.getMethod("parsePackage", File.class, int.class);
mPackage = mPackageMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);
} catch (Exception e) {
e.printStackTrace();
}
ApplicationInfo applicationInfo = (ApplicationInfo)
mApplicationInfoMethod.invoke(mPackageParser, mPackage, 0, mPackageUserStateClass.newInstance());
applicationInfo.publicSourceDir = pulginPath;
applicationInfo.sourceDir = pulginPath;
return applicationInfo;
}
hook PackageManagerService避免插件LoadedApk创建Application时报错
private void hookGetPackageInfo(final Context context) {
try {
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(context.getClassLoader(),
new Class[]{mIPackageManagerClass}, // 要监听的接口
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("getPackageInfo".equals(method.getName())) {
Object packageInfo = method.invoke(packageManager, args);
if (packageInfo == null){
Log.d(TAG, "为插件创建packageInfo");
packageInfo = new PackageInfo();
}
return packageInfo;
}
return method.invoke(packageManager, args);
}
});
sPackageManagerField.set(null, mIPackageManagerProxy);
} catch (Exception e) {
e.printStackTrace();
}
}
总结
启动插件Activity需要处理以下三个步骤
- 将启动插件Activity替换为宿主中的占位Activity,绕过AMS检测
- 将被替换的插件Activity还原回来
- 为插件创建LoadedApk,实现对插件APK中类与资源文件的加载
使用纯Hook试的插件化方案优势在于侵入性较低。劣势在于兼容性较差,在hook的过程中通过反射访问的系统类与方法大部分都被hide,这些类与方法很有可能在Android版本迭代时发生改变。