当前位置: 首页 > news >正文

Android插件化技术之加载未安装APK

目录

  • 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] --> [结束]

Activity Instrumentation ActivityManagerService ActivityStarter ActivityStackSupervisor ActivityStack ClientLifecycleManager ClientTransaction IApplicationThread ClientTransactionHandler TransactionExecutor ClientTransactionItem LaunchActivityItem ActivityThread startActivityForResult execStartActivity startActivityAsUser startActivityUnchecked resumeFocusedStackTopActivityLocked resumeTopActivityInnerLocked scheduleTransaction schedule sendMessage execute execute execute handleLaunchActivity handleLaunchActivity performLaunchActivity newActivity、callActivityOnCreate、performCreate onCreate Activity Instrumentation ActivityManagerService ActivityStarter ActivityStackSupervisor ActivityStack ClientLifecycleManager ClientTransaction IApplicationThread ClientTransactionHandler TransactionExecutor ClientTransactionItem LaunchActivityItem ActivityThread

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!");}}@Overridepublic Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {if (intent != null) {Intent targetIntent = intent.getParcelableExtra(KEY_TARGET_INTENT);//通过插件的ClassLoader来载入一个插件的Activityif (targetIntent != null && targetIntent.getComponent() != null) {ComponentName componentName = targetIntent.getComponent();return mInstrumentation.newActivity(mPluginContext.getClassLoader(), componentName.getClassName(), targetIntent);}}return mInstrumentation.newActivity(cl, className, intent);}@Overridepublic void callActivityOnCreate(Activity activity, Bundle icicle) {injectActivity(activity);mInstrumentation.callActivityOnCreate(activity, icicle);}@Overridepublic 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 空指针的问题 2131689738if (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);// 获取注册的所有 ActivityActivityInfo[] 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();}@Overridepublic AssetManager getAssets() {return getResources().getAssets();}@Overridepublic Resources getResources() {return resources;}@Overridepublic 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*/@Overridepublic 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 {@Overrideprotected 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里面原始的 sPackageManagerField 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;}@Overridepublic 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信息包名、类名改掉就能跳转到你指定的目标)已被替换了,开始还原为系统已注册的Activityintent.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()方法返回当前的ActivityThreadObject realActivityThread = currentActivityThreadMethod.invoke(null);//拿到原始的mInstrumentationField 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类的全名访问其静态方法获取其私有属性mInstrumentationField 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及AppCompatActivitypublic 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 32defaultConfig {applicationId "com.xx.plugindemo1"minSdk 19targetSdk 32versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility 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开源项目,但其采用的实现方式不同。






原创不易,求个关注。

在这里插入图片描述

微信公众号:一粒尘埃的漫旅
里面有很多想对大家说的话,就像和朋友聊聊天。
写代码,做设计,聊生活,聊工作,聊职场。
我见到的世界是什么样子的?
搜索关注我吧。

公众号与博客的内容不同。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 数据链路层(Mac帧,报头字段,局域网通信原理),MTU,MSS,ip报文的分包与组装(ip报头字段介绍,组装过程,判断是否被分片/收到全部分片)
  • 【LeetCode面试150】——54螺旋矩阵
  • Codeforces 1304C - Air Conditioner(1500)
  • MYSQL 优化
  • 高级前端工程师React面试题
  • pikachu靶场通关攻略(XSS)(1~10关)
  • 大模型企业应用落地系列四》基于大模型的对话式推荐系统》大模型底座层
  • 探索Python性能监控的瑞士军刀:psutil的神秘面纱
  • 首发!《物流运输行业电子签最佳实践案例集》重磅发布
  • KRTS网络模块:TCP服务端、客户端实例
  • 基于SpringBoot+Vue+MySQL的小区物业管理系统
  • 什么是BI?BI系统的功能有哪些?哪些人需要BI工具支持?
  • JAVA中的线程池说明一
  • C++ | Leetcode C++题解之第377题组合总和IV
  • 上书房信息咨询:生活投诉满意度调研
  • 【跃迁之路】【585天】程序员高效学习方法论探索系列(实验阶段342-2018.09.13)...
  • Angular 响应式表单 基础例子
  • Apache Pulsar 2.1 重磅发布
  • co模块的前端实现
  • tensorflow学习笔记3——MNIST应用篇
  • Terraform入门 - 3. 变更基础设施
  • VUE es6技巧写法(持续更新中~~~)
  • vue:响应原理
  • 从零开始的webpack生活-0x009:FilesLoader装载文件
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • 使用权重正则化较少模型过拟合
  • 双管齐下,VMware的容器新战略
  • 微信如何实现自动跳转到用其他浏览器打开指定页面下载APP
  • 一些关于Rust在2019年的思考
  • - 语言经验 - 《c++的高性能内存管理库tcmalloc和jemalloc》
  • Oracle Portal 11g Diagnostics using Remote Diagnostic Agent (RDA) [ID 1059805.
  • MPAndroidChart 教程:Y轴 YAxis
  • Spark2.4.0源码分析之WorldCount 默认shuffling并行度为200(九) ...
  • 格斗健身潮牌24KiCK获近千万Pre-A轮融资,用户留存高达9个月 ...
  • ​一些不规范的GTID使用场景
  • #微信小程序:微信小程序常见的配置传值
  • #我与Java虚拟机的故事#连载16:打开Java世界大门的钥匙
  • (21)起落架/可伸缩相机支架
  • (3)选择元素——(17)练习(Exercises)
  • (55)MOS管专题--->(10)MOS管的封装
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (差分)胡桃爱原石
  • (多级缓存)多级缓存
  • (二)pulsar安装在独立的docker中,python测试
  • (二)十分简易快速 自己训练样本 opencv级联lbp分类器 车牌识别
  • (附源码)ssm考生评分系统 毕业设计 071114
  • (附源码)ssm跨平台教学系统 毕业设计 280843
  • (附源码)计算机毕业设计SSM智慧停车系统
  • (介绍与使用)物联网NodeMCUESP8266(ESP-12F)连接新版onenet mqtt协议实现上传数据(温湿度)和下发指令(控制LED灯)
  • (最优化理论与方法)第二章最优化所需基础知识-第三节:重要凸集举例
  • .NET C# 使用GDAL读取FileGDB要素类
  • .NET CLR基本术语
  • .NET Core 成都线下面基会拉开序幕
  • .Net CoreRabbitMQ消息存储可靠机制
  • .Net Core缓存组件(MemoryCache)源码解析