最新热修复原理学习(7)so库加载原理,2024Android春招面试真题详解

文末

初级工程师拿到需求会直接开始做,然后做着做着发现有问题了,要么技术实现不了,要么逻辑有问题。

而高级工程师拿到需求会考虑很多,技术的可行性?对现有业务有没有帮助?对现有技术架构的影响?扩展性如何?等等…之后才会再进行设计编码阶段。

而现在随着跨平台开发,混合式开发,前端开发之类的热门,Android开发者需要学习和掌握的技术也在不断的增加。

通过和一些行业里的朋友交流讨论,以及参考现在大厂面试的要求。我们花了差不多一个月时间整理出了这份Android高级工程师需要掌握的所有知识体系。你可以看下掌握了多少。

混合式开发,微信小程序。都是得学会并且熟练的

这些是Android相关技术的内核,还有Java进阶

高级进阶必备的一些技术。像移动开发架构项目实战等

Android前沿技术;包括了组件化,热升级和热修复,以及各种架构跟框架的详细技术体系

以上即是我们整理的Android高级工程师需要掌握的技术体系了。可能很多朋友觉得很多技术自己都会了,只是一些新的技术不清楚而已。应该没什么太大的问题。

而这恰恰是问题所在!为什么别人高级工程师能年限突破30万,而你只有十几万呢?

就因为你只需补充你自己认为需要的,但并不知道企业需要的。这个就特别容易造成差距。因为你的技术体系并不系统,是零碎的,散乱的。那么你凭什么突破30万年薪呢?

我这些话比较直接,可能会戳到一些人的玻璃心,但是我知道肯定会对一些人起到点醒的效果的。而但凡只要有人因为我的这份高级系统大纲以及这些话找到了方向,并且付出行动去提升自我,为了成功变得更加努力。那么我做的这些就都有了意义。

喜欢的话请帮忙转发点赞一下能让更多有需要的人看到吧。谢谢!

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

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

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

2.2 静态注册native方法的实时生效


上面通过尝试对补丁so库进行重命名为全局唯一的名称,可以确保在第二次加载补丁时so库可以做到Dalvik下和Art下动态注册方法的实时生效,而要实现静态注册native方法的实时生效还需要做更多的工作。

前面我们说过静态注册native方法的映射是在native方法第一次执行的时候就完成了映射,所以如果native方法在加载补丁so库之前已经执行过了,那么是否这种时候这个静态注册的native方法一定得不到修复?幸运的是,系统JNI API通过了这个了解注册的接口。

static jint UnregisterNatives(JNIEnv* env, jclass jclazz) {

ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(ts.self(), jclazz);

dvmUnregisterJNINativeMethods(clazz);

return JNI_OK;

}

void dvmUnregisterJNINativeMethods(ClassObject* clazz) {

unregisterJNINativeMethods(clazz->directMethods, clazz->directionMethodCount);

unregisterJNINativeMethods(clazz->virtualMethods, clazz->virtualMethodCount)

}

static void unregisterJNINativeMethods(Method* methods, size_t count) {

while(count != 0) {

count–;

Method* meth = &methods[count];

if (!dvmIsNativeMethod(meth)) {

continue;

}

if (dvmIsAbstractMethod(meth)) { /* avoid abstract method stubs */

continue;

}

dvmSetNativeFunc(meth, dvmResolveNativeMethod, NULL); // meth->nativeFunc重新指向dvmResolveNativeMethod

}

}

UnregisterNatives函数会把jclazz所在类的所有native方法都重新指向为dvmResolveNativeMethod,所以调用UnregisterNatives 之后不管是静态注册还是动态注册native方法、之前是否执行过,在加载补丁so的时候都会重新去做映射。

所以我们只需要调用:

static void patchNativeMethod(JNIEnv *env, jclass clz) {

env->UnregisterNatives(clz);

}

这里有一个难点,因为native方法是在so库,所以补丁工具很难检测出到底是哪个Java类需要解除native方法的注册。 这个问题暂且放下。

假设我们现在可以知道哪个具体的Java类需要解除注册native方法,然后load补丁库,再次执行该native方法,按照道理来说是可以让native方法实时生效,但是测试发现,在补丁so库重命名的前提下,Java层native方法可能映射到原so库的方法,也可能映射到补丁so库的修复后的新方法。(即时而生效,时而不生效)

首先,静态注册的native方法之前从未执行过的话或者调用了UnregisterJNINativeMethods方法解除注册,那么该方法将指向dvmResolveNativeMethod(meth->nativeFunc = dvmesolveNativeMethod),那么真正运行该方法的时候,实际上执行的是dvmResolveNative()方法。这个函数主要完成Java层的native方法和native层方法的逻辑映射。

void dvmResolveNativeMethod(const u4* args, JValue* pResult, const Method* method, Thread* self) {

void* func = lookupSharedLibMethod(method);

… …

if (func != NULL) {

// 调用lookupSharedLibMethod方法,拿到so库文件对应的native方法函数指针。

dvmUseJNIBridage((Method*) method, func);

(*method->nativeFunc)(args, pResult, method, self);

return;

}

… …

dvmThrowUnstatisfiedLinkError(“Native method not found”, method);

}

static void* lookupSharedLibMethod(const Method* method) {

return (void*) dvmHashForeach(gDvm.nativeLibs, findMethodInLib, (void*) method);

}

int dvmHashForeach(HashTable* pHashTable, HashForeachFunc func, void* arg) {

int i, val, tableSize;

tableSize = pHashTable->tableSize;

for (i = 0; i < tableSize; i++) {

HashEntry* pEnt = &pHashTable->pEntries[i];

if (pEnt->data != NULL && pEnt->data != HASH_TOMBSTONE) {

val = (*func)(pEnt->data, arg);

if (val != 0) {

return val;

}

}

}

return 0;

}

gDvm.nativeLibs是一个全局变量,它是一个HashTable,存放着整个虚拟机加载so库的SharedLib结构指针。然后该变量作为参数传递给dvmHashForeach 函数进行HashTable遍历。执行findMethodInLib函数看是否找到对应的native函数指针,如果第一个就找到,就直接return。

这个结构很重要,在虚拟机中大量使用到了HashTable这个数据结构,实现源码在dalvik/vm/Hash.hdalvik/vm/Hash.cpp文件。

有兴趣的可以自行查看源码,这里不进行详细分析,hashtable的遍历和插入都是在dvmHashTableLookup()中实现,简单说下 Java中的HashTable和c中的HashTable的不同点:

  • 共同点:两者实际上都是数组实现,都是对key进行hash计算后跟hashtable的长度进行取模作为bucket。

  • 不同点:Dalvik虚拟机下的HashTable实现要比Java中的实现简单一些。

Java中的HashTable的put操作要处理hash冲突的情况,一般情况下会在冲突节点上新增一个链表处理冲突,然后get实现会遍历链表。Dalvik下的HashTable的put操作只是简单的把指针下移到下一个空间点。get实现首先根据hash值算出bucket位置,然后比较是否一致,不一致的话,指针下移,HashTable的遍历实现就是数组遍历

知道了DVM下HashTable的实现原理,那我们再来看下前面提到的:补丁so库重命名的前提下,为什么Java层的native方法可能映射到原so库的方法,也可能映射到补丁so库修复后的新方法,如下图所示:

在这里插入图片描述

由于HashTable的实现方法以及dvmHashForeach的遍历实现,so注册位置跟文件命名hash后的bucket值有关,如果顺序靠前,那么生效的永远是最前面的,而后面一直无法生效。

可见so库实时生效方案,对于静态注册的native方法有一定的局限性,不能满足通用性。

2.3 so库实时生效方案总结


基于上面的分析,so库的实时生效方案必须满足下面几点:

  • so库为了兼容Dalvik虚拟机下动态注册native方法的实时生效,必须对so文件进行改名

  • 针对so库静态注册native方法的实时生效,首先需要解注册静态注册的native方法,这个也是难点, 因为我们很难知道so库中哪几个静态注册的native方法发生了变更。假设就算我们知道如果静态注册的native方法需要解注册,重新加载补丁so库有可能被修复,也有可能不被修复

  • 上面对补丁so进行了第二次加载,那么可能是多消耗了一次本地内存,如果补丁so库够大、够多,那么JNI层的OOM也不是没可能

  • 另一方面补丁so库新增了一个动态注册的方法而dex中没有相应方法,直接去加载这个补丁so文件会报 NoSuchMethodError异常,具体逻辑在 dvmRegisterJNIMethod中。我们知道如果dex新增了一个native方法,那么就不能热部署只能冷启动生效,所以此时so库就不能第二次加载了。这种情况下so库的修复验证依赖于dex的修复方案。

3. so库冷部署重启生效实现方案

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

为了更好的兼容通用性,我们尝试通过冷部署重新生效的角度分析下补丁so库的修复方案。

3.1 接口调用替换方案


SDK提供接口替换System默认加载so库接口:

SoPatchManager.loadLibrary(String libName) -> 代替 System.loadLibrary(String libName)

SoPatchManager.loadLibrary 接口加载so库的时候优先尝试加载 SDK指定目录下的补丁so,加载策略如下:

  1. 如果存在则加载补丁so库

  2. 如果不存在,那么调用 System.loadLibrary加载安装apk目录下的so库

在这里插入图片描述

我们可以很清楚的看到这个方案的优缺点:

  • 优点:不需要对不同SDK版本进行兼容,因为所有的SDK版本都有System.loadLibrary这个接口

  • 缺点:调用方需要替换掉System默认加载so库接口为SDK提供的接口,如果是已经编译混淆好的第三方库so需要patch,那么很难做到接口的替换。

虽然这种方案简单,但是有一定的局限性没法修复三方包的so库。

3.2 反射注入方案


前面介绍过System.loadLibrary("native-lib") 加载so库的原理,其实这个so库最终传给native方法的参数是 so库在磁盘中的完整路径。调用native层的时候参数就会包装成/data/app-lib/com.rikkatheworld.jni-2/libnative-lib.so,so库会在DexPathList.nativeLibraryDirectories/nativeLibraryPathElements变量所表示的目录下去遍历搜索。

Android SDK版本小于23时,DexPathList.findLibrary() 实现如下:

private final File[] nativeLibraryDirectories;

public String findLibrary(String libraryName) {

String fileName = System.mapLibraryName(libraryName);

for (File directory : nativeLibraryDirectories) {

String path = new File(directory, fileName).getPath();

if (IoUtils.canOpenReadOnly(path)) {

return path;

}

}

return null;

}

这里会发现遍历 nativeLibraryDirectories数组,如果找到了 IoUtils.canOpenReadOnly(path)返回true,那么就直接返回该path。

它会true的前提肯定是需要path表示so文件存在的。那么我门可以采取类似类修复反射注入方式,只要把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能够使得加载so库时,加载的是补丁so库,而不是原来so库的目录,从而达到修复的目的。

Android SDK在版本23以上时,DexPathList.findLibrary实现如下:

学习分享

在当下这个信息共享的时代,很多资源都可以在网络上找到,只取决于你愿不愿意找或是找的方法对不对了

很多朋友不是没有资料,大多都是有几十上百个G,但是杂乱无章,不知道怎么看从哪看起,甚至是看后就忘

如果大家觉得自己在网上找的资料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。

2021最新上万页的大厂面试真题

七大模块学习资料:如NDK模块开发、Android框架体系架构…

只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。

这份体系学习笔记,适应人群:
**第一,**学习知识比较碎片化,没有合理的学习路线与进阶方向。
**第二,**开发几年,不知道如何进阶更进一步,比较迷茫。
第三,到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。如果你有需要,我这里恰好有为什么,不来领取!说不定能改变你现在的状态呢!
由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示

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

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

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

*学习知识比较碎片化,没有合理的学习路线与进阶方向。

**第二,**开发几年,不知道如何进阶更进一步,比较迷茫。
第三,到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。如果你有需要,我这里恰好有为什么,不来领取!说不定能改变你现在的状态呢!
由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值