Android Hook无清单启动Activity的应用
我们已经实现了启动没有在menifest
中注册的Activity
的效果,然而,这样做到底在生产开发中有什么样的应用呢?
答案:插件化
插件化是一个宽泛的概念,只要是实现了宿主app上插件功能的灵活拔插,实现了宿主app业务和插件功能的完全解耦,就可以称之为插件化
原理是用
宿主中真实Activity
作为代理
,来启动插件中的Activity
,管理插件中Activity的生命周期,并且处理好插件源代码
和资源文件
。现在,插件化有另一种方式,就是利用
无清单启动Activity的原理
,实现插件apk中Activity的启动
整体思路
下方有两张图:表示了插件化架构中,插件单独运行,和插件作为宿主的一部分随宿主启动的技术关键点
如上图,如果跟随宿主一起启动,插件apk
的资源文件要能够被宿主读到,插件的apk
的class
文件也必须能够被宿主读取,实现的方式就是,让在宿主的代码中进行hook
编程,生成一个能够读取宿主以及所有插件内class
的ClassLoader
,以及一个能够读取宿主以及插件内所有资源的Resource
。而,实现的具体过程,就是一个融合过程。
实际效果展示
宿主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
发生融合,也就是说,即使我们完成了对ClassLoader
和Resource
的融合,实现了宿主对插件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
,而我们构建能够访问插件中class
的classLoader
是DexClassLoader
,它们有共同的父类BaseDexClassLoader
,而且,这个BaseDexClassLoader
类的本身就拥有能够装载多个dex
路径的能力。
插件DexClassLoader
读取的是插件apk
中的classes.dex
,宿主PathClassLoader
读取的是data/app/包名/base.apk
的classes.dex
。它们分别将读取到的路径,存到了上图中的Element[] dexElements
数组中
那么如果我们可以将插件DexClassLoader
中的dexElements
融合到宿主PathClassLoader
的dexElements
中去,就可以实现宿主读取插件apk
的class.dex
HookInjectHelper
类中的injectPluginClass
方法,就是以上面的思路为依据进行的hook
。具体步骤为:
-
- 构建插件
DexClassLoader
对象
- 构建插件
-
- 获得系统的
PathClassLoader
对象
- 获得系统的
-
- 分别获得插件
DexClassLoader
和系统PathClassLoader
的DexPathList
中的dexElements
数组
- 分别获得插件
-
- 将上述两个
dexElements
数组进行融合
- 将上述两个
-
- 将融合之后的的
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
默认不带ActionBar
,Theme
的使用也和前者不同
所以到目前为止也很疑惑,不过倒并不影响我们插件化开发,用android.app.Activity
和AppCompatActivity
开发的Activity
也并没有出现什么兼容问题
其实在 Android插件化启动Activity 中,也出现过一次类似的问题,使用android.app.Activity
没问题,但是换成AppCompatActivity
,则会报上面一样的错误,相当诡异,但是也同样不影响开发.
结语
插件化开发这个话题,看起来高深莫测,实际上玩起来也并不简单。实现的方式也不止一种。目前了解看来有两种解决方案,用宿主的真实Activity
去代理插件Activity
,另一种就是用hook
去绕过manifest
检查。
两种方案各有优劣,hook
可能会失效,因为谷歌最近发布了禁用反射的API名单,而且`Android Studio也在使用反射的时候提示,反射可能失效。但是,还是那句话,天塌下来砸不到我们的头上,自然有大佬顶着,到时候,如果谷歌真的禁用反射,国内的巨佬们自然有新的解决办法,到时候跟随大流就好。
而代理Activity
的方式,则多了一个PluginLib
层,需要维护,好处就是,不用看谷歌脸色。
版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/29/android-hook-launching-activity-without-manifests-application/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。
共有 0 条评论