目录
- 1、概述
- 2、HOOK技术
- 2.1、根据Android9.0系统源码,查看Activity.startActivity(intent,...)的调用流程
- 2.2、根据Android9.0系统源码,查看Context.startActivity(intent,...)的调用流程
- 3、最终解决方案
- 3.1、实现思路
- 3.2、演示效果
- 4、最终代码实现
- 4.1、创建代理类:InstrumentationProxy.java
- 4.2、创建插件上下文类:PluginContext.java
- 4.3、宿主中TestActivity调用
- 4.4、宿主与插件的主题
- 4.5、插件依赖库保持简单一点
- 4.6、反射工具类Reflect.java
- 4.7、反射异常类ReflectException.java
- 5、实践过程中的各种报错及原因
- 5.1、Failed to resolve attribute at index 1: TypedValue{t=0x2/d=0x101005a a=-1}
- 5.2、'android.view.Window$Callback android.view.Window.getCallback()' on a null object reference
- 5.3、Exception while getting ActivityInfo
- 5.4、You need to use a Theme.AppCompat theme (or descendant) with this activity.
- 6、参考资料
1、概述
Android插件化是一种解决方案,当一个应用发展成一个平台级应用时,就更需要针对各个子业务模块按需动态加载,要做到按需动态加载一种是可以通过H5的方案,另一种就是
针对各个子业务模块单独开发成一个APK,这时候这个平台级应用我们称为宿主,子业务模块APK称为插件,宿主通过反射点击去学习、代理点击去学习等实现hook技术来完成插件APK的免安装加载。
所以必须要先了解这个HOOK技术。
2、HOOK技术
HOOK翻译成钩子,钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。在Android系统中,通俗来讲,就是去阅读源码,然后通过反射或代理的方式去替换/绕过系统原有的调用逻辑,嵌入自己的逻辑代码,从而实现业务功能的扩展。
Android的插件化过程中最重要的步骤就是在宿主应用中去打开未安装插件APK的Activity,要想实现这个过程,通过常规的API方式去调用是无法做到这一点的,startActivity只可以实现打开一个已安装应用的且android:exported="true"的Activity,所以我们必须要想办法去解决这个问题,也就是做到不安装APK也能打开其Activity。
所以,就需要通过源码去了解startActivity的调用过程,找到可以hook的地方,startActivty有三种常用的调用方式,Context.startActivity(intent,…),Activity.startActivity(intent,…),隐式调用。
2.1、根据Android9.0系统源码,查看Activity.startActivity(intent,…)的调用流程
[开始] --> [startActivity] --> [Activity.startActivityForResult]
–> [mInstrumentation.execStartActivity] --> [ActivityManager.getService().startActivity]
–> [ActivityManagerService.startActivity] --> [ActivityManagerService.startActivityAsUser]
–> [ActivityStarter.execute()] --> [startActivity] --> [startActivityUnchecked]
–> [ActivityStackSupervisor.resumeFocusedStackTopActivityLocked] --> [ActivityStack.resumeTopActivityUncheckedLocked]
–> [resumeTopActivityInnerLocked] --> [ClientLifecycleManager.scheduleTransaction]
–> [ClientTransaction.schedule] --> [IApplicationThread.scheduleTransaction]
–> [ActivityThread.this.scheduleTransaction] --> [ClientTransactionHandler.sendMessage]
–> [TransactionExecutor.execute] --> [executeCallbacks] --> [ClientTransactionItem.execute]
–> [LaunchActivityItem.execute] --> [ClientTransactionHandler.handleLaunchActivity]
–> [ActivityThread.handleLaunchActivity] --> [performLaunchActivity]
–> [mInstrumentation.newActivity] --> [mInstrumentation.callActivityOnCreate]
–> [Activity.performCreate] --> [Activity.onCreate] --> [结束]
2.2、根据Android9.0系统源码,查看Context.startActivity(intent,…)的调用流程
[开始] --> [getBaseContext.startActivity] --> [ContextImpl.startActivity] --> [mMainThread.getInstrumentation().execStartActivity] --> [ActivityManager.getService().startActivity] --> …之后同上Activity.startActivity… --> [结束]
看源码的网站,随便推荐一个: 点击查看Android系统源码 。
3、最终解决方案
3.1、实现思路
现在开始来选取Hook的注入点,主要是针对Instrumentation这个类进行代理扩展系统业务功能,execStartActivity执行前,先把传入的插件APK目标Intent替换成我们在宿主项目中创建的已在manifest.xml中注册的替身类ShaowActivity,绕过Manifest注册检查(对应的checkStartActivityResult函数就是做这个检查的),然后我们选择newActivity作为关键点,创建新的Activity时使用插件APK的Activity,创建好插件APK的Activity之后,还需要替换插件的资源(因为现在的资源还是宿主的),故我们选择在callActivityOnCreate这个地方作为替换插件APK的上下文、Resources、Theme等等资源的关键点,具体操作就是需要构建插件的上下文然后通过反射进行替换。(多多参考学习)
3.2、演示效果
4、最终代码实现
4.1、创建代理类:InstrumentationProxy.java
import android.app.Activity;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PersistableBundle;
import android.util.Log;
import com.xx.escape.plan.ProxyActivity;
import com.xx.escape.plan.plugin.PluginContext;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* @author james
* @date 2024-08-09 ~ 2024-08-23 调试完毕
* @brief description
* 标准版
*/
public class InstrumentationProxy extends Instrumentation {
public static final String TAG = InstrumentationProxy.class.getSimpleName();
Instrumentation mInstrumentation;
static final String KEY_TARGET_INTENT = "key_target_intent";
public InstrumentationProxy(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
}
private PluginContext mPluginContext;
public void inject(PluginContext pluginContext) {
this.mPluginContext = pluginContext;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d(TAG, "\n执行了execStartActivity, 参数如下: \n" + "who = [" + who + "], " +
"\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
"\ntarget = [" + target + "], \nintent = [" + intent +
"], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
try {
//List<ResolveInfo> infoList = who.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_ALL);//这种写法不兼容6.0以下的设备
boolean isPluginActivity = isUnregisteredManifestActivity((Activity) who, intent);
//替换系统传递的intent(这个意图里面就是我们要访问的插件APK的Activity界面,直接访问插件APK里面的Activity是会被系统拦截的,因为没有注册manifest.xml,所以我们需要伪装)为我们指定的目标targetIntent,通过伪装成访问宿主的ProxyActivity,跳过manifest.xml的注册检测
Intent targetIntent;
if (isPluginActivity) {
targetIntent = new Intent(who, ProxyActivity.class);
//塞入到Extra中,执行到后面的newActivity再进行还原
targetIntent.putExtra(KEY_TARGET_INTENT, intent);
} else {
targetIntent = intent;
}
Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class,
Activity.class, Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
return (ActivityResult) execStartActivity.invoke(mInstrumentation, who, contextThread, token, target, targetIntent, requestCode, options);
} catch (Exception e) {
throw new RuntimeException("don't support context start!");
}
}
/**
* 隐式调用
*
* @param intent
* @return
* @throws InstantiationException
* @throws IllegalAccessException
* @throws ClassNotFoundException
*/
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, String target,
Intent intent, int requestCode, Bundle options) {
Log.d(TAG, "\n执行了execStartActivity, 参数如下: \n" + "who = [" + who + "], " +
"\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
"\ntarget = [" + target + "], \nintent = [" + intent +
"], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
try {
boolean isPluginActivity = isUnregisteredManifestActivity((Activity) who, intent);
//替换系统传递的intent(这个意图里面就是我们要访问的插件APK的Activity界面,直接访问插件APK里面的Activity是会被系统拦截的,因为没有注册manifest.xml,所以我们需要伪装)为我们指定的目标targetIntent,通过伪装成访问宿主的ProxyActivity,跳过manifest.xml的注册检测
Intent targetIntent;
if (isPluginActivity) {
targetIntent = new Intent(who, ProxyActivity.class);
//塞入到Extra中,执行到后面的newActivity再进行还原
targetIntent.putExtra(KEY_TARGET_INTENT, intent);
} else {
targetIntent = intent;
}
Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class,
Activity.class, Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
return (ActivityResult) execStartActivity.invoke(mInstrumentation, who, contextThread, token, target, targetIntent, requestCode, options);
} catch (Exception e) {
throw new RuntimeException("don't support context start!");
}
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
if (intent != null) {
Intent targetIntent = intent.getParcelableExtra(KEY_TARGET_INTENT);
//通过插件的ClassLoader来载入一个插件的Activity
if (targetIntent != null && targetIntent.getComponent() != null) {
ComponentName componentName = targetIntent.getComponent();
return mInstrumentation.newActivity(mPluginContext.getClassLoader(), componentName.getClassName(), targetIntent);
}
}
return mInstrumentation.newActivity(cl, className, intent);
}
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
injectActivity(activity);
mInstrumentation.callActivityOnCreate(activity, icicle);
}
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) {
injectActivity(activity);
mInstrumentation.callActivityOnCreate(activity, icicle, persistentState);
}
/**
* 载入插件的Activity之后,就可以替换这个插件的Activity属性,mResources、mBase、mApplication
*
* @param activity
*/
private void injectActivity(Activity activity) {
try {
Intent targetIntent = activity.getIntent().getParcelableExtra(KEY_TARGET_INTENT);
if (targetIntent == null) {
Log.d(TAG, "没有目标参数,不是需要启动的插件Activity");
return;
}
boolean isPluginActivity = isUnregisteredManifestActivity(activity, targetIntent);
if (isPluginActivity) {
Context base = activity.getBaseContext();
Reflect.on(activity).set("mBase", mPluginContext);
Reflect.on(base).set("mResources", mPluginContext.getResources());
Reflect.on(activity).set("mResources", mPluginContext.getResources());
Reflect.on(activity).set("mApplication", mPluginContext.getApplicationContext());
//改变主题 ,解决DecorContentParent.setWindowCallback 空指针的问题 2131689738
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
activity.setTheme(mPluginContext.getTheme());
} else {
//mTheme属性是Activity没有,在Activity的父类ContextThemeWrapper中定义了这个属性 ContextThemeWrapper,也需要设置否则出现空指针的问题
Reflect.on(activity).set("mTheme", mPluginContext.getTheme());
}
//设置Title直接反射Activity的mTitle,替换成插件的即可,参考xPlugin框架
Reflect.on(activity).set("mTitle", "插件APK标题");
//兼容AppCompat ,替换Activity父类的 mThemeResource 这个属性值 old 2131689738/7f0f010a 0x7f0f010a Theme.AppCompat.NoActionBar false, true ; new 2131689477/7f0f0005 0x7f0f0005 AppFullScreenTheme false, @ref/0x00000000, true, false
//当Application 没有设置,只有Activity设置了@style/AppFullScreenTheme,则需要替换调用下面的代码;如果Application 设置了@style/AppFullScreenTheme,则不需要执行下面这段代码
setActivityResIdTheme(activity);
}
} catch (Exception ex) {
Log.e("InjectActivity", "Error during injectActivity", ex);
}
}
/**
* 根据是否在Manifest中注册了Activity,来判断是否是插件Activity;
*
* @param activity
* @return
*/
private boolean isUnregisteredManifestActivity(Activity activity, Intent targetIntent) {
try {
ComponentName component = targetIntent.getComponent();
// 获取包管理器
PackageManager packageManager = activity.getPackageManager();
// 获取当前应用的包信息
PackageInfo packageInfo = packageManager.getPackageInfo(activity.getPackageName(), PackageManager.GET_ACTIVITIES);
// 获取注册的所有 Activity
ActivityInfo[] activities = packageInfo.activities;
if (activities != null) {
for (ActivityInfo activityInfo : activities) {
String activityName = activityInfo.name;
String targetIntentName = component.getClassName();
if (activityName.equals(targetIntentName)) {
Log.d(TAG, "已在宿主项目中注册,不是需要启动的插件Activity");
return false;
}
}
} else {
Log.i(TAG, "No activities registered in the manifest.");
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return true;
}
final Map<String, ActivityInfo> activityMap = new HashMap<>(); // <cl
/**
* 为了支持AppCompat.Theme主题,必须要执行此函数,否则会一直报错
* java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
* 是从ActivityInfo中取获取theme并设置到Activity中,而不能直接从,activity变量中的mTheme变量是null的值或者说是Activity的样式而不是我们需要的AppCompatActivity中的mTheme样式,所以需要从ActivityInfo中去获取,ActivityInfo右是根据ApplicationInfo来的
* 参考了xPlugin 框架
*
* @param activity
*/
private void setActivityResIdTheme(Activity activity) {
PackageManager pm = mPluginContext.getApplicationContext().getPackageManager();
PackageInfo pkgInfo = pm.getPackageArchiveInfo(mPluginContext.getPluginPath(),
PackageManager.GET_META_DATA | PackageManager.GET_ACTIVITIES |
PackageManager.GET_SERVICES | PackageManager.GET_RECEIVERS |
PackageManager.GET_PROVIDERS);
if (pkgInfo != null) {
if (pkgInfo.activities != null) {
ApplicationInfo appInfo = pkgInfo.applicationInfo;
int appTheme = appInfo.theme;
int appLabelRes = appInfo.labelRes;
int appIcon = appInfo.icon;
for (ActivityInfo info : pkgInfo.activities) {
info.theme = info.theme != 0 ? info.theme : appTheme;
info.labelRes = info.labelRes != 0 ? info.labelRes : appLabelRes;
info.icon = info.icon != 0 ? info.icon : appIcon;
String className = info.name.startsWith(".") ?
info.packageName + info.name : info.name;
activityMap.put(className, info);
}
ActivityInfo activityInfo = activityMap.get(activity.getClass().getName());
if (activityInfo != null) {
ActivityInfo activityInfoNew = new ActivityInfo(activityInfo);
if (activityInfoNew.theme != 0) {
// 虽然ContextThemeWrapper已通过反射更改了Theme,但是Activity又重写了这个类的setTheme(int resId)函数,所以还需要在这个地方设置theme,注意这个theme是int类型的,所以可以从插件apk中的ActivityInfo来取到这个int值,这个地方需要多次尝试
// 设置方式1、调用公开访问的API ,推荐使用这种方式
activity.setTheme(activityInfoNew.theme);
// 设置方式2、也可以使用反射来间接调用
/**
* Reflect.on(activity).set("mThemeResource", activityInfoNew.theme);
Reflect.on(activity).call("initializeTheme");
Reflect.on(activity.getWindow()).call("setTheme", activityInfoNew.theme);
*/
}
}
}
}
}
}
4.2、创建插件上下文类:PluginContext.java
mport android.app.Application;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.view.ContextThemeWrapper;
import java.lang.reflect.Method;
/**
* @author james
* @date 2024-08-20
* @brief description
* 独立构造一个新的插件的上下文
* 其实就是自己扩展一个ContextThemeWrapper,来new 出一个插件的上下文
*/
public class PluginContext extends ContextThemeWrapper {
private Context context;
private Application application;
private Resources resources;
private ClassLoader classLoader;
private AssetManager assetManager;
private Resources.Theme theme;
public String getPluginPath() {
return pluginPath;
}
private String pluginPath;
public PluginContext(Context context, Application application, ClassLoader classLoader, String pluginPath) {
super(context, 0);
this.context = context;
this.application = application;
this.classLoader = classLoader;
this.pluginPath = pluginPath;
generateResources();
}
private void generateResources() {
try {
assetManager = AssetManager.class.newInstance();
Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
method.setAccessible(true);
method.invoke(assetManager, pluginPath);
resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
public Context getApplicationContext() {
return application;
}
public PackageManager getPackageManager() {
return context.getPackageManager();
}
@Override
public AssetManager getAssets() {
return getResources().getAssets();
}
@Override
public Resources getResources() {
return resources;
}
@Override
public ClassLoader getClassLoader() {
return classLoader;
}
/**
* 必须要重新构建这个主题,不然会报一个错误
* Attempt to invoke interface method 'void androidx.appcompat.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
*
* @return
*/
@Override
public Resources.Theme getTheme() {
if (this.theme == null) {
Resources.Theme oldTheme = super.getTheme();
this.theme = this.getResources().newTheme();
this.theme.setTo(oldTheme);
}
return this.theme;
}
}
4.3、宿主中TestActivity调用
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
//初始化插件上下文
initPluginContext();
//绕过getActivityInfo检查,解除不能打开未安装应用的Activity的限制
hookPackageManager();
//Context.startActivity(intent)方式,注入InstrumentationProxy代理类
attachContext();
//注意Context与Activity的startActivity函数对应的Instrumentation是不一样的,Context.startActivity对应的是ActivityThread中的mInstrumentation,实现类是ContextImpl,而Activity.startActivity对应的是Activity中的mInstrumentation,所以都需要Hook
//Activity.startActivity(intent)方式,注入InstrumentationProxy代理类
attachActivity();
}
private String pluginPath;//插件APK存放地址,你也可以自定义
private File nativeLibDir;
private File dexOutPath;
private PluginContext pluginContext;//插件上下文,这是非常关键的一个类
public static String pluginActivityName = "com.xx.plugindemo1.PluginAActivity";//插件APK内的Activity类名
public static String pluginPackageName = "com.xx.plugindemo1";//插件APK内的包名
private void initPluginContext() {
String fileName = "plugin.apk";
File filesDir = getFilesDir();//路径是:/data/data/< package name >/files/…,插件APK需要拷贝到这个路径下面,这个路径你也可以放在其他目录,只是要注意权限问题
pluginPath = new File(filesDir, fileName).getAbsolutePath();
nativeLibDir = new File(filesDir, "pluginlib");
dexOutPath = new File(filesDir, "dexout");
if (!dexOutPath.exists()) {
dexOutPath.mkdirs();
}
DexClassLoader pluginClassLoader = new DexClassLoader(pluginPath, dexOutPath.getAbsolutePath(), nativeLibDir.getAbsolutePath(), this.getClassLoader());
pluginContext = new PluginContext(this, getApplication(), pluginClassLoader, pluginPath);
}
private void hookPackageManager() {
// 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装
// 如果没有安装, 直接抛出异常, 这里需要临时Hook掉 PMS, 绕过这个检查.
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 获取ActivityThread里面原始的 sPackageManager
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);
// 准备好代理对象, 用来替换原始的对象
Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
new Class<?>[]{iPackageManagerInterface},
new IPackageManagerHookHandler(sPackageManager));
// 1. 替换掉ActivityThread里面的 sPackageManager 字段
sPackageManagerField.set(currentActivityThread, proxy);
// set ApplicationPackageManager#mPM
// 2. 替换 ApplicationPackageManager里面的 mPM对象
//这段代码非常关键,解决了代理类中AppCompatDelegateImpl.java:2669) final ActivityInfo info = pm.getActivityInfo( new ComponentName(mContext, mHost.getClass()), flags); 不能被调用的问题
PackageManager pm = this.getPackageManager();
Field mPmField = pm.getClass().getDeclaredField("mPM");
mPmField.setAccessible(true);
mPmField.set(pm, proxy);
} catch (Exception e) {
e.printStackTrace();
}
}
static class IPackageManagerHookHandler implements InvocationHandler {
Object realPackageManager;
public IPackageManagerHookHandler(Object realPackageManager) {
this.realPackageManager = realPackageManager;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("getActivityInfo")) {
Log.d(InstrumentationProxy.TAG, "getActivityInfo============>" + method.getName() + ", args :" + Arrays.deepToString(args));
ComponentName component = (ComponentName) args[0];
Intent intent = new Intent();
intent.setPackage(component.getPackageName());
intent.setComponent(component);
if (((ComponentName) args[0]).getClassName().equals(pluginActivityName)) {
//intent的信息(主要就是指component信息包名、类名改掉就能跳转到你指定的目标)已被替换了,开始还原为系统已注册的Activity
intent.setClassName(component.getPackageName(), ProxyActivity.class.getName());
//更改原始的com.xx.escape.plan/com.xx.plugindemo1.PluginAActivity (未注册)变成已注册的(com.xx.escape.plan/com.xx.escape.plan.ProxyActivity)
args[0] = intent.getComponent();
ActivityInfo info = (ActivityInfo) method.invoke(realPackageManager, args);
Log.d(InstrumentationProxy.TAG, "info1========> " + info);
return info;
} else {
ActivityInfo info = (ActivityInfo) method.invoke(realPackageManager, args);
Log.d(InstrumentationProxy.TAG, "info2========> " + info);
return info;
}
}
return method.invoke(realPackageManager, args);
}
}
private void attachContext() {
try {
// 获取ActivityThread类的全名
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
//根据ActivityThread类的全名访问其静态方法currentActivityThread(),主线程只有一个
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
//调用currentActivityThread()方法返回当前的ActivityThread
Object realActivityThread = currentActivityThreadMethod.invoke(null);
//拿到原始的mInstrumentation
Field realInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
realInstrumentationField.setAccessible(true);
//注意是根据currentActivityThread来获取的属性值
Instrumentation realInstrumentation = (Instrumentation) realInstrumentationField.get(realActivityThread);
//创建重载函数的子类对象
InstrumentationProxy myInstrumentation = new InstrumentationProxy(realInstrumentation);
//开始为原来的字段,通过原来的对象赋予新的字段值
realInstrumentationField.set(realActivityThread, myInstrumentation);
//TODO 注入初始化
myInstrumentation.inject(pluginContext);
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* 只调用这个方法有缺陷,会导致newActivity不会回调,但是可以结合上面 attachContext() 一起调用,就能回调newActivity,只是创建了两个InstrumentationProxy 代理对象
*/
private void attachActivity() {
try {
// 获取Activity类的全名
Class<?> activityClass = Class.forName("android.app.Activity");
//根据Activity类的全名访问其静态方法获取其私有属性mInstrumentation
Field instrumentationField = activityClass.getDeclaredField("mInstrumentation");
instrumentationField.setAccessible(true);
//注意是根据this(当前Activity)来获取的属性值
Instrumentation realInstrumentation = (Instrumentation) instrumentationField.get(this);
//创建重载函数的子类对象
InstrumentationProxy myInstrumentation = new InstrumentationProxy(realInstrumentation);
//通过this(当前Activity)为原来的字段赋予新的字段值
instrumentationField.set(this, myInstrumentation);
//TODO 注入初始化
myInstrumentation.inject(pluginContext);
} catch (Exception ex) {
ex.printStackTrace();
}
}
//注册一个xml的点击事件,打开插件的Activity,支持Activity及AppCompatActivity
public void viewOpenPluginActivity(View view) {
// 根据apk路径加载apk代码到DexClassLoader中
try {
ClassLoader classLoader = pluginContext.getClassLoader();
Class clazz = classLoader.loadClass(pluginActivityName);
Intent intent = new Intent(TestBActivity.this, clazz);
intent.setPackage(pluginPackageName);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getBaseContext().startActivity(intent);//Context.startActivity这种方式支持
//startActivity(intent); //Activity.startActivity这种方式也支持
} catch (Exception e) {
Log.d(InstrumentationProxy.TAG, "TestBActivity==========>" + e.getMessage());
}
}
}
4.4、宿主与插件的主题
建议保持一致,可以避免很多奇怪的问题,最关键的是要支持Theme.AppCompat ,这个主题直接关系到extends AppCompatActivity会不会报错。
插件APK的主题设置< application android:theme=“@style/AppFullScreenTheme” >如下:
<resources>
<!-- Base application theme. -->
<style name="Theme.PluginDemo1" parent="Theme.AppCompat.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryDark">@color/purple_700</item>
<item name="colorAccent">@color/white</item>
</style>
<style name="AppFullScreenTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowNoTitle">false</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
</resources>
宿主的主题设置< application android:theme=“@style/Theme.EscapePlanDemo” >如下:
宿主可以随便一点,只是插件主题设置需要有要求。
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.EscapePlanDemo" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryDark">@color/purple_700</item>
<item name="colorAccent">@color/white</item>
</style>
</resources>
4.5、插件依赖库保持简单一点
在插件APK中,每新增一个库都有可能导致不兼容,所以加库需谨慎。
插件的APP的build.gradle 配置如下如下:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.xx.plugindemo1'
compileSdk 32
defaultConfig {
applicationId "com.xx.plugindemo1"
minSdk 19
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
4.6、反射工具类Reflect.java
实现参考如下:
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 一个拥有流畅特性(Fluent-API)的反射工具类, 使用起来就像直接调用一样流畅易懂.
*
* @author Lody
*/
public class Reflect {
private final Object object;
private final boolean isClass;
private boolean isSuper;
private Reflect(Class<?> type) {
this.object = type;
this.isClass = true;
}
private Reflect(Object object) {
this.object = object;
this.isClass = false;
}
/**
* 根据指定的类名构建反射工具类
*
* @param name 类的全名
* @return 反射工具类
* @throws ReflectException 如果反射出现意外
* @see #on(Class)
*/
public static Reflect on(String name) throws ReflectException {
return on(forName(name));
}
/**
* 从指定的类加载起寻找类,并构建反射工具类
*
* @param name 类的全名
* @param classLoader 需要构建工具类的类的类加载器 loaded.
* @return 反射工具类
* @throws ReflectException 如果反射出现意外
* @see #on(Class)
*/
public static Reflect on(String name, ClassLoader classLoader) throws ReflectException {
return on(forName(name, classLoader));
}
/**
* 根据指定的类构建反射工具类
* <p>
* 当你需要访问静态字段的时候本方法适合你, 你还可以通过调用 {@link #create(Object...)} 创建一个对象.
*
* @param clazz 需要构建反射工具类的类
* @return 反射工具类
*/
public static Reflect on(Class<?> clazz) {
return new Reflect(clazz);
}
// ---------------------------------------------------------------------
// 构造器
// ---------------------------------------------------------------------
/**
* Wrap an object.
* <p>
* Use this when you want to access instance fields and methods on any
* {@link Object}
*
* @param object The object to be wrapped
* @return A wrapped object, to be used for further reflection.
*/
public static Reflect on(Object object) {
return new Reflect(object);
}
/**
* 让一个{@link AccessibleObject}可访问.
*
* @param accessible
* @param <T>
* @return
*/
public static <T extends AccessibleObject> T accessible(T accessible) {
if (accessible == null) {
return null;
}
if (accessible instanceof Member) {
Member member = (Member) accessible;
if (Modifier.isPublic(member.getModifiers())
&& Modifier.isPublic(member.getDeclaringClass().getModifiers())) {
return accessible;
}
}
if (!accessible.isAccessible()) {
accessible.setAccessible(true);
}
return accessible;
}
// ---------------------------------------------------------------------
// Fluent Reflection API
// ---------------------------------------------------------------------
/**
* 将给定字符串的开头改为小写.
*
* @param string
* @return
*/
private static String property(String string) {
int length = string.length();
if (length == 0) {
return "";
} else if (length == 1) {
return string.toLowerCase();
} else {
return string.substring(0, 1).toLowerCase() + string.substring(1);
}
}
private static Reflect on(Constructor<?> constructor, Object... args) throws ReflectException {
try {
return on(accessible(constructor).newInstance(args));
} catch (Exception e) {
throw new ReflectException(e);
}
}
private static Reflect on(Method method, Object object, Object... args) throws ReflectException {
try {
accessible(method);
if (method.getReturnType() == void.class) {
method.invoke(object, args);
return on(object);
} else {
return on(method.invoke(object, args));
}
} catch (Exception e) {
throw new ReflectException(e);
}
}
/**
* 取得内部维护的对象.
*/
private static Object unwrap(Object object) {
if (object instanceof Reflect) {
return ((Reflect) object).get();
}
return object;
}
/**
* 将Object数组转换为其类型的数组. 如果对象中包含null,我们用NULL.class代替.
*
* @see Object#getClass()
*/
private static Class<?>[] types(Object... values) {
if (values == null) {
return new Class[0];
}
Class<?>[] result = new Class[values.length];
for (int i = 0; i < values.length; i++) {
Object value = values[i];
result[i] = value == null ? NULL.class : value.getClass();
}
return result;
}
/**
* 取得一个类,此操作会初始化类的static区域.
*
* @see Class#forName(String)
*/
private static Class<?> forName(String name) throws ReflectException {
try {
return Class.forName(name);
} catch (Exception e) {
throw new ReflectException(e);
}
}
private static Class<?> forName(String name, ClassLoader classLoader) throws ReflectException {
try {
return Class.forName(name, true, classLoader);
} catch (Exception e) {
throw new ReflectException(e);
}
}
/**
* 如果给定的Class是原始类型,那么将其包装为对象类型, 否则返回本身.
*/
public static Class<?> wrapper(Class<?> type) {
if (type == null) {
return null;
} else if (type.isPrimitive()) {
if (boolean.class == type) {
return Boolean.class;
} else if (int.class == type) {
return Integer.class;
} else if (long.class == type) {
return Long.class;
} else if (short.class == type) {
return Short.class;
} else if (byte.class == type) {
return Byte.class;
} else if (double.class == type) {
return Double.class;
} else if (float.class == type) {
return Float.class;
} else if (char.class == type) {
return Character.class;
} else if (void.class == type) {
return Void.class;
}
}
return type;
}
/**
* 取得内部维护的实际对象
*
* @param <T>
* @return
*/
@SuppressWarnings("unchecked")
public <T> T get() {
return (T) object;
}
/**
* 设置指定字段为指定值
*
* @param name
* @param value
* @return
* @throws ReflectException
*/
public Reflect set(String name, Object value) throws ReflectException {
try {
Field field = field0(name);
field.setAccessible(true);
field.set(object, unwrap(value));
return this;
} catch (Exception e) {
throw new ReflectException(e);
}
}
/**
* @param name name
* @param <T> type
* @return object
* @throws ReflectException
*/
public <T> T get(String name) throws ReflectException {
return field(name).get();
}
/**
* 取得指定名称的字段
*
* @param name name
* @return reflect
* @throws ReflectException
*/
public Reflect field(String name) throws ReflectException {
try {
Field field = field0(name);
return on(field.get(object));
} catch (Exception e) {
throw new ReflectException(object.getClass().getName(), e);
}
}
private Field field0(String name) throws ReflectException {
Class<?> type = type();
// 先尝试取得公有字段
try {
return type.getField(name);
}
// 此时尝试非公有字段
catch (NoSuchFieldException e) {
do {
try {
return accessible(type.getDeclaredField(name));
} catch (NoSuchFieldException ignore) {
}
type = type.getSuperclass();
} while (type != null);
throw new ReflectException(e);
}
}
/**
* 取得一个Map,map中的key为字段名,value为字段对应的反射工具类
*
* @return Map
*/
public Map<String, Reflect> fields() {
Map<String, Reflect> result = new LinkedHashMap<String, Reflect>();
Class<?> type = type();
do {
for (Field field : type.getDeclaredFields()) {
if (!isClass ^ Modifier.isStatic(field.getModifiers())) {
String name = field.getName();
if (!result.containsKey(name))
result.put(name, field(name));
}
}
type = type.getSuperclass();
} while (type != null);
return result;
}
/**
* 调用指定的无参数方法
*
* @param name
* @return
* @throws ReflectException
*/
public Reflect call(String name) throws ReflectException {
return call(name, new Object[0]);
}
/**
* 调用方法根据传入的参数
*
* @param name
* @param args
* @return
* @throws ReflectException
*/
public Reflect call(String name, Object... args) throws ReflectException {
Class<?>[] types = types(args);
try {
Method method = exactMethod(name, types);
return on(method, object, args);
} catch (NoSuchMethodException e) {
try {
Method method = similarMethod(name, types);
return on(method, object, args);
} catch (NoSuchMethodException e1) {
throw new ReflectException(e1);
}
}
}
public Method exactMethod(String name, Class<?>[] types) throws NoSuchMethodException {
Class<?> type = type();
try {
return type.getMethod(name, types);
} catch (NoSuchMethodException e) {
do {
try {
return type.getDeclaredMethod(name, types);
} catch (NoSuchMethodException ignore) {
}
type = type.getSuperclass();
} while (type != null);
throw new NoSuchMethodException();
}
}
/**
* 根据参数和名称匹配方法,如果找不到方法,
*/
private Method similarMethod(String name, Class<?>[] types) throws NoSuchMethodException {
Class<?> type = type();
for (Method method : type.getMethods()) {
if (isSimilarSignature(method, name, types)) {
return method;
}
}
do {
for (Method method : type.getDeclaredMethods()) {
if (isSimilarSignature(method, name, types)) {
return method;
}
}
type = type.getSuperclass();
} while (type != null);
throw new NoSuchMethodException("No similar method " + name + " with params " + Arrays.toString(types)
+ " could be found on type " + type() + ".");
}
private boolean isSimilarSignature(Method possiblyMatchingMethod, String desiredMethodName,
Class<?>[] desiredParamTypes) {
return possiblyMatchingMethod.getName().equals(desiredMethodName)
&& match(possiblyMatchingMethod.getParameterTypes(), desiredParamTypes);
}
/**
* 创建一个实例通过默认构造器
*
* @return Reflect
* @throws ReflectException
*/
public Reflect create() throws ReflectException {
return create(new Object[0]);
}
/**
* 创建一个实例根据传入的参数
*
* @param args 参数
* @return Reflect
* @throws ReflectException
*/
public Reflect create(Object... args) throws ReflectException {
Class<?>[] types = types(args);
try {
Constructor<?> constructor = type().getDeclaredConstructor(types);
return on(constructor, args);
} catch (NoSuchMethodException e) {
for (Constructor<?> constructor : type().getDeclaredConstructors()) {
if (match(constructor.getParameterTypes(), types)) {
return on(constructor, args);
}
}
throw new ReflectException(e);
}
}
/**
* 创建一个动态代理根据传入的类型. 如果我们正在维护的是一个Map,那么当调用出现异常时我们将从Map中取值.
*
* @param proxyType 需要动态代理的类型
* @return 动态代理生成的对象
*/
@SuppressWarnings("unchecked")
public <P> P as(Class<P> proxyType) {
final boolean isMap = (object instanceof Map);
final InvocationHandler handler = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
try {
return on(object).call(name, args).get();
} catch (ReflectException e) {
if (isMap) {
Map<String, Object> map = (Map<String, Object>) object;
int length = (args == null ? 0 : args.length);
if (length == 0 && name.startsWith("get")) {
return map.get(property(name.substring(3)));
} else if (length == 0 && name.startsWith("is")) {
return map.get(property(name.substring(2)));
} else if (length == 1 && name.startsWith("set")) {
map.put(property(name.substring(3)), args[0]);
return null;
}
}
throw e;
}
}
};
return (P) Proxy.newProxyInstance(proxyType.getClassLoader(), new Class[]{proxyType}, handler);
}
/**
* 检查两个数组的类型是否匹配,如果数组中包含原始类型,将它们转换为对应的包装类型.
*/
private boolean match(Class<?>[] declaredTypes, Class<?>[] actualTypes) {
if (declaredTypes.length == actualTypes.length) {
for (int i = 0; i < actualTypes.length; i++) {
if (actualTypes[i] == NULL.class)
continue;
if (wrapper(declaredTypes[i]).isAssignableFrom(wrapper(actualTypes[i])))
continue;
return false;
}
return true;
} else {
return false;
}
}
/**
* {@inheritDoc}
*/
public int hashCode() {
return object.hashCode();
}
/**
* {@inheritDoc}
*/
public boolean equals(Object obj) {
return obj instanceof Reflect && object.equals(((Reflect) obj).get());
}
/**
* {@inheritDoc}
*/
public String toString() {
return object.toString();
}
/**
* 取得我们正在反射的对象的类型.
*
* @see Object#getClass()
*/
public Class<?> type() {
if (isClass) {
return (Class<?>) object;
} else {
if (isSuper) {
return object.getClass().getSuperclass();
}
return object.getClass();
}
}
public Reflect superClass() {
isSuper = true;
return this;
}
public static String getMethodDetails(Method method) {
StringBuilder sb = new StringBuilder(40);
sb.append(Modifier.toString(method.getModifiers()))
.append(" ")
.append(method.getReturnType().getName())
.append(" ")
.append(method.getName())
.append("(");
Class<?>[] parameters = method.getParameterTypes();
for (Class<?> parameter : parameters) {
sb.append(parameter.getName()).append(", ");
}
if (parameters.length > 0) {
sb.delete(sb.length() - 2, sb.length());
}
sb.append(")");
return sb.toString();
}
/**
* 用来表示null的类.
*
* @author Lody
*/
private static class NULL {
}
/**
* 智能调用 但是只调用类本身声明方法 按照优先级 匹配
* <p>
* 1.完全匹配
* 2.形参 Object...
* 3.名字相同 无参数
*
* @param name
* @param args
* @return
* @throws ReflectException
*/
public Reflect callBest(String name, Object... args) throws ReflectException {
Class<?>[] types = types(args);
Class<?> type = type();
Method bestMethod = null;
int level = 0;
for (Method method : type.getDeclaredMethods()) {
if (isSimilarSignature(method, name, types)) {
bestMethod = method;
level = 2;
break;
}
if (matchObjectMethod(method, name, types)) {
bestMethod = method;
level = 1;
continue;
}
if (method.getName().equals(name) && method.getParameterTypes().length == 0 && level == 0) {
bestMethod = method;
}
}
if (bestMethod != null) {
if (level == 0) {
args = new Object[0];
}
if (level == 1) {
Object[] args2 = {args};
args = args2;
}
return on(bestMethod, object, args);
} else {
throw new ReflectException("no method found for " + name, new NoSuchMethodException("No best method " + name + " with params " + Arrays.toString(types)
+ " could be found on type " + type() + "."));
}
}
private boolean matchObjectMethod(Method possiblyMatchingMethod, String desiredMethodName,
Class<?>[] desiredParamTypes) {
return possiblyMatchingMethod.getName().equals(desiredMethodName)
&& matchObject(possiblyMatchingMethod.getParameterTypes());
}
private boolean matchObject(Class<?>[] parameterTypes) {
Class<Object[]> c = Object[].class;
return parameterTypes.length > 0 && parameterTypes[0].isAssignableFrom(c);
}
}
4.7、反射异常类ReflectException.java
实现参考如下:
/**
* @author Lody
*/
public class ReflectException extends RuntimeException {
public ReflectException(String message, Throwable cause) {
super(message, cause);
}
public ReflectException(Throwable cause) {
super(cause);
}
}
5、实践过程中的各种报错及原因
5.1、Failed to resolve attribute at index 1: TypedValue{t=0x2/d=0x101005a a=-1}
Caused by: java.lang.UnsupportedOperationException: Failed to resolve attribute at index 1: TypedValue{t=0x2/d=0x101005a a=-1}
这个错误可能的原因就是宿主与插件库资源ID冲突的,必须要去找出来。
5.2、‘android.view.Window$Callback android.view.Window.getCallback()’ on a null object reference
这个错误就是插件库不支持AppCompatActivity,只能使用Activity来开发,因为这块需要针对Compact的Theme做单独适配。
5.3、Exception while getting ActivityInfo
android.content.pm.PackageManagerNameNotFoundException: ComponentInfo{com.xx.escape.plan/com.xxx.LoadActivity}
at android.app.ApplicationPackageManager.getActivityInfo(ApplicationPackageManager.java:435)
at androidx.appcompat.app.AppCompatDelegateImpl.isActivityManifestHandlingUiMode(AppCompatDelegateImpl.java:2669)
这个错误是因为ApplicationPackageManager.getActivityInfo这个函数为空,必须要通过Hook来跳过这段代码的检测,API28要替换掉ApplicationPackageManager#mPM属性,才能进入到getActivityInfo这个地方,否则hook代码不会执行。
5.4、You need to use a Theme.AppCompat theme (or descendant) with this activity.
原因1:这个错误是因为不支持AppCompat.Theme属性造成的,需要调用Activity的setTheme(int resid)的这个函数后,才能让样式生效,其中这个resid的获取方式很神奇,需要从ActivityInfo中来获取。
原因2:插件库引入依赖包必须要注意是否存在样式重复引入的问题,比如只在插件中引入了 androidx.legacy:legacy-support-v4:1.0.0 这个库,宿主没引入,运行插件就会一直报错。
6、参考资料
I、Android插件化实现动态加载Activity笔记
II、xPlugin源码
这是一篇精炼的原创文章,耗费了一定时间,所以阅读前需要熟悉一些插件化的知识,这样才会更容易理解,很多注释在代码中写得非常详细了,很容易看懂。
注意:本文的这种Hook方式,可能无法通过GooglePlay上架审核,要解决此问题可以使用腾讯的Shadow开源项目,但其采用的实现方式不同。
原创不易,求个关注。
微信公众号:一粒尘埃的漫旅
里面有很多想对大家说的话,就像和朋友聊聊天。
写代码,做设计,聊生活,聊工作,聊职场。
我见到的世界是什么样子的?
搜索关注我吧。
公众号与博客的内容不同。