Android Hook无清单启动Activity的应用

我们已经实现了启动没有在menifest中注册的Activity的效果,然而,这样做到底在生产开发中有什么样的应用呢?

答案:插件化

插件化是一个宽泛的概念,只要是实现了宿主app上插件功能的灵活拔插,实现了宿主app业务和插件功能的完全解耦,就可以称之为插件化

原理是用宿主中真实Activity作为代理,来启动插件中的Activity,管理插件中Activity的生命周期,并且处理好插件源代码资源文件

现在,插件化有另一种方式,就是利用无清单启动Activity的原理,实现插件apk中Activity的启动

整体思路

下方有两张图:表示了插件化架构中,插件单独运行,和插件作为宿主的一部分随宿主启动的技术关键点


如上图,如果跟随宿主一起启动,插件apk的资源文件要能够被宿主读到,插件的apkclass文件也必须能够被宿主读取,实现的方式就是,让在宿主的代码中进行hook编程,生成一个能够读取宿主以及所有插件内classClassLoader,以及一个能够读取宿主以及插件内所有资源的Resource。而,实现的具体过程,就是一个融合过程。

实际效果展示

绕过Manifest检测的插件化启动

宿主manifest文件:只有一个入口Activity,其他的一概没有

<application
    android:name=".app.MyApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    tools:ignore="GoogleAppIndexingWarning">
    <activity android:name=".ui.MainActivity"
        android:screenOrientation="portrait">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

Demo源码讲解

无论是宿主的代码,还是插件的代码,都非常简单,唯一阅读价值的,就是宿主的Hook核心代码

在讲解Hook核心代码之前,先回顾一下所实现的效果:能够绕过系统的manifest检测机制,让没有在manifest中注册的Activity也能够正常启动
一定有读者在看完这篇文章之后,会想,能够不去注册就可以启动Activity,是很神奇,但是又有什么利用价值呢?仅仅是为了不去注册就去干涉系统逻辑,太华而不实了

这个问题的答案:
hook实现插件化启动Activity,插件中的manifest并不会和宿主的manifest发生融合,也就是说,即使我们完成了对ClassLoaderResource的融合,实现了宿主对插件class资源的访问,如果不能绕过系统的manifest检测,依然不能启动插件的Activity

所以,用hook技术实现插件化启动Activity,完整思路是:

以下是关键代码:

宿主的MyApplication.java主要用于调用Hook核心代码:

public class MyApplication extends Application {

    private Resources newResource;

    public static String pluginPath = null;

    @Override
    public void onCreate() {
        super.onCreate();
        pluginPath = AssetUtil.copyAssetToCache(this, Const.PLUGIN_FILE_NAME);

        //Hook第一次,绕过manifest检测
        GlobalActivityHookHelper.hook(this);

        //Hook第二次把插件的源文件class导入到系统的ClassLoader中
        HookInjectHelper.injectPluginClass(this);

        //Hook第三次,加载插件资源包,让系统的Resources能够读取插件的资源
        newResource = HookInjectHelper.injectPluginResources(this);
    }

    //重写资源管理器,资源管理器是每个Activity自带的,
    //而Application的getResources则是所有Activity共有的
    //重写了它,就不必一个一个Activity去重写了
    @Override
    public Resources getResources() {
        return newResource == null ? super.getResources() : newResource;
    }
}

绕过manifest检测的hook核心代码GlobalActivityHookHelper.java

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

import cn.appblog.hookplugindemo.utils.Util;

/**
 * hookAMS Activity的实现方式3:
 * hookAMS AMS(ActivityManagerService)兼容 26以上,以及26以下的版本(SDK 26对AMS实例的获取进行了代码更改)
 * 今天,在已经能够实现全局hook MS的方案下,进一步改造,实现 无清单启动Activity
 */
public class GlobalActivityHookHelper {

    public static void hook(Context context) {

        hookAMS(context);//使用假的Activity,骗过AMS的检测

        if (ifSdkOverIncluding28())
            hookActivityThread_mH_AfterIncluding28();//将真实的Intent还原回去,让系统可以跳到原本该跳的地方.
        else {
            hookActivityThread_mH_before28(context);
        }

        hookPM(context);//由于AppCompatActivity存在PMS检测,如果这里不hook的话,就会包PackageNameNotFoundException
    }

    //设备系统版本是不是大于等于26
    private static boolean ifSdkOverIncluding26() {
        int SDK_INT = Build.VERSION.SDK_INT;
        if (SDK_INT > 26 || SDK_INT == 26) {
            return true;
        } else {
            return false;
        }
    }

    //设备系统版本是不是大于等于26
    private static boolean ifSdkOverIncluding28() {
        int SDK_INT = Build.VERSION.SDK_INT;
        if (SDK_INT > 28 || SDK_INT == 28) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 这里对AMS进行hook
     *
     * @param context
     */
    private static void hookAMS(Context context) {
        try {
            Class<?> ActivityManagerClz;
            final Object IActivityManagerObj;//这个就是AMS实例
            Method getServiceMethod;
            Field IActivityManagerSingletonField;
            if (ifSdkOverIncluding26()) {//26,27,28的ams获取方式是通过ActivityManager.getService()
                ActivityManagerClz = Class.forName("android.app.ActivityManager");
                getServiceMethod = ActivityManagerClz.getDeclaredMethod("getService");
                IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("IActivityManagerSingleton");//单例类成员的名字也不一样
            } else {//25往下,是ActivityManagerNative.getDefault()
                ActivityManagerClz = Class.forName("android.app.ActivityManagerNative");
                getServiceMethod = ActivityManagerClz.getDeclaredMethod("getDefault");
                IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("gDefault");//单例类成员的名字也不一样
            }
            IActivityManagerObj = getServiceMethod.invoke(null);//OK,已经取得这个系统自己的AMS实例

            // 2.现在创建我们的AMS实例
            // 由于IActivityManager是一个接口,那么其实我们可以使用Proxy类来进行代理对象的创建
            // 结果被摆了一道,IActivityManager这玩意居然还是个AIDL,动态生成的类,编译器还不认识这个类,怎么办?反射咯
            Class<?> IActivityManagerClz = Class.forName("android.app.IActivityManager");

            // 构建代理类需要两个东西用于创建伪装的Intent
            String packageName = Util.getPMName(context);
            String clz = Util.getHostClzName(context, packageName);
            Object proxyIActivityManager =
                    Proxy.newProxyInstance(
                            Thread.currentThread().getContextClassLoader(),
                            new Class[]{IActivityManagerClz},
                            new ProxyInvocation(IActivityManagerObj, packageName, clz));

            //3.拿到AMS实例,然后用代理的AMS换掉真正的AMS,代理的AMS则是用 假的Intent骗过了 activity manifest检测.
            //偷梁换柱
            IActivityManagerSingletonField.setAccessible(true);
            Object IActivityManagerSingletonObj = IActivityManagerSingletonField.get(null);
            Class<?> SingletonClz = Class.forName("android.util.Singleton");//反射创建一个Singleton的class
            Field mInstanceField = SingletonClz.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
            mInstanceField.set(IActivityManagerSingletonObj, proxyIActivityManager);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static final String ORI_INTENT_TAG = "origin_intent";

    /**
     * 把InvocationHandler的实现类提取出来,因为这里包含了核心技术逻辑,最好独立,方便维护
     */
    private static class ProxyInvocation implements InvocationHandler {

        Object amsObj;
        String packageName;//这两个String是用来构建Intent的ComponentName的
        String clz;

        public ProxyInvocation(Object amsInstance, String packageName, String clz) {
            this.amsObj = amsInstance;
            this.packageName = packageName;
            this.clz = clz;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //proxy是创建出来的代理类,method是接口中的方法,args是接口执行时的实参
            if (method.getName().equals("startActivity")) {
                Log.d("GlobalActivityHook", "全局hook 到了 startActivity");

                Intent currentRealIntent = null;//侦测到startActivity动作之后,把intent存到这里
                int intentIndex = -1;
                //遍历参数,找到Intent
                for (int i = 0; i < args.length; i++) {
                    Object temp = args[i];
                    if (temp instanceof Intent) {
                        currentRealIntent = (Intent) temp;//这是原始的Intent,存起来,后面用得着
                        intentIndex = i;
                        break;
                    }
                }

                //构造自己的Intent,这是为了绕过manifest检测
                Intent proxyIntent = new Intent();
                ComponentName componentName = new ComponentName(packageName, clz);//用ComponentName重新创建一个intent
                proxyIntent.setComponent(componentName);
                proxyIntent.putExtra(ORI_INTENT_TAG, currentRealIntent);//将真正的proxy作为参数,存放到extras中,后面会拿出来还原

                args[intentIndex] = proxyIntent;//替换掉intent
                //哟,已经成功绕过了manifest清单检测. 那么,我不能老让它跳到 伪装的Activity啊,我要给他还原回去,那么,去哪里还原呢?
                //继续看源码。

            }
            return method.invoke(amsObj, args);
        }
    }

    //下面进行ActivityThread的mH的hook,这是针对SDK28做的hook
    private static void hookActivityThread_mH_AfterIncluding28() {

        try {
            //确定hook点,ActivityThread类的mh
            // 先拿到ActivityThread
            Class<?> ActivityThreadClz = Class.forName("android.app.ActivityThread");
            Field field = ActivityThreadClz.getDeclaredField("sCurrentActivityThread");
            field.setAccessible(true);
            Object ActivityThreadObj = field.get(null);//OK,拿到主线程实例

            //现在拿mH
            Field mHField = ActivityThreadClz.getDeclaredField("mH");
            mHField.setAccessible(true);
            Handler mHObj = (Handler) mHField.get(ActivityThreadObj);//ok,当前的mH拿到了
            //再拿它的mCallback成员
            Field mCallbackField = Handler.class.getDeclaredField("mCallback");
            mCallbackField.setAccessible(true);

            //2.现在,造一个代理mH,
            // 他就是一个简单的Handler子类
            ProxyHandlerCallback proxyMHCallback = new ProxyHandlerCallback();//错,不需要重写全部mH,只需要对mH的callback进行重新定义

            //3.替换
            //将Handler的mCallback成员,替换成创建出来的代理HandlerCallback
            mCallbackField.set(mHObj, proxyMHCallback);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static class ProxyHandlerCallback implements Handler.Callback {

        private int EXECUTE_TRANSACTION = 159;//这个值,是android.app.ActivityThread的内部类H 中定义的常量EXECUTE_TRANSACTION

        @Override
        public boolean handleMessage(Message msg) {
            boolean result = false;//返回值,请看Handler的源码,dispatchMessage就会懂了
            //Handler的dispatchMessage有3个callback优先级,首先是msg自带的callback,其次是Handler的成员mCallback,最后才是Handler类自身的handlerMessage方法,
            //它成员mCallback.handleMessage的返回值为true,则不会继续往下执行 Handler.handlerMessage
            //我们这里只是要hook,插入逻辑,所以必须返回false,让Handler原本的handlerMessage能够执行.
            if (msg.what == EXECUTE_TRANSACTION) {//这是跳转的时候,要对intent进行还原
                try {
                    //先把相关@hide的类都建好
                    Class<?> ClientTransactionClz = Class.forName("android.app.servertransaction.ClientTransaction");
                    Class<?> LaunchActivityItemClz = Class.forName("android.app.servertransaction.LaunchActivityItem");

                    Field mActivityCallbacksField = ClientTransactionClz.getDeclaredField("mActivityCallbacks");//ClientTransaction的成员
                    mActivityCallbacksField.setAccessible(true);
                    //类型判定,好习惯
                    if (!ClientTransactionClz.isInstance(msg.obj)) return true;
                    Object mActivityCallbacksObj = mActivityCallbacksField.get(msg.obj);//根据源码,在这个分支里面,msg.obj就是 ClientTransaction类型,所以,直接用
                    //拿到了ClientTransaction的List<ClientTransactionItem> mActivityCallbacks;
                    List list = (List) mActivityCallbacksObj;

                    if (list.size() == 0) return true;
                    Object LaunchActivityItemObj = list.get(0);//所以这里直接就拿到第一个就好了

                    if (!LaunchActivityItemClz.isInstance(LaunchActivityItemObj)) return true;
                    //这里必须判定 LaunchActivityItemClz,
                    // 因为 最初的ActivityResultItem传进去之后都被转化成了这LaunchActivityItemClz的实例

                    Field mIntentField = LaunchActivityItemClz.getDeclaredField("mIntent");
                    mIntentField.setAccessible(true);
                    Intent mIntent = (Intent) mIntentField.get(LaunchActivityItemObj);
                    Intent oriIntent = (Intent) mIntent.getExtras().get(ORI_INTENT_TAG);
                    //那么现在有了最原始的intent,应该怎么处理呢?
                    Log.d("1", "2");
                    mIntentField.set(LaunchActivityItemObj, oriIntent);
                    return result;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return result;
        }
    }

    /**
     * @param context
     * @throws Exception
     */
    private static void hookActivityThread_mH_before28(Context context) {
        try {
            Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
            Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread");
            sCurrentActivityThreadField.setAccessible(true);
            Object sCurrentActivityThreadObj = sCurrentActivityThreadField.get(null);

            Field mHField = activityThreadClazz.getDeclaredField("mH");
            mHField.setAccessible(true);
            Handler mH = (Handler) mHField.get(sCurrentActivityThreadObj);
            Field callBackField = Handler.class.getDeclaredField("mCallback");
            callBackField.setAccessible(true);
            callBackField.set(mH, new ActivityThreadHandlerCallBack(context));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static class ActivityThreadHandlerCallBack implements Handler.Callback {

        private final Context mContext;

        public ActivityThreadHandlerCallBack(Context context) {
            mContext = context;
        }

        @Override
        public boolean handleMessage(Message msg) {
            int LAUNCH_ACTIVITY = 0;
            try {
                Class<?> clazz = Class.forName("android.app.ActivityThread$H");
                Field field = clazz.getField("LAUNCH_ACTIVITY");
                LAUNCH_ACTIVITY = field.getInt(null);
            } catch (Exception e) {
            }
            if (msg.what == LAUNCH_ACTIVITY) {
                handleLaunchActivity(mContext, msg);
            }
            return false;
        }
    }

    private static void handleLaunchActivity(Context context, Message msg) {
        try {
            Object obj = msg.obj;
            Field intentField = obj.getClass().getDeclaredField("intent");
            intentField.setAccessible(true);
            Intent proxyIntent = (Intent) intentField.get(obj);
            //拿到之前真实要被启动的Intent 然后把Intent换掉
            Intent originallyIntent = proxyIntent.getParcelableExtra(ORI_INTENT_TAG);
            if (originallyIntent == null) {
                return;
            }
            proxyIntent.setComponent(originallyIntent.getComponent());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 由于我只在SDK 28 对应的9.0设备上做过成功的试验,所以此方法命名为hookPMAfter28
     *
     * @param context
     */
    private static void hookPM(Context context) {
        try {
            String pmName = Util.getPMName(context);
            String hostClzName = Util.getHostClzName(context, pmName);

            Class<?> forName = Class.forName("android.app.ActivityThread");//PM居然是来自ActivityThread
            Field field = forName.getDeclaredField("sCurrentActivityThread");
            field.setAccessible(true);
            Object activityThread = field.get(null);
            Method getPackageManager = activityThread.getClass().getDeclaredMethod("getPackageManager");
            Object iPackageManager = getPackageManager.invoke(activityThread);

            String packageName = Util.getPMName(context);
            PMSInvocationHandler handler = new PMSInvocationHandler(iPackageManager, packageName, hostClzName);
            Class<?> iPackageManagerIntercept = Class.forName("android.content.pm.IPackageManager");
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new
                    Class<?>[]{iPackageManagerIntercept}, handler);
            // 获取 sPackageManager 属性
            Field iPackageManagerField = activityThread.getClass().getDeclaredField("sPackageManager");
            iPackageManagerField.setAccessible(true);
            iPackageManagerField.set(activityThread, proxy);
        } catch (
                Exception e)

        {
            e.printStackTrace();
        }
    }

    static class PMSInvocationHandler implements InvocationHandler {

        private Object base;
        private String packageName;
        private String hostClzName;

        public PMSInvocationHandler(Object base, String packageName, String hostClzName) {
            this.packageName = packageName;
            this.base = base;
            this.hostClzName = hostClzName;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

            if (method.getName().equals("getActivityInfo")) {
                ComponentName componentName = new ComponentName(packageName, hostClzName);
                return method.invoke(base, componentName, PackageManager.GET_META_DATA, 0);//破费,一定是这样
            }

            return method.invoke(base, args);
        }
    }

}

将宿主和插件的ClassLoader/Resource融合的HookInjectHelper.java

import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
import cn.appblog.hookplugindemo.app.MyApplication;

public class HookInjectHelper {

    /**
     *
     * 此方法的作用是:插件内的class融合到宿主的classLoader中,让宿主可以直接读取插件内的class
     *
     * @param context
     */
    public static void injectPluginClass(Context context) {
        String cachePath = context.getCacheDir().getAbsolutePath();
        String apkPath = MyApplication.pluginPath;

        //还记不记得dexClassLoader?它是专门用于加载外部apk的classes.dex文件的
        //(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
        // 4个参数分别是,外部dex的path,优化之后的目录,lib库文件查找目录,我们这没有用到lib里面的so,所以可以设置为null,最后一个是父ClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(apkPath, cachePath, null, context.getClassLoader());
        //先构造一个能够读取外部apk的classLoader对象

        //第一步 找到插件的Elements数组dexPathlist dexElement

        try {
            Class myDexClazzLoader = Class.forName("dalvik.system.BaseDexClassLoader");
            Field myPathListFiled = myDexClazzLoader.getDeclaredField("pathList");
            myPathListFiled.setAccessible(true);
            Object myPathListObject = myPathListFiled.get(dexClassLoader);

            Class myPathClazz = myPathListObject.getClass();
            Field myElementsField = myPathClazz.getDeclaredField("dexElements");
            myElementsField.setAccessible(true);
            //自己插件的  dexElements[]
            Object myElements = myElementsField.get(myPathListObject);

            //第二步 找到系统的Elements数组dexElements
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            Class baseDexClazzLoader = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListFiled = baseDexClazzLoader.getDeclaredField("pathList");
            pathListFiled.setAccessible(true);
            Object pathListObject = pathListFiled.get(pathClassLoader);

            Class systemPathClazz = pathListObject.getClass();
            Field systemElementsField = systemPathClazz.getDeclaredField("dexElements");
            systemElementsField.setAccessible(true);
            //系统的dexElements[]
            Object systemElements = systemElementsField.get(pathListObject);
            //第三步 上面的dexElements数组合并成新的dexElements然后通过反射重新注入系统的Field(dexElements)变量中

            //新的Element[]对象
            //dalvik.system.Element

            int systemLength = Array.getLength(systemElements);
            int myLength = Array.getLength(myElements);
            //找到 Element 的Class类型数组每一个成员的类型
            Class<?> sigleElementClazz = systemElements.getClass().getComponentType();
            int newSysteLength = myLength + systemLength;
            Object newElementsArray = Array.newInstance(sigleElementClazz, newSysteLength);
            //融合
            for (int i = 0; i < newSysteLength; i++) {
                //先融合插件的Elements
                if (i < myLength) {
                    Array.set(newElementsArray, i, Array.get(myElements, i));
                } else {
                    Array.set(newElementsArray, i, Array.get(systemElements, i - myLength));
                }
            }
            Field elementsField = pathListObject.getClass().getDeclaredField("dexElements");

            elementsField.setAccessible(true);
            //将新生成的EleMents数组对象重新放到系统中去
            elementsField.set(pathListObject, newElementsArray);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Resources injectPluginResources(Context context) {
        AssetManager assetManager;
        Resources newResource = null;
        String apkPath = MyApplication.pluginPath;
        try {
            assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.setAccessible(true);
            addAssetPathMethod.invoke(assetManager, apkPath);
            Resources supResource = context.getResources();
            newResource = new Resources(assetManager, supResource.getDisplayMetrics(), supResource.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return newResource;
    }
}

关于Resource的融合,在文章:Android hook技术实现一键换肤 里面有提及
绕过manifest检测,在另一篇文章 Android Hook-实现无清单启动Activity 有详解,这里不再赘述

详细讲讲ClassLoader如何融合

我们用context.getClassLoader拿到的是PathClassLoader,而我们构建能够访问插件中classclassLoaderDexClassLoader,它们有共同的父类BaseDexClassLoader,而且,这个BaseDexClassLoader类的本身就拥有能够装载多个dex路径的能力。

插件DexClassLoader读取的是插件apk中的classes.dex,宿主PathClassLoader读取的是data/app/包名/base.apkclasses.dex。它们分别将读取到的路径,存到了上图中的Element[] dexElements数组中

那么如果我们可以将插件DexClassLoader中的dexElements融合到宿主PathClassLoaderdexElements中去,就可以实现宿主读取插件apkclass.dex

HookInjectHelper类中的injectPluginClass方法,就是以上面的思路为依据进行的hook。具体步骤为:

    1. 构建插件DexClassLoader对象
    1. 获得系统的PathClassLoader对象
    1. 分别获得插件DexClassLoader和系统PathClassLoaderDexPathList中的dexElements数组
    1. 将上述两个dexElements数组进行融合
    1. 将融合之后的的dexElements设置到系统PathClassLoader

至此,系统也能够访问插件apk中的class了

那么接下来,如何启动插件中的Activity呢?

由于我们在写宿主代码的时候,并不能直接引用插件的类,所以我们只能通过如下方式:

findViewById(R.id.btn1).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("cn.appblog.plugin",  //插件的包名
                "cn.appblog.plugin.Plugin1Activity"));  //插件Activity的完整类名
        startActivity(intent);
    }
});

那么又如何启动宿主自身的Activity其他呢?可以按照上面的方式。或者也可以用普通的方式:

findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(MainActivity.this, Main2Activity.class);
        startActivity(intent);
    }
});

而宿主的manifest里,依然只有一个Activity,其他的都可以不经注册直接启动,剩下的这一个是为了作为Launch Activity

<application
    android:name=".app.MyApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    tools:ignore="GoogleAppIndexingWarning">
    <activity android:name=".ui.MainActivity"
        android:screenOrientation="portrait">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

OK,全部讲完

坑坑更健康

细心的读者会发现,在宿主里面用的是android.app.Activity,而不是AppCompatActivity。包括宿主内的第二个Main2Activity,依然是android.app.Activity

因为发现,如果换成AppCompatActivity,在启动宿主的时候,就会报莫名其妙的异常:

03-09 18:39:19.069 16437-16437/cn.appblog.hookplugindemo E/AndroidRuntime: FATAL EXCEPTION: main
    Process: cn.appblog.hookplugindemo, PID: 16437
    java.lang.RuntimeException: Unable to start activity ComponentInfo{cn.appblog.hookplugindemo/cn.appblog.hookplugindemo.ui.MainActivity}: java.lang.NullPointerException: Attempt to invoke interface method 'void android.support.v7.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2443)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2503)
        at android.app.ActivityThread.-wrap11(ActivityThread.java)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1353)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:148)
        at android.app.ActivityThread.main(ActivityThread.java:5529)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:745)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:635)
     Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'void android.support.v7.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
        at android.support.v7.app.AppCompatDelegateImplV9.createSubDecor(AppCompatDelegateImplV9.java:410)
        at android.support.v7.app.AppCompatDelegateImplV9.ensureSubDecor(AppCompatDelegateImplV9.java:323)
        at android.support.v7.app.AppCompatDelegateImplV9.setContentView(AppCompatDelegateImplV9.java:284)
        at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:139)
        at cn.appblog.hookplugindemo.ui.MainActivity.onCreate(MainActivity.java:22)
        at android.app.Activity.performCreate(Activity.java:6278)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2396)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2503) 
        at android.app.ActivityThread.-wrap11(ActivityThread.java) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1353) 
        at android.os.Handler.dispatchMessage(Handler.java:102) 
        at android.os.Looper.loop(Looper.java:148) 
        at android.app.ActivityThread.main(ActivityThread.java:5529) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:745) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:635) 

请教了大佬,得到了靠谱答案,AppCompatActivity在启动的时候会进行上下文检查,于是报出了上面的问题。使用Activity即可,不用使用AppCompatActivity

实际上后续也查了两者的区别,AppCompatActivity是为了兼容低版本设备而设计的,它和Activity的区别是,AppCompatActivity拥有默认的ActionBar,也拥有自己的Theme类。而Activity默认不带ActionBarTheme的使用也和前者不同

所以到目前为止也很疑惑,不过倒并不影响我们插件化开发,用android.app.ActivityAppCompatActivity开发的Activity也并没有出现什么兼容问题

其实在 Android插件化启动Activity 中,也出现过一次类似的问题,使用android.app.Activity没问题,但是换成AppCompatActivity,则会报上面一样的错误,相当诡异,但是也同样不影响开发.

结语

插件化开发这个话题,看起来高深莫测,实际上玩起来也并不简单。实现的方式也不止一种。目前了解看来有两种解决方案,用宿主的真实Activity去代理插件Activity,另一种就是用hook去绕过manifest检查。

两种方案各有优劣,hook可能会失效,因为谷歌最近发布了禁用反射的API名单,而且`Android Studio也在使用反射的时候提示,反射可能失效。但是,还是那句话,天塌下来砸不到我们的头上,自然有大佬顶着,到时候,如果谷歌真的禁用反射,国内的巨佬们自然有新的解决办法,到时候跟随大流就好。

代理Activity的方式,则多了一个PluginLib层,需要维护,好处就是,不用看谷歌脸色。

本文转载至:https://www.jianshu.com/p/a8184c8fe688

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/29/android-hook-launching-activity-without-manifests-application/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Android Hook无清单启动Activity的应用
我们已经实现了启动没有在menifest中注册的Activity的效果,然而,这样做到底在生产开发中有什么样的应用呢? 答案:插件化 插件化是一个宽泛的概念,只要是实……
<<上一篇
下一篇>>
文章目录
关闭
目 录