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] --> [结束]
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开源项目,但其采用的实现方式不同。
原创不易,求个关注。
微信公众号:一粒尘埃的漫旅
里面有很多想对大家说的话,就像和朋友聊聊天。
写代码,做设计,聊生活,聊工作,聊职场。
我见到的世界是什么样子的?
搜索关注我吧。
公众号与博客的内容不同。