写点什么

Android 插件化-Activity 篇,安卓开发面试问题

用户头像
Android架构
关注
发布于: 2 小时前

//当然在实际到开发中这里到情况会复杂的多,比如与服务器插件版本进行对比判断是否需要重新下载等 File file = new File(filePath);if (file.exists()) {Log.i(TAG, "had copy apk before,so no need copy again");} else {Log.i(TAG, "althogh save apk file path success,but file not exists");extractAssets(context, filePath, pluginName);}}}


上述方法就是判断是否需要复制插件 apk 到对应的目录下,接下来就是 copy 部分了。因为代码量不到逻辑也很简单,就直接看代码了。


public static boolean extractAssets(Context context, String filePath, String pluginName) {AssetManager assetManager = context.getAssets();


FileOutputStream fileOutputStream = null;InputStream inputStream = null;try {


Log.i(TAG, "save apk file path is " + filePath);


fileOutputStream = new FileOutputStream(filePath);//获取 assets 目录下的插件 apk 输入流 inputStream = assetManager.open(pluginName);


byte[] bytes = new byte[1024];int length = -1;//将 apk 文件复制到对应到文件目录下 while ((length = inputStream.read(bytes)) != -1) {fileOutputStream.write(bytes, 0, length);}fileOutputStream.flush();return true;} catch (Exception e) {Log.e(TAG, "copy file failed " + e.getMessage());} finally {try {if (null != inputStream) {inputStream.close();}if (null != fileOutputStream) {fileOutputStream.close();}} catch (Exception e) {Log.i(TAG, "extra


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


ctAssets: " + e.getMessage());}}return false;}

2.2 ActivityManager 的 Hook

这一块儿的 hook,简单来说就是通过动态代理的方式将 ActivityManager.getService 所获取到的 Binder 代理对象进行替换,那么我们就能够对诸如 start Activity 等方法进行入侵了。相关代码实现如下。[RefInvoke.java](


)类可到 github 上查看。


public static void hookAMN() {try {//通过反射获取到 ActivityManager 的 class 对象 Class<?> mActivityManagerCls = RefInvoke.getClass("android.app.ActivityManager");//首先通过反射获取到 ActivityManager 类中的单例对象 IActivityManagerSingleton//然后通过反射获取到对象对象 IActivityManagerSingleton 的值 Object mIActivityManagerSingletonObj = RefInvoke.getStaticFieldValue(RefInvoke.getField(mActivityManagerCls, "IActivityManagerSingleton"), mActivityManagerCls);//获取到 ActivityManager 与 AMS 的 Binder 通信接口 IActivityManager 的 class 对象,用于后续生成对应的代理对象 Class<?> mIActivityManagerCls = RefInvoke.getClass("android.app.IActivityManager");if (null != mIActivityManagerSingletonObj) {//因为上述的单例对象是 Singleton 实现类,所以通过反射首先获取到该类中的 mInstance 属性 Field mInstanceField = RefInvoke.getField("android.util.Singleton", "mInstance");//然后通过反射获取到上述单例对象中的 mInstance 属性对应的值 Object mInstance = RefInvoke.getFieldValue(mInstanceField, mIActivityManagerSingletonObj);//根据上述提供的接口以及当前的 ClassLoader 生成代理对象 Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{mIActivityManagerCls}, new AMSHookHelperInvocationHandler(mInstance));//将 ActivityManager.getService 获取到的单例对象替换成代理对象 RefInvoke.setFieldValue(mInstanceField, mIActivityManagerSingletonObj, proxy);} else {Log.i(TAG, "IActivityManagerSingleton not exists");}} catch (Exception e) {Log.i(TAG, "hook ATM failed " + e);}}


接下来就是实现 InvocationHandler 接口对 startActivity 方法进行入侵了,相关代码如下。


public class AMSHookHelperInvocationHandler implements InvocationHandler {private static final String TAG = Constants.TAG + "AMSHookHandler";


//被代理对象 private Object mBase;


public AMSHookHelperInvocationHandler(Object base) {mBase = base;}


@Overridepublic Object invoke(Object o, Method method, Object[] objects) throws Throwable {//劫持 startActivity 方法对上层应用真正要启动的 Activity 进行替换 if (TextUtils.equals(method.getName(), "startActivity")) {Log.i(TAG, "replace start up activity");


int index = -1;//获取上层应用传递过来的 Intent 对象 for (int i = 0; i < objects.length; i++) {if (objects[i] instanceof Intent) {index = i;break;}}if (-1 == index) {Log.i(TAG, "not found intent in params");return method.invoke(mBase, objects);}


//这就是上层应用所需要启动的插件 Activity 对应的 Intent 了 Intent realIntent = (Intent) objects[index];


//根据宿主中预先声明的 Activity 生成对应的 Intent 对象用于替换上层应用传递过来的插件 Activity 相关的 Intent 对象,以达到欺骗 AMS 的目的 Intent replacedStartUpIntent = realIntent.getParcelableExtra(Constants.REPLACED_START_UP_INTENT);if (null != replacedStartUpIntent) {Log.i(TAG, "origin intent is " + realIntent);realIntent.putExtra(Constants.REPLACED_START_UP_INTENT,"");replacedStartUpIntent.putExtra(Constants.START_UP_INTENT, realIntent);objects[index] = replacedStartUpIntent;Log.i(TAG, "replaced start up intent is " + replacedStartUpIntent);} else {Log.i(TAG, "replaced intent activity is null");}}//继续通过 Binder 的方式调用到 AMSreturn method.invoke(mBase, objects);}}


有了上述对 ActivityManager 的 hook 过程,接下来我们就可以直接在应用中启动插件 Activity 了,使用方式如下。


public void click(View view) {int id = view.getId();switch (id) {case R.id.plugin:Intent intent = new Intent();ComponentName componentName = new ComponentName(getPackageName(), "com.xx.xx.pluginActivity");intent.setComponent(componentName);intent.putExtra(Constants.REPLACED_START_UP_INTENT, createStartUpIntent());


startActivity(intent);break;}


}


//使用宿主中预先声明好的 Activity 构造 Intent 对象用于欺骗 AMS,后续统称为中转页面 private Intent createStartUpIntent() {Intent startUpIntent = new Intent();ComponentName componentName = new ComponentName(DePluginApplication.getContext(), StandardStubActivity.class.getName());startUpIntent.setComponent(componentName);startUpIntent.putExtra(Constants.DEX_PATH, DePluginSP.getInstance(DePluginApplication.getContext()).getString(Constants.COPY_FILE_PATH, ""));return startUpIntent;}


虽然我们能够在宿主中直接去启动插件中的 Activity 并且不会报出 ActivityNotFound 异常了,但是最后会发现启动的 Activity 并不是插件中的 Activity,而是我们的中转页面 StandardStubActivity。因此为了能够实现最终启动 Activity 是插件中的 Activity,我们还需要对 ActivityThread 中中的各个对象进行 hook。

2.3 ActivityThread 中的 hook

这一块儿所涉及的流程就稍微复杂一点了,因此代码量也稍微多一点。所以,建议大家有时间多可以去瞅瞅 Activity 启动流程源码分析相关的文章。

2.3.1 mH 的 hook

对于 ActivityThread 中 mH 属性,如果采用生成 Handler 对象直接通过反射的方式去替换,最终系统会无情的给你抛出一个 hook H failed java.lang.IllegalArgumentException;这是因为虽然 mH 属性对应的类继承了 Handler 对象,但是它的实际引用类型却是 H。所以此路肯定是行不通的。这个时候我们不妨去看看[Handler.java](


)类中最终 msg 分发的 dispatchMessage 函数源码实现,如下:


public void dispatchMessage(Message msg) {if (msg.callback != null) {handleCallback(msg);} else {//重点我们看这里,如果当前 Handler 中的 CallBack 实例对象为空才会走到 handleMessage 方法//因此我们可以为 mH 这个继承了 Handler 的实例对象构造一个实现了 CallBack 的接口实例对象,那说干就干 if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}}


所以我们首先构造一个实现了 Handler 中 CallBack 接口的实例对象,并通过反射的方式将这个实例对象赋值给 mH 对象。代码如下:


public static void hookH() {try {//通过反射获取到 ActivityThread 实例对象 Object sCurrentActivityThread = RefInvoke.getStaticFieldValue(RefInvoke.getField("android.app.ActivityThread", "sCurrentActivityThread"), RefInvoke.getClass("android.app.ActivityThread"));//获取到 ActivityThread 中的 mH 实例对象 Field mHField = RefInvoke.getField(sCurrentActivityThread.getClass(), "mH");Handler mH = (Handler) RefInvoke.getFieldValue(mHField, sCurrentActivityThread);//首先通过反射获取到 Handler 中的 mCallBack 属性//通过反射的方式将 mH 实例对象中的 mCallBack 属性赋值为 ActivityThreadHandler 的实例对象 RefInvoke.setFieldValue(RefInvoke.getField(Handler.class, "mCallback"), mH, new ActivityThreadHandler(mH));Log.i(TAG, "hook H complete");} catch (Exception e) {Log.i(TAG, "hook H failed " + e);}}


接着就是在 CallBack 中的 handleMessage 方法中对 Activity 的启动进行拦截了,然后将需要加载的 Activity 替换成插件中的 Activity,并将加载 Activity 的 ClassLoader 对象替换成以插件 apk 生成的 ClassLoader 对象,最后在 ActivityThread 中实际所加载的 Activity 就是插件中的 Activity 了。


public boolean handleMessage(@NonNull Message message) {int what = message.what;switch (what) {//这里为什么是 159 可以到 ActivityThread 中找到答案 case 159://首先获取从 AMS 中传递过来的 ClientTransaction 对象 Object object = message.obj;try {//这里的 CallBack 对象就是实现 Activity 生命周期的各个对象了 List<Object> mActivityCallbacks = RefInvoke.on(object, "getCallbacks").invoke();//获取开始执行 Activity onCreate 方法的实例对象,并将其中的 Intent 对象中的 ComponentName 对象替换成插件 Activity 对应的 ComponentName 对象 Class<?> mLaunchActivityItemCls = RefInvoke.getClass("android.app.servertransaction.LaunchActivityItem");for (Object obj : mActivityCallbacks) {if (mLaunchActivityItemCls.isInstance(obj)) {Intent intent = getIntent(mLaunchActivityItemCls, obj);if (null == intent) {break;}//只对需要实现插件化的 Activity 进行拦截,防止出现误拦截的情况 String path = intent.getStringExtra(Constants.DEX_PATH);if (TextUtils.isEmpty(path)) {Log.i(TAG, "dex path is empty,so do need replace class loader");break;}//替换成加载插件类的 ClassLoaderreplaceClassloader(mLaunchActivityItemCls, obj, path);//将实际需要加载的 Activity 替换成插件中的 Activityreplace(intent);break;}}} catch (Exception e) {Log.e(TAG, "getActivityToken failed " + e.getMessage());}break;default:


}mBase.handleMessage(message);return true;}

2.3.2 ClassLoader 的 hook

对于加载 Activity 的 ClassLoader 替换则稍显复杂了,因此在代码实现之前我们还是简单去看一下源码,了解一下如何将加载 Activity 的 ClassLoader 替换成加载插件中 Activity 的 ClassLoader。


源码解析


ActivityThread 中对 Activity 初始化是在 performLaunchActivity 中完成,部分源码如下:


private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {ActivityInfo aInfo = r.activityInfo;//生成 LoadedApk 对象 if (r.packageInfo == null) {r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,Context.CONTEXT_INCLUDE_CODE);}


ComponentName component = r.intent.getComponent();if (component == null) {component = r.intent.resolveActivity(mInitialApplication.getPackageManager());r.intent.setComponent(component);}


if (r.activityInfo.targetActivity != null) {component = new ComponentName(r.activityInfo.packageName,r.activityInfo.targetActivity);}//为当前启动的 Activity 生成对应的 Context 对象 ContextImpl appContext = createBaseContextForActivity(r);Activity activity = null;try {//获取 ClassLoader 对象以加载需要启动的 Activity 类 java.lang.ClassLoader cl = appContext.getClassLoader();activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);.........} catch (Exception e) {if (!mInstrumentation.onException(activity, e)) {throw new RuntimeException("Unable to instantiate activity " + component


  • ": " + e.toString(), e);}}............


return activity;}


在该方法中首先会根据要启动 Activity 中所携带的 ApplicationInfo 等对象生成 LoadedApk 对象,然后通过 LoadedApk 中携带的 ClassLoader 属性为当前需要启动的 Activity 生成对对应的 Context 对象,并通过该 ClassLoader 加载需要启动的 Activity 类。其中 Context 对象创建是在 ContextImpl 的 createActivityContext 方法中完成,部分源码如下:


static ContextImpl createActivityContext(ActivityThread mainThread,LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,Configuration overrideConfiguration) {if (packageInfo == null) throw new IllegalArgumentException("packageInfo");


String[] splitDirs = packageInfo.getSplitResDirs();//获取 LoadedApk 中的 ClassLoader 对象,并根据该 ClassLoader 创建对应的 Context 对象 ClassLoader classLoader = packageInfo.getClassLoader();........ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,activityToken, null, 0, classLoader);.......final ResourcesManager resourcesManager = ResourcesManager.getInstance(); context.setResources(resourcesManager.createBaseActivityResources(activityToken,packageInfo.getResDir(),splitDirs,packageInfo.getOverlayDirs(),packageInfo.getApplicationInfo().sharedLibraryFiles,displayId,overrideConfiguration,compatInfo,classLoader));context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,context.getResources());return context;}


既然最终加载 Activity 类的 ClassLoader 是从 LoadedApk 对象来的,所以我们只需要将上述 getPackageInfo 方法所得来的 LoadedApk 对象中的 ClassLoader 对象替换成通过插件 apk 生成的插件就行了。但是有个问题就是这里的 getPackageInfo 方法我们并不能 hook 住,因此并不能把握住该方法的调用时机,所以通过 getPackageInfo 方法生成的 LoadedApk 我们并不能动态的去替换掉;


因此这里我们所采用的是直接通过 getPackageInfo 方法创建一个属于我们自己的 LoadedApk 对象,至于我们为什么可以这样做,还是需要去看一下源码才知道。performLaunchActivity 中调用的 getPackageInfo 方法最终会调用到该方法的重载方法中,实现如下:


private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,ClassLoader baseLoader, boolean securityViolation, boolean includeCode,boolean registerPackage) {final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));synchronized (mResourcesManager) {WeakReference<LoadedApk> ref;if (differentUser) {ref = null;//最终会进入到这个 if 中//可以看到的是首先会去 mPackages 这个 ArrayMap 中查找该 Activity 所属的包名是否存在 LoadedApk 的缓存,如果存在缓存则直接使用} else if (includeCode) {ref = mPackages.get(aInfo.packageName);} else {ref = mResourcePackages.get(aInfo.packageName);}//如果不存在缓存则重新生成 LoadedApk 对象,并添加到 mPackagesLoadedApk packageInfo = ref != null ? ref.get() : null;if (packageInfo == null || (packageInfo.mResources != null&& !packageInfo.mResources.getAssets().isUpToDate())) {if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package ": "Loading resource-only package ") + aInfo.packageName


  • " (in " + (mBoundApplication != null? mBoundApplication.processName : null)

  • ")");packageInfo =new LoadedApk(this, aInfo, compatInfo, baseLoader,securityViolation, includeCode &&(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);


if (mSystemThread && "android".equals(aInfo.packageName)) {packageInfo.installSystemApplicationInfo(aInfo,getSystemContext().mPackageInfo.getClassLoader());}


if (differentUser) {// Caching not supported across users} else if (includeCode) {mPackages.put(aInfo.packageName,new WeakReference<LoadedApk>(packageInfo));} else {mResourcePackages.put(aInfo.packageName,new WeakReference<LoadedApk>(packageInfo));}}return packageInfo;}}


根据上述的源码解析,如何生成对应 LoadedApk 对象以及 ClassLoader 对象我们的思路就很明确了。


(1)首先我们通过反射的方式以插件 apk 生成对应的 ApplicationInfo 对象以及 CompatibilityInfo;


(2)接着调用如下方法生成对应的 LoadedApk 对象并将 ActivityInfo 所对应的 ApplicationInfo 中的 PackageName 设置为插件的 ApplicatioInfo,这样 Activity 初始化的时候就能够直接获取缓存中的我们所生成的 LoadedApk 对象了;


(3)最后再将以插件 Apk 生成的 ClassLoader 再以反射的方式赋值给 LoadedApk 对象中的成员变量,到这里整个 Activity 的插件化也就完成了一大半了。


public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,int flags) {.....

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android插件化-Activity篇,安卓开发面试问题