写点什么

【Android 面试】热修复,赶紧收藏备战金三银四

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

在 Android 传统开发中,一旦应用的代码被打包成 APK 并被上传到各个应用市场,我们就不能修改应用的源码了,只能通过服务器来控制应用中预留的分支代码。但是很多时候我们无法预知需求和突然发生的情况,也就不能提前在应用代码中预留分支代码,这时就需要采用动态加载技术,即在程序运行时,动态加载一些程序中原本不存在的可执行文件并运行这些文件里的代码逻辑。其中可执行文件包括动态链接库 so 和 dex 相关文件(dex 以及包含 dex 的 jar/apk 文件)。随着应用开发技术和业务的逐步发展,动态加载技术派生出两个技术:热修复和插件化。其中热修复技术主要用来修复 Bug,而插件化技术则主要用于解决应用越来越庞大以及功能模块的解耦。详细点说,就是为了解决以下几种情况:


  • 1、业务复杂、模块耦合:随着业务越来越复杂,应用程序的工程和功能模块数量会越来越多,一个应用可能由几十甚至几百人来协同开发,其中的一个工程可能就由一个小组来进行开发维护,如果功能模块间的耦合度较高,修改一个模块会影响其它功能模块,势必会极大地增加沟通成本。

  • 2、应用间的接入:当一个应用需要接入其它应用时,如淘宝,为了将流量引流到其它的淘宝应用如:飞猪旅游、口碑外卖、聚划算等等应用,如使用常规技术有两个问题:可能要维护多个版本的问题或单个应用体积将会非常庞大的问题。

  • 3、65536 限制,内存占用大。

插件化的思想:

安装的应用可以理解为插件,这些插件可以自由地进行插拔。

插件化的定义:

插件一般是指经过处理的 APK,so 和 dex 等文件,插件可以被宿主进行加载,有的插件也可以作为 APK 独立运行。


将一个应用按照插件的方式进行改造的过程就叫作插件化。

插件化的优势:

  • 低耦合

  • 应用间的接入和维护更便捷,每个应用团队只需要负责自己的那一部分。

  • 应用及主 dex 的体积也会相应变小,间接地避免了 65536 限制。

  • 第一次加载到内存的只有淘宝客户端,当使用到其它插件时才会加载相应插件到内存,以减少内存占用。

插件化框架对比:

  • 最早的插件化框架:2012 年大众点评的屠毅敏就推出了 AndroidDynamicLoader 框架。

  • 目前主流的插件化方案有滴滴任玉刚的 VirtualApk、360 的 DroidPlugin、RePlugin、Wequick 的 Small 框架。

  • 如果加载的插件不需要和宿主有任何耦合,也无须和宿主进行通信,比如加载第三方 App,那么推荐使用 RePlugin,其他情况推荐使用 VirtualApk。由于 VirtualApk 在加载耦合插件方面是插件化框架的首选,具有普遍的适用性,因此有必要对它的源码进行了解。

插件化原理:

Activity 插件化:

主要实现方式有三种:


  • 反射:对性能有影响,主流的插件化框架没有采用此方式。

  • 接口:dynamic-load-apk 采用。

  • Hook:主流。


Hook 实现方式有两种:Hook IActivityManager 和 Hook Instrumentation。主要方案就是先用一个在 AndroidManifest.xml 中注册的 Activity 来进行占坑,用来通过 AMS 的校验,接着在合适的时机用插件 Activity 替换占坑的 Activity。


Hook IActivityManager:


1、占坑、通过校验:


在 Android 7.0 和 8.0 的源码中 IActivityManager 借助了 Singleton 类实现单例,而且该单例是静态的,因此 IActivityManager 是一个比较好的 Hook 点。


接着,定义替换 IActivityManager 的代理类 IActivityManagerProxy,由于 Hook 点 IActivityManager 是一个接口,建议这里采用动态代理。


  • 拦截 startActivity 方法,获取参数 args 中保存的 Intent 对象,它是原本要启动插件 TargetActivity 的 Intent。

  • 新建一个 subIntent 用来启动 StubActivity,并将前面得到的 TargetActivity 的 Intent 保存到 subIntent 中,便于以后还原 TargetActivity。

  • 最后,将 subIntent 赋值给参数 args,这样启动的目标就变为了 StubActivity,用来通过 AMS 的校验。


然后,用代理类 IActivityManagerProxy 来替换 IActivityManager。


  • 当版本大于等于 26 时,使用反射获取 ActivityManager 的 IActivityManagerSingleton 字段,小于时则获取 ActivityManagerNative 中的 gDefault 字段。

  • 然后,通过反射获取对应的 Singleton 实例,从上面得到的 2 个字段中拿到对应的 IActivityManager。

  • 最后,使用 Proxy.newProxyInstance()方法动态创建代理类 IActivityManagerProxy,用 IActivityManagerProxy 来替换 IActivityManager。


2、还原插件 Activity:


  • 前面用占坑 Activity 通过了 AMS 的校验,但是我们要启动的是插件 TargetActivity,还需要用插件 TargetActivity 来替换占坑的 SubActivity,替换时机为图中步骤 2 之后。

  • 在 ActivityThread 的 H 类中重写的 handleMessage 方法会对 LAUNCH_ACTIVITY 类型的消息进行处理,最终会调用 Activity 的 onCreate 方法。在 Handler 的 dispatchMessage 处理消息的这个方法中,看到如果 Handelr 的 Callback 类型的 mCallBack 不为 null,就会执行 mCallback 的 handleMessage 方法,因此 mCallback 可以作为 Hook 点。我们可以用自定义的 Callback 来替换 mCallback。


自定义的 Callback 实现了 Handler.Callback,并重写了 handleMessage 方法,当收到消息的类型为 LAUNCH_ACTIVITY 时,将启动 SubActivity 的 Intent 替换为启动 TargetActivity 的 Intent。然后使用反射将 Handler 的 mCallback 替换为自定义的 CallBack 即可。使用时则在 application 的 attachBaseContext 方法中进行 hook 即可。


3、插件 Activity 的生命周期:


  • AMS 和 ActivityThread 之间的通信采用了 token 来对 Activity 进行标识,并且此后的 Activity 的生命周期处理也是根据 token 来对 Activity 进行标识的,因为我们在 Activity 启动时用插件 TargetActivity 替换占坑 SubActivity,这一过程在 performLaunchActivity 之前,因此 performLaunchActivity 的 r.token 就是 TargetActivity。所以 TargetActivity 具有生命周期。


Hook Instrumentation:


Hook Instrumentation 实现同样也需要用到占坑 Activity,与 Hook IActivity 实现不同的是,用占坑 Activity 替换插件 Activity 以及还原插件 Activity 的地方不同。


分析:在 Activity 通过 AMS 校验前,会调用 Activity 的 startActivityForResult 方法,其中调用了 Instrumentation 的 execStartActivity 方法来激活 Activity 的生命周期。并且在 ActivityThread 的 performLaunchActivity 中使用了 mInstrumentation 的 newActivity 方法,其内部会用类加载器来创建 Activity 的实例。


方案:在 Instrumentation 的 execStartActivity 方法中用占坑 SubActivity 来通过 AMS 的验证,在 Instrumentation 的 newActivity 方法中还原 TargetActivity,这两部操作都和 Instrumentation 有关,因此我们可以用自定义的 Instumentation 来替换掉 mInstrumentation。具体为:


  • 首先检查 TargetActivity 是否已经注册,如果没有则将 TargetActivity 的 ClassName 保存起来用于后面还原。接着把要启动的 TargetActivity 替换为 StubActivity,最后通过反射调用 execStartActivity 方法,这样就可以用 StubActivity 通过 AMS 的验证。

  • 在 newActivity 方法中创建了此前保存的 TargetActivity,完成了还原 TargetActivity。最后使用反射用 InstrumentationProxy 替换 mInstumentation。

资源插件化:

资源的插件化和热修复的资源修复都借助了 AssetManager。


资源的插件化方案主要有两种:


  • 1、合并资源方案,将插件的资源全部添加到宿主的 Resources 中,这种方案插件可以访问宿主的资源。

  • 2、构建插件资源方案,每个插件都构造出独立的 Resources,这种方案插件不可以访问宿主资源。

so 的插件化:

so 的插件化方案和 so 热修复的第一种方案类似,就是将 so 插件插入到 NativelibraryElement 数组中,并且将存储 so 插件的文件添加到 nativeLibraryDirectories 集合中就可以了。

插件的加载机制方案:
  • 1、Hook ClassLoader。

  • 2、委托给系统的 ClassLoader 帮忙加载。

2、模块化和组件化

模块化的好处

[www.jianshu.com/p/376ea8a19…](


)

分析现有的组件化方案:

很多大厂的组件化方案是以 多工程 + 多 Module 的结构(微信, 美团等超级 App 更是以 多工程 + 多 Module + 多 P 工程(以页面为单元的代码隔离方式) 的三级工程结构), 使用 Git Submodule 创建多个子仓库管理各个模块的代码, 并将各个模块的代码打包成 AAR 上传至私有 Maven 仓库使用远程版本号依赖的方式进行模块间代码的隔离。

组件化开发的好处:

  • 避免重复造轮子,可以节省开发和维护的成本。

  • 可以通过组件和模块为业务基准合理地安排人力,提高开发效率。

  • 不同的项目可以共用一个组件或模块,确保整体技术方案的统一性。

  • 为未来插件化共用同一套底层模型做准备。

跨组件通信:

跨组件通信场景:


  • 第一种是组件之间的页面跳转 (Activity 到 Activity, Fragment 到 Fragment, Activity 到 Fragment, Fragment 到 Activity) 以及跳转时的数据传递 (基础数据类型和可序列化的自定义类类型)。

  • 第二种是组件之间的自定义类和自定义方法的调用(组件向外提供服务)。

跨组件通信方案分析:

  • 第一种组件之间的页面跳转不需要过多描述了, 算是 ARouter 中最基础的功能, API 也比较简单, 跳转时想传递不同类型的数据也提供有相应的 API。

  • 第二种组件之间的自定义类和自定义方法的调用要稍微复杂点, 需要 ARouter 配合架构中的 公共服务(CommonService) 实现:

提供服务的业务模块:

在公共服务(CommonService) 中声明 Service 接口 (含有需要被调用的自定义方法), 然后在自己的模块中实现这个 Service 接口, 再通过 ARouter API 暴露实现类。

使用服务的业务模块:

通过 ARouter 的 API 拿到这个 Service 接口(多态持有, 实际持有实现类), 即可调用 Service 接口中声明的自定义方法, 这样就可以达到模块之间的交互。 此外,可以使用 AndroidEventBus 其独有的 Tag, 可以在开发时更容易定位发送事件和接受事件的代码, 如果以组件名来作为 Tag 的前缀进行分组, 也可以更好的统一管理和查看每个组件的事件, 当然也不建议大家过多使用 EventBus。

如何管理过多的路由表?

RouterHub 存在于基础库, 可以被看作是所有组件都需要遵守的通讯协议, 里面不仅可以放路由地址常量, 还可以放跨组件传递数据时命名的各种 Key 值, 再配以适当注释, 任何组件开发人员不需要事先沟通只要依赖了这个协议, 就知道了各自该怎样协同工作, 既提高了效率又降低了出错风险, 约定的东西自然要比口头上说的强。


Tips: 如果您觉得把每个路由地址都写在基础库的 RouterHub 中, 太麻烦了, 也可以在每个组件内部建立一个私有 RouterHub, 将不需要跨组件的路由地址放入私有 RouterHub 中管理, 只将需要跨组件的路由地址放入基础库的公有 RouterHub 中管理, 如果您不需要集中管理所有路由地址的话, 这也是比较推荐的一种方式。

ARouter 路由原理:

ARouter 维护了一个路由表 Warehouse,其中保存着全部的模块跳转关系,ARouter 路由跳转实际上还是调用了 startActivity 的跳转,使用了原生的 Framework 机制,只是通过 apt 注解的形式制造出跳转规则,并人为地拦截跳转和设置跳转条件。

多模块开发的时候不同的负责人可能会引入重复资源,相同的字符串,相同的 icon 等但是文件名并不一样,怎样去重?

3、gradle

gradle 熟悉么,自动打包知道么?

如何加快 Gradle 的编译速度?

Gradle 的 Flavor 能否配置 sourceset?

Gradle 生命周期

4、编译插桩

谈谈你对 AOP 技术的理解?

说说你了解的编译插桩技术?

面试复习笔记:

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


**戳我领取:[Android 对线暴打面试指南](


)、[超硬核 Android 面试知识笔记](


)、[3000 页 Android 开发者架构师


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


核心知识笔记](


)**


《960 页 Android 开发笔记》



《1307 页 Android 开发面试宝典》


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



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


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


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



资料已经上传在[我的 GitHub](


),或者关注后简信我【666】即可领取(无偿)。




推荐阅读:[字节跳动 8 年老 Android 面试官谈;Context 都没弄明白凭什么拿高薪?](


)[做了六年 Android,终于熬出头了,15K 到 31K 全靠这份高级面试题+解析](


)[字节、腾讯,阿里 Android 高级面试真题汇总,会一半随便进大厂](


)

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
【Android面试】热修复,赶紧收藏备战金三银四