2021 Android 大厂面试(五)插件化

阿里系:DeXposed、andfix:从底层二进制入手(c语言)。阿里andFix hook 方法在native的具体字段。

art虚拟机上是一个叫ArtMethod的结构体。通过修改该结构体上有bug的字段来达到修复bug方法的目的,

但这个artMethod是根据安卓原生的结构写死的,国内很多第三方厂家会改写ArtMethod结构,导致替换失效。

腾讯系:tinker:从java加载机制入手。qq的dex插装就类似上面分析的那种。通过将修复的dex文件插入到app的dexFileList的前面,达到更新bug的效果,但是不能及时生效,需要重启。

但虚拟机在安装期间会为类打上CLASS_ISPREVERIFIED标志,是为了提高性能的,我们强制防止类被打上标志是否会有些影响性能

美团robust:是在编译器为每个方法插入了一段逻辑代码,并为每个类创建了一个ChangeQuickRedirect静态成员变量,当它不为空会转入新的代码逻辑达到修复bug的目的。

优点是兼容性高,但是会增加应用体积

PathClassLoader和DexClassLoader都继承自BaseDexClassLoader

1、Android使用PathClassLoader作为其类加载器,只能去加载已经安装到Android系统中的apk文件;

2、DexClassLoader可以从.jar和.apk类型的文件内部加载classes.dex文件就好了。热修复也用到这个类。

(1)动态改变BaseDexClassLoader对象间接引用的dexElements;

(2)在app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志。

(3)我们使用 hook 思想代理 startActivity 这个方法,使用占坑的方式。

1. startActivity 的时候最终会走到 AMS 的 startActivity 方法

2. 系统会检查一堆的信息验证这个 Activity 是否合法。

3. 然后会回调 ActivityThread 的 Handler 里的 handleLaunchActivity

4. 在这里走到了 performLaunchActivity 方法去创建 Activity 并回调一系列生命周期的方法

5. 创建 Activity 的时候会创建一个 LoaderApk对象,然后使用这个对象的 getClassLoader 来创建 Activity

6. 我们查看 getClassLoader() 方法发现返回的是 PathClassLoader,然后他继承自 BaseDexClassLoader

7. 然后我们查看 BaseDexClassLoader 发现他创建时创建了一个 DexPathList 类型的 pathList对象,然后在 findClass 时调用了 pathList.findClass 的方法

8. 然后我们查看 DexPathList类 中的 findClass 发现他内部维护了一个 Element[] dexElements的dex 数组,findClass 时是从数组中遍历查找的

2.插件化原理分析


DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。

DexClassloader多传了一个optimizedDirectory

DexPathList

多DexClassLoader

每个插件单独一个DexClassLoader,相对隔离,RePlugin采用该方案

单DexClassLoader

将插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。方便插件与宿主(插件)之间的调用,Small采用该方案

插件调用主工程

主工程的ClassLoader作为插件ClassLoader的父加载器

主工程调用插件

若使用多ClassLoader机制,通过插件的ClassLoader先加载类,再通过反射调用

若使用单ClassLoader机制,直接通过类名去访问插件中的类,弊端是库的版本可能不一致,需要规范

资源加载

//创建AssetManager对象

AssetManager assets = new AssetManager();

//将apk路径添加到AssetManager中

if (assets.addAssetPath(resDir) == 0){

return null;

}

//创建Resource对象

r = new Resources(assets, metrics, getConfiguration(), compInfo);

插件apk的路径加入到AssetManager中

通过反射去创建,并且部分Rom对创建的Resource类进行了修改,所以需要考虑不同Rom的兼容性。

资源路径的处理

Context的处理

// 第一步:创建Resource

if (Constants.COMBINE_RESOURCES) {

//插件和主工程资源合并时需要hook住主工程的资源

Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());

ResourcesManager.hookResources(context, resources);

return resources;

} else {

//插件资源独立,该resource只能访问插件自己的资源

Resources hostResources = context.getResources();

AssetManager assetManager = createAssetManager(context, apk);

return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());

}

//第二步:hook主工程的Resource

//对于合并式的资源访问方式,需要替换主工程的Resource,下面是具体替换的代码。

public static void hookResources(Context base, Resources resources) {

try {

ReflectUtil.setField(base.getClass(), base, “mResources”, resources);

Object loadedApk = ReflectUtil.getPackageInfo(base);

ReflectUtil.setField(loadedApk.getClass(), loadedApk, “mResources”, resources);

Object activityThread = ReflectUtil.getActivityThread(base);

Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, “mResourcesManager”);

if (Build.VERSION.SDK_INT < 24) {

Map<Object, WeakReference> map = (Map<Object, WeakReference>) ReflectUtil.getField(resManager.getClass(), resManager, “mActiveResources”);

Object key = map.keySet().iterator().next();

map.put(key, new WeakReference<>(resources));

} else {

// still hook Android N Resources, even though it’s unnecessary, then nobody will be strange.

Map map = (Map) ReflectUtil.getFieldNoException(resManager.getClass(), resManager, “mResourceImpls”);

Object key = map.keySet().iterator().next();

Object resourcesImpl = ReflectUtil.getFieldNoException(Resources.class, resources, “mResourcesImpl”);

map.put(key, new WeakReference<>(resourcesImpl));

}

} catch (Exception e) {

e.printStackTrace();

}

替换了主工程context中LoadedApk的mResource对象

将新的Resource添加到主工程ActivityThread的mResourceManager中,并且根据Android版本做了不同处理

//第三步:关联resource和Activity

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

activity.setIntent(intent);

//设置Activity的mResources属性,Activity中访问资源时都通过mResources

ReflectUtil.setField(ContextThemeWrapper.class, activity, “mResources”, plugin.getResources());

资源冲突

资源id是由8位16进制数表示,表示为0xPPTTNNNN, 由三部分组成:PackageId+TypeId+EntryId

修改aapt源码,编译期修改PP段。

修改resources.arsc文件,该文件列出了资源id到具体资源路径的映射。

// Main.cpp

result = handleCommand(&bundle);

case kCommandPackage: return doPackage(bundle);

// Command.cpp

int doPackage(Bundle* bundle) {

if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {

err = buildResources(bundle, assets, builder);

if (err != 0) {

goto bail;

}

}

}

Resource.cpp

buildResources

ResourceTable.cpp

switch(mPackageType) {

case App:

case AppFeature:

packageId = 0x7f;

break;

case System:

packageId = 0x01;

break;

case SharedLibrary:

packageId = 0x00;

break;

}

首先找到入口类:Main.cpp:main函数,解析参数,然后调用handleCommand函数处理参数对应的逻辑,我们看到了有一个函数doPackage。

然后就搜索到了Command.cpp:在他内部的doPackage函数中进行编译工具的一个函数:buildResources函数,在全局搜索,发现了Resource.cpp:发现这里就是处理编译工作,构建ResourceTable的逻辑,在ResourceTable.cpp中,也是获取PackageId的地方,下面我们就来看看如何修改呢?

其实最好的方法是,能够修改aapt源码,添加一个参数,把我们想要编译的PackageId作为输入值,传进来最好了,那就是Bundle类型,他是从Main.cpp中的main函数传递到了最后的buildResources函数中,那么我们就可以把这个参数用Bundle进行携带。

————————————————————————————————————————————————

在整个过程中,需要修改到R文件、resources.arsc和二进制的xml文件

四大组件支持

ProxyActivity代理

代理方式的关键总结起来有下面两点:

ProxyActivity中需要重写getResouces,getAssets,getClassLoader方法返回插件的相应对象。生命周期函数以及和用户交互相关函数,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要转发给插件。

PluginActivity中所有调用context的相关的方法,如setContentView,getLayoutInflater,getSystemService等都需要调用ProxyActivity的相应方法。

该方式有几个明显缺点:

插件中的Activity必须继承PluginActivity,开发侵入性强。

如果想支持Activity的singleTask,singleInstance等launchMode时,需要自己管理Activity栈,实现起来很繁琐。

插件中需要小心处理Context,容易出错。

如果想把之前的模块改造成插件需要很多额外的工作。

预埋StubActivity,hook系统启动Activity的过程

VirtualAPK通过替换了系统的Instrumentation,hook了Activity的启动和创建,省去了手动管理插件Activity生命周期的繁琐,让插件Activity像正常的Activity一样被系统管理,并且插件Activity在开发时和常规一样,即能独立运行又能作为插件被主工程调用。

其他插件框架在处理Activity时思想大都差不多,无非是这两种方式之一或者两者的结合。在hook时,不同的框架可能会选择不同的hook点。如360的RePlugin框架选择hook了系统的ClassLoader,即构造Activity2的ClassLoader,在判断出待启动的Activity是插件中的时,会调用插件的ClassLoader构造相应对象。另外RePlugin为了系统稳定性,选择了尽量少的hook,因此它并没有选择hook系统的startActivity方法来替换intent,而是通过重写Activity的startActivity,因此其插件Activity是需要继承一个类似PluginActivity的基类的。不过RePlugin提供了一个Gradle插件将插件中的Activity的基类换成了PluginActivity,用户在开发插件Activity时也是没有感知的。

复制代码

www.jianshu.com/p/ac96420fc…

sanjay-f.github.io/2016/04/17/…

www.jianshu.com/p/d43e1fb42…

Service插件化总结

初始化时通过ActivityManagerProxy Hook住了IActivityManager。

服务启动时通过ActivityManagerProxy拦截,判断是否为远程服务,如果为远程服务,启动RemoteService,如果为同进程服务则启动LocalService。

如果为LocalService,则通过DexClassLoader加载目标Service,然后反射调用attach方法绑定Context,然后执行Service的onCreate、onStartCommand方法

如果为RemoteService,则先加载插件的远程Service,后续跟LocalService一致。

复制代码

3.模块化实现(好处,原因)


1、模块间解耦,复用。

(原因:对业务进行模块化拆分后,为了使各业务模块间解耦,因此各个都是独立的模块,它们之间是没有依赖关系。

每个模块负责的功能不同,业务逻辑不同,模块间业务解耦。模块功能比较单一,可在多个项目中使用。)

2、可单独编译某个模块,提升开发效率。

(原因:每个模块实际上也是一个完整的项目,可以进行单独编译,调试)

3、可以多团队并行开发,测试。

原因:每个团队负责不同的模块,提升开发,测试效率。

组件化与模块化

组件化是指以重用化为目的,将一个系统拆分为一个个单独的组件

避免重复造轮子,节省开发维护成本;

降低项目复杂性,提升开发效率;

多个团队公用同一个组件,在一定层度上确保了技术方案的统一性。

模块化业务分层:由下到上

基础组件层:

底层使用的库和封装的一些工具库(libs),比如okhttp,rxjava,rxandroid,glide等

业务组件层:

与业务相关,封装第三方sdk,比如封装后的支付,即时通行等

业务模块层:

按照业务划分模块,比如说IM模块,资讯模块等

Library Module开发问题

在把代码抽取到各个单独的Library Module中,会遇到各种问题。

最常见的就是R文件问题,Android开发中,各个资源文件都是放在res目录中,在编译过程中,会生成R.java文件。

R文件中包含有各个资源文件对应的id,这个id是静态常量,但是在Library Module中,这个id不是静态常量,那么在开发时候就要避开这样的问题。

举个常见的例子,同一个方法处理多个view的点击事件,有时候会使用switch(view.getId())这样的方式,

然后用case R.id.btnLogin这样进行判断,这时候就会出现问题,因为id不是经常常量,那么这种方式就用不了。

4.热修复、插件化


宿主: 就是当前运行的APP

插件: 相对于插件化技术来说,就是要加载运行的apk类文件

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

总结

最后对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

相信它会给大家带来很多收获:

上述【高清技术脑图】以及【配套的架构技术PDF】可以关注我免费获取

Android学习PDF+架构视频+面试文档+源码笔记

74)]
[外链图片转存中…(img-QDOZtmDU-1710488060875)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-hRUfG3qP-1710488060875)]

总结

最后对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

相信它会给大家带来很多收获:

[外链图片转存中…(img-BN6SedVV-1710488060875)]

[外链图片转存中…(img-XBSDIecC-1710488060876)]

上述【高清技术脑图】以及【配套的架构技术PDF】可以关注我免费获取

Android学习PDF+架构视频+面试文档+源码笔记

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值