写在前面
目的
本文旨在对四大组件的一些探索中,总结出在插件化开发过程中,有哪些需要注意的地方,需要解决的问题。
计划
此篇是介绍插件化的一种实现方案,预计是写完三篇
- 插件化入门(1) -基础 -点我查看
- 插件化入门(2) -欺上瞒下的方案
- 插件化入门(3) -代理式插件化 -todo
提前的说明
-
Android插件化有非常多的实现方案,本文中列举的,也并不是最优解或普遍方案
比如在插件化入门(1)中提到加载插件apk使用了PathClassLoader,但实际在demo中,使用的方案是合并dex及so到宿主ClassLoader,具体的请参考Demo
-
因为插件化大量使用了hook,因为Android 不同版本的源码实现不同,导致代码不能运行在所有的android版本上,本文提及的代码和demo,都没有进行版本兼容,仅支持sdk23
ps:目前应该就一处可能需要兼容,DexPathListClass#makePathElements,这个方法在不同版本的参数不同,但比较简单,如果想运行在sdk23之外的其他版本,请手动兼容下
Android四大组件的特殊性
Android四大组件有自己的特殊性,比如Activity、Service、ContentProvider都必须在manifest中注册 如果不处理,直接使用加载到的插件Activity是肯定会出错的 所以下面会对Android四大组件怎么去实现插件化做一个介绍
以下内容默认已经完成了dex的加载,资源的加载,so文件的加载
activity
插件activity并没有注册在manifest的问题
分析
在完成dex加载后,如果直接启动Activity,启动代码如下
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.dennisce.testplugin","com.dennisce.testplugin.PluginActivity"));
startActivity(newIntent);
会直接得到下面的错误
想想确实也是,这个Activity确实没有注册在宿主app的manifest中,要解决这个问题,先找到是哪里抛出的异常?
从startActivity开始跟代码,最后找到Instrumentation#execStartActivity
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
......
int result = ActivityTaskManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
......
}
看一下checkStartActivityResult 方法
public static void checkStartActivityResult(int res, Object intent) {
.......
switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
.......
}
}
可以看到checkStartActivityResult 根据 ActivityTaskManager.getService().startActivity() 的返回值及Intent的getComponent()来判断是否抛出异常
所以关键点在 ActivityTaskManager.getService().startActivity() 的返回值
先看 ActivityTaskManager.getService().startActivity() 做了什么事情?
/** @hide */
public static IActivityManager getService() {
return IActivityManager.get();
}
@UnsupportedAppUsage
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
// 调用点
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
public static android.app.IActivityManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof android.app.IActivityManager))) {
return ((android.app.IActivityManager)iin);
}
// 调用点
return new android.app.IActivityManager.Stub.Proxy(obj);
}
到这里,熟悉binder,AIDL的同学应该都明白了
ActivityTaskManager.getService().startActivity()实际上是调用了AMS的startActivty
所以没有注册在manifest里面的activity是由ams做了检测,然后在Instrumentation#checkStartActivityResult抛出了异常
AMS的代码是没有办法去hook的,所以要在调用AMS前,将的Intent替换为manifest中注册过的Activity来骗过AMS的检测,然后在AMS回到Client的时候启动真正的Activity
欺骗AMS
目标:在AMS前,将intent替换为在宿主app声明的一个占位activity
先看下startActivty到AMS的流程(api 28)
从start的流程看到有下面几个着手点
- 重新Activity的startActivityForResult方法
- 对activity的mInstrumentation进行hook
- 之前提到的IActivityManagerSingleton instance是一个IActivityManager类型接口,生成一个动态代理,替换到IActivityManagerSingleton 的值,使getService拿到的就是hook后的对象
代码演示使用hook IActivityManagerSingleton 的值来达到目的
版本兼容问题
在Android的不同版本,Instrumentation#execStartActivity的实现是不完全一样的 比如在7.0和8.0,调用AMS的路径不同,在8.0之前,还会有AcitivtyManagerNative、AcitivtyManagerProxy的路径,本质是没有区别的,这里不再多加赘述
//android 7.0
public ActivityResult execStartActivity(
.......
int result = ActivityManagerNative.getDefault()//hook的地方
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
........
}
//android 8.0
public ActivityResult execStartActivity(
........
int result = ActivityManager.getService()//hook的地方
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target, requestCode, 0, null, options);
........
}
代码
public static boolean tryHookStartActivity() {
try {
Object singleton;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//版本兼容
singleton = ReflectUtils.reflect("android.app.ActivityManager").field("IActivityManagerSingleton").get();
} else {
singleton = ReflectUtils.reflect("android.app.ActivityManagerNative").field("gDefault").get();
}
final Object iActivityManager = ReflectUtils.reflect(singleton).field("mInstance").get();
Class<?> iActivityManagerInterface = ReflectUtils.reflect("android.app.IActivityManager").get();
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{iActivityManagerInterface},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!method.getName().equals("startActivity")) {
return method.invoke(iActivityManager, args);
}
Timber.d("invoke start activity");
List<Class> activitys = ClassUtils.getActivitiesClass(Utils.getApp(), Utils.getApp().getPackageName(), null);
Intent rawIntent = (Intent) args[2];
for (Class clz : activitys) {
if (clz.getName().equals(rawIntent.getComponent().getClassName())) {// 在manifest注册的activity
return method.invoke(iActivityManager, args);
}
}
Intent newIntent = new Intent();
newIntent.putExtra(Constant.RAW_INTENT, (Intent) args[2]);
newIntent.setComponent(new ComponentName(StubActivity.class.getPackage().getName(), StubActivity.class.getName()));
args[2] = newIntent;
return method.invoke(iActivityManager, args);
}
});
ReflectUtils.reflect(singleton).field("mInstance", proxy);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
欺骗完AMS,不能让自己也被骗了
要在AMS做完处理,回到Client创建Activity的流程中,将Activity替换为真正要启动的Activity
实际跟了代码后发现,可以hook的点有两个
- H的mCallBack字段
- ActivityTrread的mInstrumentation字段
代码
这里以hook mInstrumentation为例
创建一个自定义的Instrumentation
public class ProxyInstrumentation extends Instrumentation {
private Instrumentation rawInstrumentation;
ProxyInstrumentation(Instrumentation rawInstrumentation) {
this.rawInstrumentation = rawInstrumentation;
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// intent不为占位class
if (!intent.getComponent().getClassName().equals(StubActivity.class.getName())) {
return rawInstrumentation.newActivity(cl, className, intent);
}
// rawIntent为null
Intent rawIntent = intent.getParcelableExtra(Constant.RAW_INTENT);
if (rawIntent==null){
return rawInstrumentation.newActivity(cl, className, intent);
}
return
//跳转到真正的activity
rawInstrumentation.newActivity(ApkLoadManager.getSingleton().getPluginClassLoader(), rawIntent.getComponent().getClassName(), rawIntent);
}
}
利用反射赋值
public static boolean tryHookInstrumentation() {
try {
Object currentActivityThread = ReflectUtils.reflect("android.app.ActivityThread").field("sCurrentActivityThread").get();
Instrumentation rawInstrumentation = ReflectUtils.reflect(currentActivityThread).field("mInstrumentation").get();
ReflectUtils.reflect(currentActivityThread).field("mInstrumentation", new ProxyInstrumentation(rawInstrumentation));
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
activity中的资源加载
在插件化入门(1)中,分析了activity中调用资源的方法,得出的结论是,主要使用了getAssets以及getResources,所以重写Activity的这两个方法,返回宿主生成的AssetManager和Resources
插件Activity改造
override fun getAssets(): AssetManager? {
Resource.assetManager?.run {
return this
}
return super.getAssets()
}
override fun getResources(): Resources {
Resource.resources?.run {
return this
}
return super.getResources()
}
public class Resource {
public static AssetManager assetManager = null;
public static Resources resources = null;
}
宿主创建Resources并赋值
private Boolean createResource() {
.......创建AssetManager、Resources
// 在插件Activity中,我重写了getAssets()及getResources()
// 而获取到的AssetManager、Resources就是这里反射设置的。所以插件activty可以正常使用资源
ReflectUtils.reflect("com.dennisce.testplugin.Resource", pluginClassLoader).field("assetManager", assetManager);
ReflectUtils.reflect("com.dennisce.testplugin.Resource", pluginClassLoader).field("resources", resources);
return true;
}
tips:为什么代码有的是kotlin,有的是java呢?
因为创建项目的时候一个kt,一个java了,也就没改,插件app是kt写的,宿主app是java写的
launchMode
前面通过stubActivity欺骗过了AMS,完成了Activity的启动,但是只处理了standard模式,singleTop、singleTask、singleInstance怎么去处理呢?
答案:宿主app申明多个launchMode的Actiivty,解析插件类的manifest使用不同launchMode的Actiivty来启动
service
service的启动和Activity中有很多相似的地方,这里不再更多的解释,贴出一点关键代码
private ComponentName startServiceCommon(Intent service, boolean requireForeground,
UserHandle user) {
...............
ComponentName cn = ActivityManager.getService().startService(
mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
getContentResolver()), requireForeground,
getOpPackageName(), user.getIdentifier());
.........
}
ActivityManager.getService().startService看到了熟悉的东西
所以同样可以使用动态代理 IActivityManager的值来实现hook
下面说说和Activity不同的地方
service 在AMS回调Client的时候没有使用Instrumentation,所以不能hook Instrumentation 这个点,但是,可以hook H类。
public static boolean tryHookActivityThread() {
Object currentActivityThread = ReflectUtils.reflect("android.app.ActivityThread").field("sCurrentActivityThread").get();
Handler mH = ReflectUtils.reflect(currentActivityThread).field("mH").get();
ReflectUtils.reflect(mH).field("mCallback",new ProxyMhHandler(mH));
return true;
}
@Override
public boolean handleMessage(@NonNull Message msg) {
if (msg.what==114){
replaceRealService(msg);
}
base.handleMessage(msg);
return true;
}
private void replaceRealService(Message message){
Object object=message.obj;
ServiceInfo serviceInfo=ReflectUtils.reflect(object).field("info").get();
// 这里获取真实的service名字,应该有一个映射表,这里只是验证记载是否可行,所以直接给了serviceName
serviceInfo.name= "com.dennisce.testplugin.PluginService";
}
Service不能像Activity一样同一个StubActivity启动多次,所以需要注册多个service,具体的数量就要根据实际业务和项目情况来定义了
BoardcastReceiver
先说结论,boardcastReceiver不需要和AMS交互,所以在处理BoardcastReceiver的时候,可以将其视为一个普通类来处理,直接注册就行
BroadcastReceiver receiver = (BroadcastReceiver) ApkLoadManager.getSingleton().getPluginClassLoader().loadClass("com.dennisce.testplugin.PluginBroadcastReceiver").newInstance();
context.registerReceiver(receiver,new IntentFilter("Plugin_Receiver"));
静态广播
静态广播的注册在插件的manifest中,如果想要注册,需要解析manifest,然后将其动态注册
解析manifest
private boolean registerReceivers() {
Object packageParser = ReflectUtils.reflect("android.content.pm.PackageParser").newInstance().get();
Object packageObj = ReflectUtils.reflect(packageParser).method("parsePackage", apkFile, PackageManager.GET_RECEIVERS).get();
List receivers = ReflectUtils.reflect(packageObj).field("receivers").get();
for (Object receiver : receivers) {
if (!registerReceiver(receiver)){
return false;
}
}
return true;
}
注册
private boolean registerReceiver(Object receiver) {
List<IntentFilter> filtes= ReflectUtils.reflect(receiver).field("intents").get();
for (IntentFilter intentFilter:filtes){
ActivityInfo receiverInfo=ReflectUtils.reflect(receiver).field("info").get();
BroadcastReceiver broadcastReceiver=ReflectUtils.reflect(receiverInfo.name,pluginClassLoader).newInstance().get();
context.registerReceiver(broadcastReceiver,intentFilter);
}
return true;
}
另外一种情况
应用在未启动的情况下也可以收到静态广播,虽然android8.0对其做了非常多的限制,但是还是有少部分白名单广播可以接收,所以上一种静态转动态的方案无法适配应用未启动的情况
在宿主注册一个静态广播,注册足够多的action,然后再转发到插件的广播
ContentProvider
ContentProvider的处理也是比较简单的,声明一个StubContentProvider,由他来转发ContentProvider
直接上代码
public class StubContentProvider extends ContentProvider { // 核心代码
@Override
public Uri insert(@NotNull Uri uri, ContentValues values) { // 转发
return getContext().getContentResolver().insert(getRealUri(uri), values);
}
private Uri getRealUri(Uri raw) { // 获取真正的ContentProvider
String uriString = raw.toString();
uriString = uriString.replaceAll(rawAuth + '/', "");
Uri newUri = Uri.parse(uriString);
Timber.i("realUri:%s", newUri);
return newUri;
}
}
// 注册插件中的Providers
public static boolean installProviders(Context context, String apkPath) {
List<ProviderInfo> providerInfoList = getProviders(apkPath);
for (ProviderInfo providerInfo : providerInfoList) {
providerInfo.applicationInfo.packageName = context.getPackageName();
}
Object currentActivityThread = ReflectUtils.reflect("android.app.ActivityThread").field("sCurrentActivityThread").get();
ReflectUtils.reflect(currentActivityThread).method("installContentProviders", context, providerInfoList);
return true;
}
// 获取插件中的Providers
private static List<ProviderInfo> getProviders(String apkPath) {
Object packageParser = ReflectUtils.reflect("android.content.pm.PackageParser").newInstance().get();
Object packageObj = ReflectUtils.reflect(packageParser).method("parsePackage", new File(apkPath), PackageManager.GET_PROVIDERS).get();
List providers = ReflectUtils.reflect(packageObj).field("providers").get();
Object defaultUserState = ReflectUtils.reflect("android.content.pm.PackageUserState").newInstance().get();
int userId = ReflectUtils.reflect("android.os.UserHandle").method("getCallingUserId").get();
List<ProviderInfo> ret = new ArrayList<>();
for (Object provider : providers) {
ProviderInfo info = ReflectUtils.reflect(packageParser).method("generateProviderInfo", provider, 0, defaultUserState, userId).get();
ret.add(info);
}
return ret;
}
Demo地址
关于插件化
hook带来的问题
在实现4大组件的过程中,大量的使用了hook,其实有两个问题
- 有大量的适配工作,举个例子,比如启动Actiivity/Service的流程,在Android 7.0/8.0.9.0/10.0的路径都不一样,需要做兼容
- hook使用的部分API,已经被标记为hide或者UnsupportedAppUsage,对于未来版本来说,有很大的不确定性
有没有其他的方案
关于插件化,其实一直有两个方向
- 欺上瞒下 hook系统服务,欺骗AMS,来直接启动插件的activity等
- 代理式插件化, 将四大组件当做普通的类,由proxy组件去分发生命周期及代码,有点类似上文提到的ContentProvider方案,但不止这么简单,更多的细节,以后的文章再做分析
写在最后
由于本人水平有限,如有错误,欢迎交流指出