写点什么

2021 Android 大厂面试(五)插件化,androidframework 开发书籍

发布于: 23 小时前

将插件的 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) {


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


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<Resources>> map = (Map<Object, WeakReference<Resources>>) 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 类文件


补丁: 相对于热修复技术来说,就是要加载运行的.patch,.dex,*.apk 等一系列包含 dex 修复内容的文件。



QQ 空间超级补丁方案


Tinker



HotFix



当然就热修复的实现,各个大厂还有各自的实现,比如饿了吗的 Amigo,美团的 Robust,实现及优缺点各有差异,但总的来说就是两大类


ClassLoader 加载方案


Native 层替换方案


或者是参考 Android Studio Instant Run 的思路实现代码整体的增量更新。但这样势必会带来性能的影响。


Sophix


底层替换方案


原理:在已经加载的类中直接替换掉原有方法,是在原有类的结构基础上进行修改的。在 hook 方法入口 ArtMethod 时,通过构造一个新的 ArtMethod 实现替换方法入口的跳转。


应用:能即时生效,Andfix 采用此方案。


缺点:底层替换稳定性不好,适用范围存在限制,通过改造代码绕过限制既不优雅也不方便,并且还没提供资源及 so 的修复。


类加载方案


原理:让 app 重新启动后让 ClassLoader 去加载新的类。如果不重启,原来的类还在虚拟机中无法重复加载。


优点:修复范围广,限制少。


应用:腾讯系包括 QQ 空间,手 QFix,Tinker 采用此方案。


QQ 空间会侵入打包流程。


QFix 需要获取底层虚拟机的函数,不稳定。


Tinker 是完整的全量 dex 加载。



Tinker 与 Sophix 方案不同之处


Tinker 采用 dex merge 生成全量 DEX 方案。反编译为 smali,然后新 apk 跟基线 apk 进行差异对比,最后得到补丁包。


Dalvik 下 Sophix 和 Tinker 相同,在 Art 下,Sophix 不需要做 dex merge,因为 Art 下本质上虚拟机已经支持多 dex 的加载,要做的仅仅是把补丁 dex 作为主 dex(classes.dex)加载而已:


将补丁 dex 命名为 classes.dex,原 apk 中的 dex 依次命名为 classes(2, 3, 4...).dex 就好了,然后一起打包为一个压缩文件。然后 DexFile.loadDex 得到 DexFile 对象,最后把该 DexFile 对象整个替换旧的 dexElements 数组就好了。


资源修复方案


基本参考 InstantRun 的实现:构造一个包含所有新资源的新的 AssetManager。并在所有之前引用到原来的 AssetManager 通过反射替换掉。


Sophix 不修改 AssetManager 的引用,构造的补丁包中只包含有新增或有修改变动的资源,在原 AssetManager 中 addAssetPath 这个包就可以了。资源包不需要在运行时合成完整包。


so 库修复方案


本质是对 native 方法的修复和替换。类似类修复反射注入方式,将补丁 so 库的路径插入到 nativeLibraryDirectories 数据最前面。


Method Hook


5.项目组件化的理解




总结


组件化相较于单一工程,在组件模式下可以提高编译速度,方便单元测试,提高开发效率。


开发人员分工更加明确,基本上做到互不干扰。


业务组件的架构也可以自由选择,不影响同伴之间的协作。


降低维护成本,代码结构更加清晰。


6.描述清点击 Android Studio 的 build 按钮后发生了什么




apply plugin : 'com.android.application'


apply plugin : 'com.android.library'


编译五阶段


1.准备依赖包 Preparation of dependecies


2.合并资源并处理清单 Merging resources and proccesssing Manifest


3.编译 Compiling


4.后期处理 Postprocessing


5.包装和出版 Packaging and publishing



简单构建流程:


1. Android 编译器(5.0 之前是 Dalvik,之后是 ART)将项目的源代码(包括一些第三方库、jar 包和 aar 包)转换成 DEX 文件,将其他资源转换成已编译资源。


2. APK 打包器将 DEX 文件和已编译资源在使用秘钥签署后打包。


3. 在生成最终 APK 之前,打包器会使用 zipalign 等工具对应用进行优化,减少其在设备上运行时的内存占用。


构建流程结束后获得测试或发布用的 apk。



图中的矩形表示用到或者生成的文件,椭圆表示工具。


1. 通过 aapt 打包 res 资源文件,生成 R.java、resources.arsc 和 res 文件


2. 处理.aidl 文件,生成对应的 Java 接口文件


3. 通过 Java Compiler 编译 R.java、Java 接口文件、Java 源文件,生成.class 文件


4. 通过 dex 命令,将.class 文件和第三方库中的.class 文件处理生成 classes.dex


5. 通过 apkbuilder 工具,将 aapt 生成的 resources.arsc 和 res 文件、assets 文件和 classes.dex 一起打包生成 apk


6. 通过 Jarsigner 工具,对上面的 apk 进行 debug 或 release 签名


7. 通过 zipalign 工具,将签名后的 apk 进行对齐处理。


这样就得到了一个可以安装运行的 Android 程序。



7.彻底搞懂 Gradle、Gradle Wrapper 与 Android Plugin for Gradle 的区别和联系




Offline work时可能出现"No cached version of com.android.tools.build:gradle:xxx available for offline mode"问题


Gradle: gradle-wrapper.properties 中的 distributionUrl=https/://services.gradle.org/distributions/gradle-2.10-all.zip


Gradle 插件:build.gradle 中依赖的 classpath 'com.android.tools.build:gradle:2.1.2'


Gradle:


一个构建系统,构建项目的工具,用来编译 Android app,能够简化你的编译、打包、测试过程。


Gradle 是一个基于 Apache Ant 和 Apache Maven 概念的项目自动化建构工具。它使用一种基于 Groovy 的特定领域语言来声明项目设置,而不是传统的 XML。当前其支持的语言限于 Java、Groovy 和 Scala


Gradle 插件:


我们在 AS 中用到的 Gradle 被叫做 Android Plugin for Gradle,它本质就是一个 AS 的插件,它一边调用 Gradle 本身的代码和批处理工具来构建项目,一边调用 Android SDK 的编译、打包功能。


Gradle 插件跟 Android SDK BuildTool 有关联,因为它还承接着 AS 里的编译相关的功能,在项目的 local.properties 文件里写明 Android SDK 路径、在 build.gradle 里写明 buildToolsVersion 的原因。


| 插件版本 | Gradle 版本 |


| --- | --- |


| 1.0.0 - 1.1.3 | 2.2.1 - 2.3 |


| 1.2.0 - 1.3.1 | 2.2.1 - 2.9 |


| 1.5.0 | 2.2.1 - 2.13 |


| 2.0.0 - 2.1.2 | 2.10 - 2.13 |


| 2.1.3 - 2.2.3 | 2.14.1+ |


| 2.3.0+ | 3.3+ |


| 3.0.0+ | 4.1+ |


| 3.1.0+ | 4.4+ |


| 3.2.0 - 3.2.1 | 4.6+ |


| 3.3.0 - 3.3.2 | 4.10.1+ |


| 3.4.0+ | 5.1.1+ |


Done…

总结

程序员,立之根本还是技术,一个程序员的好坏,虽然不能完全用技术强弱来判断,但是技术水平一定是基础,技术差的程序员只能 CRUD,技术不深的程序员也成不了架构师。程序员对于技术的掌握,除了从了解-熟悉-熟练-精通的过程以外,还应该从基础出发,到进阶,到源码,到实战。所以,程序员想要成功,首先要成就自己。


今天,这份(解读开源框架设计思想笔记)终于爆火了, 看完之后我直接跪了!这份 Android 全能笔记内容齐全,包括以下几个方面:


解读开源框架设计思想:热修复设计+插件化框架解读+组件化框架设计+图片加载框架+网络访问框架设计+RXJava 响应式编程框架设计+IOC 架构设计+Android 架构组件 Jetpack



需要相关知识点可以查看我的【GitHub】,对于已经掌握的可以忽略以节省时间。


如果不方便查看,我已经整理成了一份 PDF 包含 Android 入门,基础—高级的全部系列知识点,还有新技术学习笔记。

需要全套系列笔记可以直接,点击链接

https://jq.qq.com/?_wv=1027&k=OQA7ghiD】找群主大大免费获取!

解读开源框架设计思想

1.热修复设计


  • AOT/JIT & dexopt 与 dex2oat

  • 热修复设计之 CLASS_ISPREVERIFIED 问题

  • 热修复设计之热修复原理

评论

发布
暂无评论
2021 Android 大厂面试(五)插件化,androidframework开发书籍