2024年Android开发的艺术:插件化框架Virtual APK实现原理解析(1),2024年最新字节跳动+京东+美团+腾讯面试总结

文末

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

<activity android:exported=“false” android:name=“.A$2” android:launchMode=“standard”

android:theme=“@android:style/Theme.Translucent” />

  • 2.2.2 hook Instrumentation
  1. 将系统提供的Instrumentation替换为自定义的VAInstrumentation,将主线程Handler的Callback也替换为VAInstrumentation(VAInstrumentation 实现了Handler.Callback接口)

protected void hookInstrumentationAndHandler() {

try {

// 获取当前进程的activityThread

ActivityThread activityThread = ActivityThread.currentActivityThread();

// 获取当前进程的Instrumentation

Instrumentation baseInstrumentation = activityThread.getInstrumentation();

// 创建自定义Instrumentation

final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);

// 将当前进程原有的Instrumentation对象替换为自定义的

Reflector.with(activityThread).field(“mInstrumentation”).set(instrumentation);

// 将当前进程原有的主线程Hander的callback替换为自定义的

Handler mainHandler = Reflector.with(activityThread).method(“getHandler”).call();

Reflector.with(mainHandler).field(“mCallback”).set(instrumentation);

this.mInstrumentation = instrumentation;

Log.d(TAG, "hookInstrumentationAndHandler succeed : " + mInstrumentation);

} catch (Exception e) {

Log.w(TAG, e);

}

}

  • 2.2.3 启动Activity时对AMS进行欺骗

如果我们熟悉Activity启动流程的话,我们一定知道Activity的启动和生命周期管理,都间接通过Instrumentation进行管理的。–如果不熟悉也没关系,可以看我之前写的AMS系列文章,看完保证秒懂(雾)。VAInstrumentation重写了这个类的一些重要方法,我们根据Activity启动流程一个一个说

  • 2.2.3.1 execStartActivity

这个方法有很多个重载,挑其中一个:

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode) {

// 对原始Intent进行处理

injectIntent(intent);

return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode);

}

injectIntent方法对Intent的处理在ComponentsHandler#markIntentIfNeeded方法,对原始Intent进行解析,获取目标Actiivty的包名和类名,如果目标Activity的包名和当前进程不同且该包名对应的LoadedPlugin对象存在,则说明它是我们加载过的插件APK中的Activity,则对该Intent的目标进行替换:

public void markIntentIfNeeded(Intent intent) {

String targetPackageName = intent.getComponent().getPackageName();

String targetClassName = intent.getComponent().getClassName();

// 判断是否需要启动的是插件Apk的Activity

if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {

// 将原始Intent的目标Acitivy替换为预设的插桩Activity中的一个

dispatchStubActivity(intent);

}

}

dispatchStubActivity方法根据原始Intent的启动模式选择合适的插桩Activity,将原始Intent中的类名修改为插桩Activity的类名,示例代码:

case ActivityInfo.LAUNCH_SINGLE_TOP: {

usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;

stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);

break;

}

case ActivityInfo.LAUNCH_SINGLE_TASK: {

usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;

stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);

break;

}

case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {

usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;

stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);

break;

}

  • 2.2.3.2 newActivity

如果只是对原始Intent进行替换,那么最终启动的会是插桩Activity,这显然达不到启动插件Apk中Acitivty的目的,在Activity实例创建阶段,还需要对实际创建的Actiivty进行替换,方法在VAInstrumentation#newActivity:

@Override

public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {

try {

cl.loadClass(className);

Log.i(TAG, String.format(“newActivity[%s]”, className));

} catch (ClassNotFoundException e) {

ComponentName component = PluginUtil.getComponent(intent);

String targetClassName = component.getClassName();

Log.i(TAG, String.format(“newActivity[%s : %s/%s]”, className, component.getPackageName(), targetClassName));

LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);

// 使用在LoadedPlugin对象中创建的DexClassLoader进行类加载,该ClassLoader指向插件APK所在路径

Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);

activity.setIntent(intent);

// 插件Activity实例创建后,将Resource替换为插件APK的资源

Reflector.QuietReflector.with(activity).field(“mResources”).set(plugin.getResources());

return newActivity(activity);

}

return newActivity(mBase.newActivity(cl, className, intent));

}

如果我们启动的是插件APK里的Activity,这个方法的Catch语句块是一定会被执行的,因为入参className已经被替换为插桩Activity的,但是我们只是在宿主App的AndroidManifest.xml中定义了这些Actiivty,并没有真正的实现。在进入Catch语句块后,使用LoadedPlugin中保存的DexClassloader进行Activity的创建。

  • 2.2.3.3 AMS对插件APK中的Activity管理

看到这里,可能就会有同学有问题了,你把要启动的Activity给替换了,但是AMS中不是还记录的是插桩Actiivty么,那么这个Activity实例后续跟AMS的交互怎么办?那岂不是在AMS中的记录找不到了?放心,不会出现这个问题的。复习之前AMS系列文章我们就会知道,AMS中对Activity管理的依据是一个叫appToken的Binder实例,在客户端对应的token会在Instrumentation#newActivity执行完成后调用Activity#attach方法传递给Actiivty。

这也是为什么对AMS进行欺骗这种插件化方案可行的原因,因为后续管理是使用的token,如果Android使用className之类的来管理的话,恐怕这种方案就不太好实现了。

  • 2.2.3.4 替换Context、applicaiton、Resources

在系统创建插件Activity的Context创建完成之后,需要将其替换为PluginContext,PluginContext和Context的区别是其内部保存有一个LoadedPlugin对象,方便对Context中的资源进行替换。代码在VAInstrumentaiton#injectActivity,调用处在VAInstrumentaiton#callActivityOnCreate

protected void injectActivity(Activity activity) {

final Intent intent = activity.getIntent();

if (PluginUtil.isIntentFromPlugin(intent)) {

Context base = activity.getBaseContext();

try {

LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);

Reflector.with(base).field(“mResources”).set(plugin.getResources());

Reflector reflector = Reflector.with(activity);

reflector.field(“mBase”).set(plugin.createPluginContext(activity.getBaseContext()));

reflector.field(“mApplication”).set(plugin.getApplication());

// set screenOrientation

ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));

if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {

activity.setRequestedOrientation(activityInfo.screenOrientation);

}

// for native activity

ComponentName component = PluginUtil.getComponent(intent);

Intent wrapperIntent = new Intent(intent);

wrapperIntent.setClassName(component.getPackageName(), component.getClassName());

wrapperIntent.setExtrasClassLoader(activity.getClassLoader());

activity.setIntent(wrapperIntent);

} catch (Exception e) {

Log.w(TAG, e);

}

}

}

2.3 Service的处理

Virtual APK启动插件APK中Activity的整体方案:

  1. 使用动态代理代理宿主APP中所有关于Service的请求

  2. 判断是否为插件APK中的Service,如果不是,则说明为宿主 APP中的,直接打开即可

  3. 如果是插件APK中的Service,则判断是否为远端Service,如果是远端Service,则启动RemoteService,并在其StartCommand方法中根据所代理的生命周期方法进行处理

  4. 如果是本地Service,则启动LocalService,并在其StartCommand方法中根据所代理的生命周期方法进行处理

  • 2.3.1 插件化框架初始化时代理系统的IActivityManager

IActivityManager是AMS的实现接口,它的实现类分别是ActivityManagerService和其proxy

这里我们需要代理的是Proxy,实现方法在PluginManager#hookSystemServices

protected void hookSystemServices() {

try {

Singleton<IActivityManager对象> defaultSingleton;

// 获取IActivityManager对象

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

defaultSingleton = Reflector.on(ActivityManager.class).field(“IActivityManagerSingleton”).get();

} else {

defaultSingleton = Reflector.on(ActivityManagerNative.class).field(“gDefault”).get();

}

IActivityManager origin = defaultSingleton.get();

// 创建activityManager对象的动态代理

IActivityManager activityManager对象的动态代理 = (IActivityManager) Proxy.newProxyInstance(mContext.getClassLoader(), new Class[] { IActivityManager.class },

createActivityManagerProxy(origin));

// 使用动态代理替换之前的IActivityManager对象实例

Reflector.with(defaultSingleton).field(“mInstance”).set(activityManagerProxy);

if (defaultSingleton.get() == activityManagerProxy) {

this.mActivityManager = activityManagerProxy;

Log.d(TAG, "hookSystemServices succeed : " + mActivityManager);

}

} catch (Exception e) {

Log.w(TAG, e);

}

}

通过将动态代理对系统创建的ActivityManager的proxy进行替换,这样,调用AMS方法时,会转到ActivityManagerProxy的invoke方法,并根据方法名对Service的生命周期进行管理,生命周期方法较多,挑选其中一个:

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

if (“startService”.equals(method.getName())) {

try {

return startService(proxy, method, args);

} catch (Throwable e) {

Log.e(TAG, “Start service error”, e);

}

}

startService:

protected Object startService(Object proxy, Method method, Object[] args) throws Throwable {

IApplicationThread appThread = (IApplicationThread) args[0];

Intent target = (Intent) args[1];

ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);

if (null == resolveInfo || null == resolveInfo.serviceInfo) {

// 插件中没找到,说明是宿主APP自己的Service

return method.invoke(this.mActivityManager, args);

}

// 启动插件APK中的Service

return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);

}

startDelegateServiceForTarget中会调用wrapperTargetIntent处理,最终在RemoteService或者LocalService的onStartCommand中对Service的各生命周期处理。

需要注意的是,在RemoteService中需要重新对APK进行解析和装载,生成LoadedPlugin,因为它运行在另一个进程中。

这也说明插件APK的Service进程如果声明了多个是无效的,因为他们最终都会运行在宿主RemoteService所在进程。

2.4 ContentProvider的处理

ContentProvicer的处理和Service是类似的,不多说了。

三、插件App的实现

======================================================================

插件APP理论上并不需要做什么特殊处理,唯一需要注意的是资源文件的冲突问题,因此,需要在插件工程app目录下的build.gradle中添加如下代码:

virtualApk {

packageId = 0x6f // the package id of Resources.

targetHost = ‘…/…/VirtualAPK/app’ // the path of application module in host project.

applyHostMapping = true //optional, default value: true.

}

它的作用是在插件APK编译时对资源ID进行重写,处理方法在ResourceCollector.groovy文件的collect方法:

def collect() {

//1、First, collect all resources by parsing the R symbol file.

parseResEntries(allRSymbolFile, allResources, allStyleables)

//2、Then, collect host resources by parsing the host apk R symbol file, should be stripped.

parseResEntries(hostRSymbolFile, hostResources, hostStyleables)

//3、Compute the resources that should be retained in the plugin apk.

filterPluginResources()

//4、Reassign the resource ID. If the resource entry exists in host apk, the reassign ID

// should be same with value in host apk; If the resource entry is owned by plugin project,

// then we should recalculate the ID value.

reassignPluginResourceId()

//5、Collect all the resources in the retained AARs, to regenerate the R java file that uses the new resource ID

vaContext.retainedAarLibs.each {

gatherReservedAarResources(it)

}

}

首先获取插件app和宿主app的资源集合,然后寻找其中冲突的资源id进行修改,修改id是 reassignPluginResourceId方法:

private void reassignPluginResourceId() {

// 对资源ID根据typeId进行排序

resourceIdList.sort { t1, t2 ->

t1.typeId - t2.typeId

}

面试复习笔记:

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960页Android开发笔记》

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960页Android开发笔记》

[外链图片转存中…(img-TSWPUdrk-1715594714497)]

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

[外链图片转存中…(img-HcUDuyef-1715594714497)]

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值