当前位置: 首页 > 科技观察

使用SO库进行Android动态加载需要注意的一些问题

时间:2023-03-13 06:09:06 科技观察

基本信息作者:kaedea项目:android-dynamical-loadingAndroid项目中的SO库恰好是动态加载的。说说使用SO库需要注意的一些问题。或许这些问题对于经常和SO库开发打交道的同学来说是老生常谈了,但是既然要讨论一整套动态加载,我觉得还是有必要谈谈使用SO库时的一些问题。在项目中使用SO库非常简单。在SD卡中加载SO库也有提到。你只需要将你需要的SO库复制到jniLibs(或者Eclipse工程中的libs)中,然后在JAVA代码中调用System.loadLibrary(“xxx”)加载对应的SO库,使用JNI语句即可调用SO库中的Native方法。不过有同学注意到,SO库文件的文件名可以随意更改,但是文件夹路径不能随意更改。相反,“armeabi”、“armeabi-v7a”和“x86”等文件夹名称有严格的要求。这些文件夹的名字是什么意思?SO库类型和CPU架构类型的原因很简单。不同CPU架构的设备需要使用不同类型的SO库(从文件名也可以猜到╮( ̄▽ ̄")╭)。还记得我还在上学的时候,一提到ARM处理器,老师就说说以后移动设备的CPU基本都是ARM类型的,老师没骗我,早期的Android系统几乎只支持ARMCPU架构,现在至少支持以下七种,三种不同的CPU架构:ARMv5、ARMv7、x86、MIPS、ARMv8、MIPS64和x86_64。每种CPU类型对应一个ABI(ApplicationBinaryInterface),“armeabi-v7a”文件夹前面的“armeabi”指的是它的ABI此类型的ARM,后面的“v7a”是指ARMv7。这7种CPU类型对应的SO库的文件夹名称分别为:armeabi、armeabi-v7a、x86、mips、arm64-v8a、mips64、x86_64。不同类型的移动设备在运行APP时需要加载自己支持的SO库类型,否则会GG。我们可以通过Build.SUPPORTED_ABIS判断当前设备支持的ABI,但一般情况下开发者不需要自己判断ABI。Android系统在安装APK时,不会安装APK中所有的SO库文件,而是会根据当前CPU类型支持的ABI,从APK中复制出最合适的SO库,保存在libs下在APP的内部存储路径中。(这里说一般情况是因为有例外,比如我们动态加载外部SO库时,需要自己判断ABI类型。)= 对应类型的SO库到这里,我们发现SO库的使用逻辑比较简单,但是Android系统加载SO库的逻辑还是给我们留下了一些坑。使用SO库需要注意的一些问题1.不要把SO库放错地方。SO库实际上是在APP运行的时候加载的,也就是说APP只有在运行的时候才知道SO库文件的存在,无法传递。编译APP时静态代码检查或检查SO库文件是否正常。所以Android开发对SO库的存放路径有着严格的要求。在使用SO库时,除了“armeabi-v7a”等文件夹名称需要严格按照规定来命名外,在工程中应该放置SO库的文件夹也必须按套路来。以下是一些总结:AndroidStudio项目放置在jniLibs/xxxabi目录下(当然你也可以通过在build.gradle文件中设置jniLibs.srcDir属性来自己指定);Eclipse项目放在libs/xxxabi目录下(这也是使用ndk-build命令生成SO库的默认目录);aar依赖包位于jni/ABI目录下(SO库会自动包含在引用AAR压缩包到APK中);在最终构建的APK文件中,SO库位于lib/xxxabi目录下(即无论你用什么方式构建,只要APK包中SO库的路径正确,就不会出现问题);通过PackageManager安装后,在Android5.0以下的系统中,SO库位于APP的nativeLibraryPath目录下;在系统大于等于Android5.0系统中,SO库位于APP的nativeLibraryRootDir/CPU_ARCH目录下;顺便说一句,我在使用AndroidStudio1.5构建APK时,发现Gradle插件默认只会将SO打包到applicationtypemodule的jniLibs下。库文件,不会打包aar依赖包的SO库,所以最终构建的APK中的SO库文件会丢失。临时的解决办法是把所有的SO库都放在application模块中(这显然不是一个好的解决方案)。不知道是不是Studio的bug。我同事的解决方案是通过修改package的SO库的GradlepluginPackaging支持来增加对aar的依赖(GitHub有开源的第三方Gradle插件项目,有Java和Groovy语言开发)。2、尽可能提供CPU支持的最优SO库。当应用程序安装到设备上时,只会安装该设备支持的CPU架构对应的SO库。然而,有时,一个设备支持不止一种类型的SO库。例如,大部分X86设备不仅支持X86类型的SO库,而且还兼容ARM类型的SO库(目前应用市场上大部分APP只兼容ARM类型如果X86类型设备的SO库是不兼容ARM类型的SO库,估计会是个屁)。所以如果你的APK只适配了ARM类型的SO库,在兼容模式下仍然可以在X86类型的设备(如华硕平板)上运行,但这并不意味着你不需要适配X86型SO库。library,因为X86CPU在兼容模式下运行ARM类型的SO库会异常卡死(试着回忆一下几年前刚开始学习Android开发时在PC上使用AVD模拟器的感觉)。3、注意SO库的编译版本除了要注意使用正确CPU类型的SO库外,还应该注意SO库的编译版本。虽然现在的AndroidStudio支持直接在项目中编译SO库,但更多时候我们还是选择使用预编译好的SO库。这个时候就要注意了。在编译APK的时候,我们总是希望使用最新版本的build-tools来编译,因为最新版本的AndroidSDK会帮助我们做到最好的向后兼容。但是编译SO库就不一样了,因为NDK平台不是向下兼容的,而是向上兼容的。您应该使用应用的minSdkVersion对应的NDK示例版本来编译SO库文件。如果使用过高版本的NDK,可能会导致APP性能不佳,或者导致一些与SO库相关的运行时异常,如“UnsatisfiedLinkError”、“dlopen:failed”等类型的Crash。一般我们使用的是编译好的SO库文件,所以在导入预编译的SO库的时候,需要检查编译的平台版本。4、尽量为每种CPU类型提供对应的SO库。比如有时候,因为业务需要,我们的APP不需要支持AMR64设备,但这并不意味着我们不需要编译ARM64对应的SO库。比如我们的APP只支持armeabi-v7a和x86架构,然后我们的APP使用了一个第三方的Library,这个Library提供了对AMR64等更多类型CPU架构的支持。在构建APK的时候,这些ARM64的SO库还是会被打包到APK中,也就是说我们自己的SO库是没有对应的ARM64的SO库的,但是第三方的库是有的。这时候一些ARM64设备在安装APK的时候,发现我们的APK里面有ARM64的SO库,就会误认为我们的APP已经做了AMR64的适配工作,所以只会选择安装ARM64类型的APK。这样会导致我们自己项目的SO库无法正确安装(虽然apk包中确实存在armeabi-v7a和x86类型的SO库)。此时正确的做法是为自己的SO库提供AMR64支持,或者不封装第三方Library项目的ARM64SO库。使用第二种方案时,可以删除APK中不需要支持的ABI文件夹,然后重新打包。在AndroidStudio下,可以通过以下构造方法指定需要的SO库类型。productFlavors{flavor1{ndk{abiFilters"armeabi-v7a"abiFilters"x86"abiFilters"armeabi"}}flavor2{ndk{abiFilters"armeabi-v7a"abiFilters"x86"abiFilters"armeabi"abiFilters"arm64-v8a"abiFilters"x86_64"}}}需要注意的是,如果我们的项目是SDK项目,最好提供所有平台的SO库支持,因为APP能支持的设备CPU类型数是所有SO库支持的最小CPU类型项目(使用我们SDK的APP支持的CPU类型只能小于等于我们SDK支持的类型)。5.不要通过“减少其他CPU类型支持的SO库”来减小APK大小的确,所有x86/x86_64/armeabi-v7a/arm64-v8a设备都支持armeabi架构的SO库,所以似乎去掉其他ABI的SO库是一个减少APK大小的好方法。但实际上并非如此,这不仅影响了函数库的性能和兼容性。X86设备可以很好的运行ARM类型的函数库,但不能保证100%不会出现死机,尤其是老设备,兼容性只是保证。64位设备(arm64-v8a、x86_64、mips64)可以运行32位函数库,但运行在32位模式下。在64位平台上运行32位版本的ART和Android组件将失去64位优化的性能(ART、webview、media等)。通过减少其他CPU类型支持的SO库来减少APK的大小并不是很明智。如果真的需要通过减少SO库来减小APK的大小,我们也有其他的方法。减少SO库大小的正确姿势1.构建一个支持特定ABI的APK我们可以构建一个支持所有CPU类型的APK。但反过来,我们可以为每种CPU类型构建一个单独的APK,然后在不同CPU类型的设备上安装相应的APK。当然,前提是应用市场必须对不同CPU类型的用户设备提供支持。目前来说,至少PLAY市场是支持的。Gradle可以通过如下配置生成不同ABI支持的APK(参考其他文章,未实际使用):android{...splits{abi{enabletruereset()include'x86','x86_64','armeabi-v7a','arm64-v8a'//选择ABIS为通用Apktrue构建APK//生成包含所有ABI的附加APK}}//映射版本代码project.ext.versionCodes=['armeabi':1,'armeabi-v7a':2,'arm64-v8a':3,'mips':5,'mips64':6,'x86':8,'x86_64':9]android.applicationVariants.all{variant->//assigndifferentversioncodeforeachoutputvariant.outputs.each{output->output.versionCodeOverride=project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI),0)*1000000+android.defaultConfig.versionCode}}}2.从网络上下载当前设备支持的SO库说到这里终于回来了到动态加载主题。⊙﹏⊙利用Android的动态加载技术,可以加载外部的SO库,所以我们可以从网上下载SO库文件并加载。我们可以下载所有类型的SO库文件,然后加载相应类型的SO库,也可以下载相应类型的SO库并加载,但是无论哪种方式,我们最好在加载之前检查SO库文件所以图书馆。对类型进行判断。我个人的解决办法是,服务器上存放的SO库还是按照APK包的压缩方式打包,即SO库放在APK包的libs/xxxabi路径下。下载带有SO库的APK包后,我们可以遍历libs路径下的所有SO库,选择加载对应类型的SO库。具体实现代码如下:/***复制一个SO库到指定路径,首先检查修改后的SO库是否兼容当前CPU**@paramsourceDirSO库目录*@paramsoSO库名*@paramdestDirtarget根目录*@paramnativeLibName目标SO库目录名*@return*/publicstaticbooleancopySoLib(FilesourceDir,Stringso,StringdestDir,StringnativeLibName)throwsIOException{booleanisSuccess=false;try{LogUtil.d(TAG,[copySo]开始处理so文件");if(Build.VERSION.SDK_INT>=21){String[]abis=Build.SUPPORTED_ABIS;if(abis!=null){for(Stringabi:abis){LogUtil.d(TAG,"[copySo]trysupportedabi:"+abi);Stringname="lib"+File.separator+abi+File.separator+so;FilesourceFile=newFile(sourceDir,name);if(sourceFile.exists()){LogUtil.i(TAG,"[copySo]copyso:"+sourceFile.getAbsolutePath());isSuccess=FileUtil.copyFile(sourceFile.getAbsolutePath(),destDir+File.separator+nativeLibName+File.separator+so);//api2164位系统目录可能有点different//copyFile(sourceFile.getAbsolutePath(),destDir+File.separator+name);break;}}}else{LogUtil.e(TAG,[copySo]getabis==null");}}else{LogUtil.d(TAG,[copySo]supportedapi:"+Build.CPU_ABI+""+Build.CPU_ABI2);Stringname="lib"+File.separator+Build.CPU_ABI+File.separator+so;FilesourceFile=newFile(sourceDir,name);如果(!sourceFile.exists()&&Build.CPU_ABI2!=null){name="lib"+File.separator+Build.CPU_ABI2+File.separator+so;sourceFile=newFile(sourceDir,name);if(!sourceFile.exists()){name="lib"+File.separator+"armeabi"+File.separator+so;sourceFile=newFile(sourceDir,name);}}if(sourceFile.exists()){LogUtil.i(TAG,“[copySo]copyso:”+sourceFile.getAbsolutePath());isSuccess=FileUtil.copyFile(sourceFile.getAbsolutePath(),destDir+File.separator+nativeLibName+File.separator+so);}}if(!isSuccess){LogUtil.e(TAG,[copySo]安装"+so+"失败:NO_MATCHING_ABIS");thrownewIOException("install"+so+"fail:NO_MATCHING_ABIS");}}catch(IOExceptione){e.printStackTrace();throwe;}returntrue;}总结一种CPU架构=一种ABI=一种对应的SO库;加载SO库时,需要加载对应类型的SO库;尝试在所有平台上提供对CPU类型的SO库支持;抛开话题,使用SO库本身就是最纯粹的动态加载技术,SO库本身不参与APK的编译使用JNI调用SO库中的Native方法的方法看起来也像是一种“硬编程”。Native方法看起来和一般的Java静态方法没什么区别,但是它的具体实现是可以随时动态替换的。(替换SO库即可),这个也可以用来实现热修复方案。与Java方法一旦加载到内存就无法替换不同,Native方法可以在不重启APP的情况下随意替换。出于安全和生态控制的考虑,GooglePlay市场不允许应用程序加载外部可执行文件。一旦你的APK被检查出额外的可执行文件,那就不好玩了,所以现在很多应用偷偷使用动态加载的可执行文件。文件的后缀改为“.so”,减少被发现的几率,因为加载SO库好像是动态加载官方正版(不然SO库怎么工作),虽然看起来这样做有点欺骗。