Android中so使用知识和问题总结以及插件开发过程中加载so的方案解析

Android技术篇 尼古拉斯.赵四 21272℃ 0评论

一、知识点概要

Android中有时候为了效率以及平台开发库的支持,难免会用到NDK开发,那么都会产生一个so文件,通过native方法进行调用,开发和调用步骤很简单,这里就不多说了,本文主要来介绍,我们在使用so的时候总是会出现一些常见的问题,而现在插件化开发也很普遍了,有时候插件中也会包含一些so文件,需要加载,这时候也会出现一些问题。本文就来详细总结一下这些问题出现的原因,以及解决方法,主要还是通过源码来分析。

本文主要知识点:

1、Android中so文件编译平台问题分析

2、Android中so文件的加载流程

3、Android中so文件释放流程

4、Android中so文件兼容策略

5、Android插件中加载so文件

6、Android中so文件使用的问题总结

二、涉及到的源码类

因为本文主要通过分析源码来分析so使用的知识点和问题总结,所以涉及到了很多的源码类,这里就现提供一下:

1、PackageManagerService.java
+setNativeLibraryPaths:设置应用的native库路径
+scanPackageDirtyLI:扫描包内容初始化应用信息

2、ActivityManagerService.java
+startProcessLocked:发送命令给Zygote进程启动一个虚拟机

3、NativeLibraryHelper.java

底层实现类:com_android_internal_content_NativeLibraryHelper.cpp

+copyNativeBinariesWithOverride:释放apk中的so文件到本地目录

+findSupportedAbi:遍历apk中的so文件结合abiList值得到应用支持的abi类型索引值

4、LoadApk类和ApplicationLoaders类

5、VMRuntime.java

底层实现类:dalvik_system_VMRuntime.c

+getInstructionSet:获取虚拟机的指令集类型

+is64BitAbi:判断VM是否为64位

6、Runtime.java

底层实现类:dalvik/vm/native/java_lang_Runtime.cpp,dalvik/vm/Native.cpp

+nativeLoad:加载so文件

三、so文件的编译平台

Android中在进行NDK开发的时候,都知道因为机型杂而多的原因,没有一个大的标准,所以很多厂商都会采用不同型号的cpu,那么在编译so文件的时候,就需要进行交叉编译出多个cpu平台版本,现在主流的cpu架构版本:

armeabi/armeabi-v7a:这个架构是arm类型的,主要用于Android4.0之后的,cpu值32位的

x86/x86_64:这个架构是x86类型的,有32位和64位,占用的设备比例比较小

arm64-v8:这个架构是arm类型,主要用于Android5.0之后,cpu是64位的

这里可以看到,其中arm类型的是往下兼容策略,比如arm64-v8a肯定兼容armeabi/armeabi-v7a,也就是说armeabi/armeabi-v7a架构的so文件可以用在arm64-v8a的设备中的,而armeabi-v7a也是兼容armeabi的,但是因为cpu型号不同,所以arm体系和x86体系之间是不能相互兼容的。

四、so加载流程分析

在Android中如果想使用so的话,首先得先加载,加载现在主要有两种方法,一种是直接System.loadLibrary方法加载工程中的libs目录下的默认so文件,这里的加载文件名是xxx,而整个so的文件名为:libxxx.so。还有一种是加载指定目录下的so文件,使用System.load方法,这里需要加载的文件名是全路径,比如:xxx/xxx/libxxx.so。上面的两种加载方式,在大部分场景中用到的都是第一种方式,而第二种方式用的比较多的就是在插件中加载so文件了。不管是第一种方式还是第二种方式,其实到最后都是调用了Runtime.java类的加载方法doLoad:

这里会先从类加载中获取到nativeLib路径,然后在调用native方法nativeLoad(java_lang_Runtime.cpp)

这里调用了一个核心的方法dvmLoadNativeCode(dalvik/vm/Native.cpp)

注意:

这里有一个检测异常的代码,而这个错误,是我们在使用插件开发加载so的时候可能会遇到的错误,比如现在我们使用DexClassLoader类去加载插件,但是因为我们为了插件能够实时更新,所以每次都会赋值新的DexClassLoader对象,但是第一次加载so文件到内存中了,这时候退出程序,但是没有真正意义上的退出,只是关闭了Activity了,这时候再次启动又会赋值新的加载器对象,那么原先so已经加载到内存中了,但是这时候是新的类加载器那么就报错了,解决办法其实很简单,主要有两种方式:

第一种方式:在退出程序的时候采用真正意义上的退出,比如调用System.exit(0)方法,这时候进程被杀了,加载到内存的so也就被释放了,那么下次赋值新的类加载就在此加载so到内存了,

第二种方式:就是全局定义一个static类型的类加载DexClassLoader也是可以的,因为static类型是保存在当前进程中,如果进程没有被杀就一直存在这个对象,下次进入程序的时候判断当前类加载器是否为null,如果不为null就不要赋值了,但是这个方法有一个弊端就是类加载器没有从新赋值,如果插件这时候更新了,但是还是使用之前的加载器,那么新插件将不会进行加载。

继续往下看:

这里主要调用了两个核心的系统方法,dlopen和dlsym,这两个方法用途还是很多的,一般是先加载so文件,然后得到指定函数的指针,最后直接调用即可,主要用于调用动态的调用so中的指定函数功能。而且这里注意到了最开始先调用so中的JNI_OnLoad函数,这个函数是so被加载之后调用的第一个方法。

到这里我们就总结一下Android中加载so的流程:

1、调用System.loadLibrary和System.load方法进行加载so文件

2、通过Runtime.java类的nativeLoad方法进行最终调用,这里需要通过类加载器获取到nativeLib路径。

3、到底层之后,就开始使用dlopen方法加载so文件,然后使用dlsym方法调用JNI_OnLoad方法,最终开始了so的执行。

五、类加载器关联so路径

上面分析so加载过程中可以发现有一个地方,就是通过类加载器来获取到so的路径,那么Android中的主要类加载器有两个,一个是PathClassLoader和DexClassLoader,关于这两个类加载不多说了,网上资料很多可以自行查找阅读。而PathClassLoader是我们Android中默认的类加载器,也就是apk文件就是由他来加载的,我们可以通过查看源码得知,Android中加载apk的类加载可以从LoadApk.java类查找到:

注意:

这个类很重要的,而这个类加载器也是我们在做插件的时候,需要做一些操作,比如需要把加载插件的DexClassLoader类给添加到这个系统加载器中,就可以解决插件中组件的生命周期问题。

看看这个类加载器在哪里赋值的:

去看看ApplicationLoaders.java类:

看到了,这里就是定义了PathClassLoader类了,所以我们Android中应用的默认加载器是PathClassLoader,再去看看这个类加载器的nativeLib是哪里:

六、so文件如何释放

我们在使用System.loadLibrary加载so的时候,传递的是so文件的libxxx.so中的xxx部分,那么系统是如何找到这个so文件然后进行加载的呢?这个就要先从apk文件安装时机说起。

我们如果还没有分析源码之前,大致能够猜想到的流程是:

在安装apk的时候,系统解析apk文件,因为so文件肯定是存放在libs下指定平台目录中的,而apk文件本身就是一个压缩文件,所以可以进行解压,然后读取libs目录下的so文件,进行本地释放解压到指定目录,然后在加载的时候就先拼接so文件的全路径,最后在进行加载工作即可。

通过猜想,下面就通过源码来分析一下流程,系统在安装apk的时候,是调用系统类:PackageManagerService.java类:

主要的核心方法是scanPackageDirtyLI:

这个方法主要通过传递的pkg变量,开始构造applicationInfo信息。我们往下面看,找到设置nativeLib信息的代码:

这里注意有一个判断,是不是多平台架构的应用:

所以,我们看看info.flags有没有设置这个标志,我们看到上面的pkg变量是通过解析apk文件的类PackageParser.java类中获取到的,所以可以去这个类中找这个标志位的设置。

这里看到了,如果在AndroidManifest.xml中设置了Application中的multiArch属性值的话就有,但是我们默认都没有设置这个属性值,那么就是false,也就是说一般应用都不是多平台的。所以上面的isMultiArch方法就返回false,代码就走到了这里:

在这里就有很多知识点了,而这里可以看到,就涉及到了so文件的释放工作了,主要是在NativeLibraryHelper类中,但是这里看到首先获取abiList值:

通过Build.SUPPORTED_ABIS来获取到的:

最终是通过获取系统属性:ro.product.cpu.abilist的值来得到的,我们可以使用getprop命令来查看这个属性值:

这里获取到的值是:arm64-v8a,armeabi-v7a,armeabi,我用的是64位的cpu设备,所以可以看到他有多个cpu架构可选,而且看到这个顺序会想到,这个顺序正好是向下兼容的顺序。

现在去看看NativeLibraryHelper类的copyNativeBinariesForSupportedAbi方法:

这个方法中主要干了三件事:

第一件事是获取应用所支持的arch架构类型

第二件事是通过架构类型获取so释放的目录

第三件事是native层中释放apk中的指定架构的so到设备目录中

第一件事:获取应用所支持的arch架构类型

NativeLibraryHelper类的findSupportedAbi方法,其实这个方法就是查找系统当前支持的架构型号索引值:

看看native方法的实现:

这里看到了,会先读取apk文件,然后遍历apk文件中的so文件,得到全路径然后在和传递进来的abiList进行比较,得到合适的索引值,其实实现逻辑很简单:abiList是:arm64-v8a,armeabi-v7a,armeabi,然后就开始比例apk中有没有这些架构平台的so文件,如果有,就直接返回abiList中的索引值即可,比如说apk中的libs结构如下:

那么这时候返回来的索引值就是0,代表的是arm64-v8a架构的。如果apk文件中没有arm64-v8a目录的话,那么就返回1,代表的是armeabi-v7a架构的。依次类推。得到应用支持的架构索引之后就可以获取so释放到设备中的目录了。

第二件事:获取so释放之后的目录

这里主要通过VMRuntime.java中的getInstructionSet方法:

这里调用了一个map结构值:

这里的arch架构和目录对应关系,如果arch是arm64-v8a的话,那么目录就是arm64了。

第三件事:释放apk中的so文件

直接调用的是native层方法iterateOverNativeFiles:

好了到这里就讲完了上面的三件事了,而这三件事做完之后,apk中的so文件就会被释放到本地设备中的指定目录中了,当然这里系统会根据abiList中的值以及apk中包含的arch类型的so来决定释放哪个目录中的so文件,比如这里通过ApplicationInfo类来打印当前应用的nativeLibraryDir值:

打印的结果:

看到了,因为是arm64-v8a类型的,所以目录是arm64的,而且可以看到这个应用不是多平台的。

我们可以看到Android中是如何释放apk中的so文件到本地目录的:

1、通过遍历apk文件中的so文件的全路径,然后和系统的abiList中的类型值进行比较,如果匹配到了就返回arch类型的索引值

2、得到了应用所支持的arch类型之后,就开始获取创建本地释放so的目录

3、然后开始释放so文件

我们在PackageMangerService类中继续往下看:

这里还要保存上面获取到应用支持的arch类型值,我们可以使用反射打印这个值:

打印结果:

这个值在后面应用创建VM的时候会用到。

接着开始设置应用的nativeLib路径了:

看看这个方法的实现:

这里先判断是不是64位:

通过arch类型对应的目录来判断的:

这里如果是64位,目录就是lib,如果是32位就是lib64:

这样就和我们上面释放so文件的目录保持一致了,所以这里的ApplicationInfo类中的lib路径就是我们上面释放so之后的路径了。

在之前说到了类加载器中的lib路径,我们可以打印一下库路径的,这里直接使用getClassLoader得到加载器打印即可:

这里看到Library的目录包含很多路径。

七、64位系统兼容32位的so

上面分析完了,so文件的释放工作,下面继续来看一下如果一个64位系统的Android设备如何做到能够运行32位的so文件,这个就需要从应用的启动说起了,那么这个类就是ActivityManagerService.java,有一个核心的方法:startProcessLocked,这个方法就是向Zygote进程发送一个消息,为这个应用创建虚拟机开始运行程序了:

这里在发送消息给Zygote进程,看到这里通过ApplicationInfo中的primaryCpuAbi类型告诉Zygote改创建多少位的虚拟机,我们查看系统启动文件init.rc内容:

这里会启动一个64位的Zygote进程

然后启动一个32位的Zygot进程

所以这里应该就可以想明白了,原来系统启动的时候,如果是64位的系统设备,会启动两个Zygote进程用来兼容32位类型的应用,我们可以使用ps命令查看进程:

看到了,这里果然启动了两个Zygote进程,一个64位的,一个是32位的。所以兼容功能的大致流程图应该是这样的:

上层启动应用的时候会把应用的abi类型带过来,然后这里会根据这个类型发送给具体的Zygote进程消息,来创建虚拟机开始运行程序,这样就做到了兼容。

八、插件中如何加载so文件

有时候我们在开发插件的时候,可能会调用so文件,一般来说有两种方案:

一种是在加载插件的时候,先把插件中的so文件释放到本地目录,然后在把目录设置到DexClassLoader类加载器的nativeLib中。

一种在插件初始化的时候,释放插件中的so文件到本地目录,然后使用System.load方法去全路径加载so文件

这两种方式的区别在于:第一种方式的代码逻辑放在了宿主工程中,同时so文件可以放在插件的任意目录中,然后在解压插件文件找到这个so文件释放即可。第二种方式的代码逻辑是放在了插件中,同时so文件只能放在插件的assets目录中,然后通过把插件文件设置到程序的AssetManager中,最后通过访问assets中的so文件进行释放。

上面就全部分析完了Android中关于so加载的相关内容:

1、so编译平台问题

2、so加载流程分析

3、so文件释放功能分析

4、so文件兼容功能分析

5、插件中so文件调用功能分析

九、常见问题分析

第一个问题:Could not find libxxx.so

这个问题看上去很好理解,就是在调用加载so的方法的时候,到底层使用dlopen方法打开so文件,发现找不到这个so文件,那么这个问题产生的原因主要有两个:

第一个是我们的确忘了在工程的libs下存放so文件了。

第二个是我们把so文件放错目录了。

第一个原因就不多说了,主要来看第二原因:

有时候我们在开发项目的时候,可能会放多个架构类型的so文件,那么现在假如我的设备是arm64-v8a类型的,我的项目中有三个so文件,比如叫做AAA.so,BBB.so,CCC.so,然后我再arm64-v8a目录中放了AAA.so,BBB.so,而CCC.so忘了放了,但是会放到armeabi-v7a和armeabi目录中,那么这时候就会发生找不到CCC.so的错误,原因很简单:

上面分析了apk中so文件的释放逻辑,系统会先遍历apk中所有so文件的全路径,然后在结合abiList的值来决定最终释放哪个目录中的so文件,那么现在系统是arm64-v8a了,而apk中的libs下也有arm64-v8a,所以这里就会把apk中的libs\arm64-v8a中的所有so文件释放解压到本地目录中,而不会在去释放armeabi/armeabi-v7a了。因为arm64-v8a中没有CCC.so文件,所以最终释放到本地目录中也是没有这个so文件的,所以加载时找不到文件了。

解决办法:就是在使用so文件的时候,需要确定在每个架构类型目录中都要有相同的so文件即可。

第二个问题:32-bit instead of 64-bit

这个问题的原因主要是因为64位的Zygote进程创建的虚拟机中加载了32位的so文件,这个问题的产生原因主要有两个:

第一个是我们把不同架构类型的so文件放错目录了,比如armeabi/armeabi-v7a的so文件放到了arm64-v8a中了。

第二个是我们在开发插件的过程中,宿主工程中有arm64-v8a目录,但是插件中加载so却是armeabi/armeabi-v7a类型的。

第一个原因就不多说了,主要是因为so放错目录了,来看一下第二个原因,我们在开发插件的时候有时候需要在插件中去加载so文件,一般都是使用System.load方式去加载全路径的so文件,那么这里就可能存在一个问题,比如宿主工程中,放了所有架构的目录,包括了64位的,因为考虑插件的大小,所以在插件中只放了armeabi-v7a目录的so文件,如果设备是64位的系统,那么这时候插件加载so文件就会报错。原因就在于上面分析的so兼容问题中说到了,因为宿主工程中包含了64位的架构arm64-v8a类型,系统的abiList中也有arm64-v8a类型,所以这时候应用的ApplicationInfo的abi就是arm64-v8a了,那么就会发送消息给Zygote64的进程,创建的也是64位的虚拟机了,而最后插件中加载so的类型是32位的armeabi-v7a,那么就会报错了,因为32位的so文件不能运行在64位的虚拟机中的。

解决办法:宿主工程和插件工程中的so文件的架构类型保持一致,这个将会带来一个很大的问题,就是插件包会变得很大,因为宿主工程为了兼容多数机型,加入了多个类型的架构so文件,但是插件为了减小包大小,就放了指定类型的so文件,但是最终会存在这种问题,所以这个解决办法就要看项目需要了。

还有一个类似的问题:64-bit instead of 32-bit:

原理都是一样的,32位的虚拟机中加载了64位的so文件问题导致的。

第三个问题:Shared library already opened

这个问题在上面介绍so加载流程中已经介绍过了,原因主要是因为之前使用DexClassLoader加载so之后,so没有释放还在内存中,而在此启动有弄了一个新的DexClassLoader对象去加载so问题,就出错了。

我们使用DexClassLoader类去加载插件,但是因为我们为了插件能够实时更新,所以每次都会赋值新的DexClassLoader对象,但是第一次加载so文件到内存中了,这时候退出程序,但是没有真正意义上的退出,只是关闭了Activity了,这时候再次启动又会赋值新的加载器对象,那么原先so已经加载到内存中了,但是这时候是新的类加载器那么就报错了。

解决办法:

第一种方式:在退出程序的时候采用真正意义上的退出,比如调用System.exit(0)方法,这时候进程被杀了,加载到内存的so也就被释放了,那么下次赋值新的类加载就在此加载so到内存了。

第二种方式:就是全局定义一个static类型的类加载DexClassLoader也是可以的,因为static类型是保存在当前进程中,如果进程没有被杀就一直存在这个对象,下次进入程序的时候判断当前类加载器是否为null,如果不为null就不要赋值了,但是这个方法有一个弊端就是类加载器没有从新赋值,如果插件这时候更新了,但是还是使用之前的加载器,那么新插件将不会进行加载。

十、技术概要

本文主要介绍了Android中关于so的相关知识,主要包括so编译多架构问题,so加载流程问题,so释放问题,so系统兼容问题以及插件中加载so文件的功能解析,看完本文之后,我们需要了解到的知识点:

1、在NDK开发时,可以指定多种架构类型编译出多种类型的so文件。

2、so的加载流程主要是System类中的两个加载方法,最终都会调用Runtime中的nativeLoad的native方法,而这个native方法最终会调用dlopen来打开so文件,然后在调用dlsym方法调用so的JNI_OnLoad方法。

3、关于apk文件在安装的时候释放so文件到本地目录中,主要是结合当前设备的abiList信息(这个信息主要是通过系统属性:ro.product.cpu.abilist值来获取的)和apk中不同类型架构,来决定最终释放哪个类型目录中的so文件,释放完成之后,还需要设置应用的nativeLib路径,以及应用的abi信息,因为这个abi信息在后面启动虚拟机的时候需要用到。

4、因为现在有很多设备已经是64位系统了,但是为了兼容32位的so文件,所以这些64位系统就会在系统启动的时候创建两个Zygote进程,一个是64位的,一个是32位的,当一个应用启动的时候,需要创建虚拟机,那么这时候就会把应用的架构类型传递过去,系统会根据这个类型来交给哪个Zygote进程来处理这个应用启动事件。这样就可以做到so调用的兼容问题了。

5、插件中加载so文件现阶段主要有两种方式,一种是先释放插件中的so文件到本地目录,然后设置DexClassLoader的nativeLib路径;还有一种方式是先释放插件中的so文件,然后调用System.load来加载全局路径的so文件。

十一、问题总结

本文还总结了在使用so文件的时候,会遇到的一些问题,主要是三个问题:

1、so文件找不到问题

这个问题一般是因为我们忘记放了so文件,或者是so文件没有放置全部,也就是没有在libs目录中所有的架构类型目录中放置。

2、不同位数的虚拟机运行了不同位数的so文件

这个问题一般是因为我们在libs目录中把so文件放错目录了,或者是宿主工程和插件工程中的so文件架构类型目录没有保持一致。

3、类加载器加载so文件再次加载

这个问题一般是因为插件开发中使用了不同的DexClassLoader去加载多次相同的so文件导致的。

十二、知识延展

我们在开发的过程中有时候想知道系统的位数,那么这里网上告知说有好几种方法,其实那些都是忽悠人的,特别是在使用这个api的时候:android.os.Build.CPU_ABI,我就是在项目中被这个方法坑爹了,这个方法其实不是获取系统的位数,而是获取当前应用的架构类型位数,就是我们前面分析的ApplicationInfo中的abi信息,我们可以查看一下源码:

这里可以看到,这个字段已经被废弃了,因为他不靠谱呀,这个字段在Build类的static块中进行赋值的:

这里会通过VMRuntime类的is64Bit方法来判断当前虚拟机的位数,来获取这个值

这里还有两个系统属性:

ro.product.cpu.abilist32是32位的所有arch架构类型

ro.product.cpu.abilist64是64位的所有arch架构类型

而这两个字段值的合集就是前面的ro.product.cpu.abilist属性值。

而VMRuntime的is64Bit方法是native方法,实现如下:

看到了,这里得到的是虚拟机的位数,那么就是上面的Zygote进程的位数了。那么问题就来了,假如我的设备是64位的,但是我的项目中没有arm64-v8a类型的so文件,这时候在解析apk进行释放so文件的时候,就会得知架构类型是armeabi/armeabi-v7a了,因为遍历apk文件,没有找到arm64-v8a类型的so文件,这时候应用的abi类型就是armeabi/armeabi-v7a了,这就是32位的了,就会通知32位的Zygote进程创建了一个32位的虚拟机,那么此时我的项目中通过Build.CPU_ABI得到的系统位数就是32了,那么完全不是我们想要的了。

所以正确的获取系统位数的方法是:

Android5.0系统之后,可以通过ro.product.cpu.abilist属性字段值来判断,如果这个字段值中包含了64的话,那么就是64位系统了

Android5.0系统之前,需要通过ro.product.cpu.abi属性字段值来判断,不过5.0系统之前都是32位的,还没有出现64位呢。

十三、选择适当架构类型

我们上面分析之后可以看到,如果想做到万无一失即,项目不报错,而且so运行效率也是非常高的话,就需要把那几个架构类型的so文件都要在项目中放一遍,那么这个问题就来了,如果so文件较大的话,apk包最终也是很大的,所以这里就需要做一次选择了。

1、我们在开发一个项目的时候因为,整个项目的so文件结构我们可以控制,所以为了防止apk包增大,我们可以考虑只放几个架构类型的so文件,比如最好的是放armeabi类型的,因为首先现在大部分设备采用cpu型号都是arm的,少数采用x86或者是mips类型的,其次是防止了armeabi类型之后,对于armeabi-v7a和arm64-v8a就可以兼容了,不会存在报错问题。但是因为系统需要兼容所以就会出现so运行效率的问题了,最好的效率就是指定架构类型的so运行在对应架构类型的设备中。因为现在大部分的设备系统版本都是4.0以上了,所以armeabi-v7a架构类型用的比较多了,所以有时候为了效率问题,项目中只放了这个架构类型的so文件,那么像老版本的手机armeabi的话就会报错了,当然这个错误是可以接受的即可。

2、有时候像x86和mips等少数类型架构的设备,开发程序的时候会单独出一个版本比如叫做xxx应用x86版本

3、在开发SDK的时候,因为开发之后的SDK包是给其他app接入的,而对于接入的app,我们不能做太多的限制,所以理论上应该把所有架构类型的so都要提供,这样给需要接入的app进行选择即可,比如像百度地图SDK:

本文的目的只有一个就是学习更多的逆向技巧和思路,如果有人利用本文技术去进行非法商业获取利益带来的法律责任都是操作者自己承担,和本文以及作者没关系,本文涉及到的代码项目可以去编码美丽小密圈自取,欢迎加入小密圈一起学习探讨技术

十四、总结

本文主要是介绍了Android中关于so的相关知识,而这些知识点都是在使用so文件中会经常用到的,同时一些问题也是我们会遇到的,这里只是做了一个总结,同时也给出了插件中加载so文件的方案已经遇到的问题解决思路等内容。

《Android应用安全防护和逆向分析》

点击立即购买:京东  天猫  

更多内容:点击这里

关注微信公众号,最新技术干货实时推送

转载请注明:尼古拉斯.赵四 » Android中so使用知识和问题总结以及插件开发过程中加载so的方案解析

喜欢 (18)or分享 (0)
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
(2)个小伙伴在吐槽
  1. 之前详细看过一次,奶蛋,记忆力实在太差了!看过就忘。刚客户报了一个问题。发现是公司将32位的so放到了arm64-v8a目录下。能不出错么。瞬间就想到这篇文章了。。。。立马回来再看一遍!公司之前没考虑arm64-v8a,却有考虑占用的设备比例比较小的x86;我也是醉-_-|||
    lucy2016-10-10 18:41 回复
  2. 又回来了!师父 x86s设备是兼容armebi版.so的呀!
    lucy2016-10-27 16:27 回复