插件化入门(2) -欺上瞒下的方案

1,275 阅读9分钟

写在前面

目的

本文旨在对四大组件的一些探索中,总结出在插件化开发过程中,有哪些需要注意的地方,需要解决的问题。

计划

此篇是介绍插件化的一种实现方案,预计是写完三篇

  • 插件化入门(1) -基础 -点我查看
  • 插件化入门(2) -欺上瞒下的方案
  • 插件化入门(3) -代理式插件化 -todo

提前的说明

  • Android插件化有非常多的实现方案,本文中列举的,也并不是最优解或普遍方案

    比如在插件化入门(1)中提到加载插件apk使用了PathClassLoader,但实际在demo中,使用的方案是合并dex及so到宿主ClassLoader,具体的请参考Demo

  • 因为插件化大量使用了hook,因为Android 不同版本的源码实现不同,导致代码不能运行在所有的android版本上,本文提及的代码和demo,都没有进行版本兼容,仅支持sdk23

    ps:目前应该就一处可能需要兼容,DexPathListClass#makePathElements,这个方法在不同版本的参数不同,但比较简单,如果想运行在sdk23之外的其他版本,请手动兼容下

Android四大组件的特殊性

Android四大组件有自己的特殊性,比如ActivityServiceContentProvider都必须在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方案,但不止这么简单,更多的细节,以后的文章再做分析

写在最后

由于本人水平有限,如有错误,欢迎交流指出