游戏电视苹果数码历史美丽
投稿投诉
美丽时装
彩妆资讯
历史明星
乐活安卓
数码常识
驾车健康
苹果问答
网络发型
电视车载
室内电影
游戏科学
音乐整形

看完这篇,帮你彻底搞懂Android动态加载so

  作者:Pika
  对于一个普通的android应用来说,so库的占比通常都是居高不下的,因为我们无可避免的在开发中遇到各种各样需要用到native的需求,所以so库的动态化可以减少极大的包体积,自从2020腾讯的bugly团队发布关于动态化so的相关文章后,已经过去两年了,经过两年的考验,实际上so动态加载也是非常成熟的一项技术了,但是很遗憾,许多公司都还没有这方面的涉略又或者说不知道从哪里开始进行,因为so动态其实涉及到下载,so版本管理,动态加载实现等多方面,我们不妨抛开这些额外的东西,从最本质的so动态加载出发吧!这里是本次的例子,我把它命名为sillyboy,欢迎pr还有后续点赞呀!
  https:cloud。tencent。comdeveloperarticle1592672
  https:github。comTestPlanBSillyBoy一、so动态加载介绍
  动态加载,其实就是把我们的so库在打包成apk的时候剔除,在合适的时候通过网络包下载的方式,通过一些手段,在运行的时候进行分离加载的过程。这里涉及到下载器,还有下载后的版本管理等等确保一个so库被正确的加载等过程,在这里,我们不讨论这些辅助的流程,我们看下怎么实现一个最简单的加载流程。
  1、从一个例子出发
  我们构建一个native工程,然后在里面编入如下内容,下面是cmake。FormoreinformationaboutusingCMakewithAndroidStudio,readthedocumentation:https:d。android。comstudioprojectsaddnativecode。htmlSetstheminimumversionofCMakerequiredtobuildthenativelibrary。cmakeminimumrequired(VERSION3。18。1)Declaresandnamestheproject。project(nativecpp)Createsandnamesalibrary,setsitaseitherSTATICorSHARED,andprovidestherelativepathstoitssourcecode。Youcandefinemultiplelibraries,andCMakebuildsthemforyou。GradleautomaticallypackagessharedlibrarieswithyourAPK。addlibrary(Setsthenameofthelibrary。nativecppSetsthelibraryasasharedlibrary。SHAREDProvidesarelativepathtoyoursourcefile(s)。nativelib。cpp)addlibrary(nativecpptwoSHAREDtest。cpp)Searchesforaspecifiedprebuiltlibraryandstoresthepathasavariable。BecauseCMakeincludessystemlibrariesinthesearchpathbydefault,youonlyneedtospecifythenameofthepublicNDKlibraryyouwanttoadd。CMakeverifiesthatthelibraryexistsbeforecompletingitsbuild。findlibrary(Setsthenameofthepathvariable。loglibSpecifiesthenameoftheNDKlibrarythatyouwantCMaketolocate。log)SpecifieslibrariesCMakeshouldlinktoyourtargetlibrary。Youcanlinkmultiplelibraries,suchaslibrariesyoudefineinthisbuildscript,prebuiltthirdpartylibraries,orsystemlibraries。targetlinklibraries(Specifiesthetargetlibrary。nativecppLinksthetargetlibrarytotheloglibraryincludedintheNDK。{loglib})targetlinklibraries(Specifiesthetargetlibrary。nativecpptwoLinksthetargetlibrarytotheloglibraryincludedintheNDK。nativecpp{loglib})
  可以看到,我们生成了两个so库一个是nativecpp,还有一个是nativecpptwo(为什么要两个呢?我们可以继续看下文)这里也给出最关键的test。cpp代码。includejni。hincludestringincludeexternCJNIEXPORTvoidJNICALLJavacomexamplenativecppMainActivityclickTest(JNIEnvenv,jobjectthiz){在这里打印一句话androidlogprint(ANDROIDLOGINFO,hello,native层方法);}
  很简单,就一个native方法,打印一个log即可,我们就可以在javakotin层进行方法调用了,即:publicnativevoidclickTest();
  2、so库检索与删除
  要实现so的动态加载,那最起码是要知道本项目过程中涉及到哪些so吧!不用担心,我们gradle构建的时候,就已经提供了相应的构建过程,即构建的task【mergeDebugNativeLibs】,在这个过程中,会把一个project里面的所有native库进行一个收集的过程,紧接着task【stripDebugDebugSymbols】是一个符号表清除过程,如果了解native开发的朋友很容易就知道,这就是一个减少so体积的一个过程,我们不在这里详述。所以我们很容易想到,我们只要在这两个task中插入一个自定义的task,用于遍历和删除就可以实现so的删除化了,所以就很容易写出这样的代码。ext{deleteSoName〔libnativecpptwo。so,libnativecpp。so〕}这个是初始化配置执行阶段中,配置阶段执行的任务之一,完成afterEvaluate就可以得到所有的tasks,从而可以在里面插入我们定制化的数据task(dynamicSo){}。doLast{println(dynamicSoinsert!!!!)projectDir在哪个project下面,projectDir就是哪个路径print(getRootProject()。findAll())deffilenewFile({projectDir}buildintermediatesmergednativelibsdebugoutlib)默认删除所有的so库if(file。exists()){file。listFiles()。each{if(it。isDirectory()){it。listFiles()。each{targetprint(file{target。name})defcompareNametarget。namedeleteSoName。each{if(compareName。contains(it)){target。delete()}}}}}}else{print(nil)}}afterEvaluate{print(dynamicSotaskstart)defcustomertasks。findByName(dynamicSo)defmergetasks。findByName(mergeDebugNativeLibs)defstriptasks。findByName(stripDebugDebugSymbols)if(merge!nullstrip!null){customer。mustRunAfter(merge)strip。dependsOn(customer)}}
  可以看到,我们定义了一个自定义taskdynamicSo,它的执行是在afterEvaluate中定义的,并且依赖于mergeDebugNativeLibs,而stripDebugDebugSymbols就依赖于我们生成的dynamicSo,达到了一个插入操作。那么为什么要在afterEvaluate中执行呢?那是因为android插件是在配置阶段中才生成的mergeDebugNativeLibs等任务,原本的gradle构建是不存在这样一个任务的,所以我们才需要在配置完所有task之后,才进行的插入,我们可以看一下gradle的生命周期。
  通过对条件检索,我们就删除掉了我们想要的so,即ibnativecpptwo。so与libnativecpp。so。
  3、动态加载so
  根据上文检索出来的两个so,我们就可以在项目中上传到自己的后端中,然后通过网络下载到用户的手机上,这里我们就演示一下即可,我们就直接放在data目录下面吧。
  真实的项目过程中,应该要有校验操作,比如md5校验或者可以解压等等操作,这里不是重点,我们就直接略过啦!
  那么,怎么把一个so库加载到我们本来的apk中呢?这里是so原本的加载过程,可以看到,系统是通过classloader检索native目录是否存在so库进行加载的,那我们反射一下,把我们自定义的path加入进行不就可以了吗?这里采用tinker一样的思路,在我们的classloader中加入so的检索路径即可,比如:
  https:cs。android。comandroidplatformsuperprojectmaster:libcoreojlunisrcmainjavajavalangRuntime。java;l1?qRuntime。javaprivatestaticfinalclassV25{privatestaticvoidinstall(ClassLoaderclassLoader,Filefolder)throwsThrowable{finalFieldpathListFieldShareReflectUtil。findField(classLoader,pathList);finalObjectdexPathListpathListField。get(classLoader);finalFieldnativeLibraryDirectoriesShareReflectUtil。findField(dexPathList,nativeLibraryDirectories);ListFileorigLibDirs(ListFile)nativeLibraryDirectories。get(dexPathList);if(origLibDirsnull){origLibDirsnewArrayList(2);}finalIteratorFilelibDirItorigLibDirs。iterator();while(libDirIt。hasNext()){finalFilelibDirlibDirIt。next();if(folder。equals(libDir)){libDirIt。remove();break;}}origLibDirs。add(0,folder);finalFieldsystemNativeLibraryDirectoriesShareReflectUtil。findField(dexPathList,systemNativeLibraryDirectories);ListFileorigSystemLibDirs(ListFile)systemNativeLibraryDirectories。get(dexPathList);if(origSystemLibDirsnull){origSystemLibDirsnewArrayList(2);}finalListFilenewLibDirsnewArrayList(origLibDirs。size()origSystemLibDirs。size()1);newLibDirs。addAll(origLibDirs);newLibDirs。addAll(origSystemLibDirs);finalMethodmakeElementsShareReflectUtil。findMethod(dexPathList,makePathElements,List。class);finalObject〔〕elements(Object〔〕)makeElements。invoke(dexPathList,newLibDirs);finalFieldnativeLibraryPathElementsShareReflectUtil。findField(dexPathList,nativeLibraryPathElements);nativeLibraryPathElements。set(dexPathList,elements);}}
  我们在原本的检索路径中,在最前面,即数组为0的位置加入了我们的检索路径,这样一来claaloader在查找我们已经动态化的so库的时候,就能够找到!
  4、结束了吗?
  一般的so库,比如不依赖其他的so的时候,直接这样加载就没问题了,但是如果存在着依赖的so库的话,就不行了!相信大家在看其他的博客的时候就能看到,是因为Namespace的问题。具体是我们动态库加载的过程中,如果需要依赖其他的动态库,那么就需要一个链接的过程对吧!这里的实现就是Linker,Linker里检索的路径在创建ClassLoader实例后就被系统通过Namespace机制绑定了,当我们注入新的路径之后,虽然ClassLoader里的路径增加了,但是Linker里Namespace已经绑定的路径集合并没有同步更新,所以出现了libxxx。so文件(当前的so)能找到,而依赖的so找不到的情况。bugly文章。
  https:cloud。tencent。comdeveloperarticle1592672?fromarticle。detail。1751968
  很多实现都采用了Tinker的实现,既然我们系统的classloader是这样,那么我们在合适的时候把这个替换掉不就可以了嘛!当然bugly团队就是这样做的,但是笔者认为,替换一个classloader显然对于一个普通应用来说,成本还是太大了,而且兼容性风险也挺高的,当然,还有很多方式,比如采用Relinker这个库自定义我们加载的逻辑。
  为了不冷饭热炒,嘿嘿,虽然我也喜欢吃炒饭(手动狗头),这里我们就不采用替换classloader的方式,而是采用跟relinker的思想,去进行加载!具体的可以看到sillyboy的实现,其实就不依赖relinker跟tinker,因为我把关键的拷贝过来了,哈哈哈,好啦,我们看下怎么实现吧!不过在此这前,我们需要了解一些前置知识。
  5、ELF文件
  我们的so库,本质就是一个elf文件,那么so库也符合elf文件的格式,ELF文件由4部分组成,分别是ELF头(ELFheader)、程序头表(Programheadertable)、节(Section)和节头表(Sectionheadertable)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。
  那么我们so中,如果依赖于其他的so,那么这个信息存在哪里呢!?没错,它其实也存在elf文件中,不然链接器怎么找嘛,它其实就存在。dynamic段中,所以我们只要找打dynamic段的偏移,就能到dynamic中,而被依赖的so的信息,其实就存在里面啦我们可以用readelf(ndk中就有toolchains目录后)查看,readelfdnativecpptwo。so这里的d就是查看dynamic段的意思。
  这里面涉及到动态加载so的知识,可以推荐大家一本书,叫做程序员的自我修养链接装载与库这里就画个初略图。
  我们再看下本质,dynamic结构体如下,定义在elf。h中。typedefstruct{Elf32Sworddtag;union{Elf32Addrdptr;。。。。}}
  当dtag的数值为DTNEEDED的时候,就代表着依赖的共享对象文件,dptr表示所依赖的共享对象的文件名。看到这里读者们已经知道了如果我们知道了文件名,不就可以再用System。load去加载这个不就可以了嘛!不用替换classloader就能够保证被依赖的库先加载!我们可以再总结一下这个方案的原理,如图:
  比如我们要加载so3,我们就需要先加载so2,如果so2存在依赖,那我们就先加载so1,这个时候so1就不存在依赖项了,就不需要再调用Linker去查找其他so库了。我们最终方案就是,只要能够解析对应的elf文件,然后找偏移,找到需要的目标项(DTNEED)就可以了。publicListStringparseNeededDependencies()throwsIOException{channel。position(0);finalListStringdependenciesnewArrayListString();finalHeaderheaderparseHeader();finalByteBufferbufferByteBuffer。allocate(8);buffer。order(header。bigEndian?ByteOrder。BIGENDIAN:ByteOrder。LITTLEENDIAN);longnumProgramHeaderEntriesheader。phnum;if(numProgramHeaderEntries0xFFFF){ExtendedNumberingIftherealnumberofprogramheadertableentriesislargerthanorequaltoPNXNUM(0xffff),itissettoshinfofieldofthesectionheaderatindex0,andPNXNUMissettoephnumfield。Otherwise,thesectionheaderatindex0iszeroinitialized,ifitexists。finalSectionHeadersectionHeaderheader。getSectionHeader(0);numProgramHeaderEntriessectionHeader。info;}longdynamicSectionOff0;for(longi0;inumProgramHeaderEntries;i){finalProgramHeaderprogramHeaderheader。getProgramHeader(i);if(programHeader。typeProgramHeader。PTDYNAMIC){dynamicSectionOffprogramHeader。offset;break;}}if(dynamicSectionOff0){Nodynamiclinkinginfo,nothingtoloadreturnCollections。unmodifiableList(dependencies);}inti0;finalListLongneededOffsetsnewArrayListLong();longvStringTableOff0;DynamicStructuredynStructure;do{dynStructureheader。getDynamicStructure(dynamicSectionOff,i);if(dynStructure。tagDynamicStructure。DTNEEDED){neededOffsets。add(dynStructure。val);}elseif(dynStructure。tagDynamicStructure。DTSTRTAB){vStringTableOffdynStructure。val;dptrunion}i;}while(dynStructure。tag!DynamicStructure。DTNULL);if(vStringTableOff0){thrownewIllegalStateException(Stringtableoffsetnotfound!);}MaptofileoffsetfinallongstringTableOffoffsetFromVma(header,numProgramHeaderEntries,vStringTableOff);for(finalLongstrOff:neededOffsets){dependencies。add(readString(buffer,stringTableOffstrOff));}returndependencies;}
  6、扩展
  我们到这里,就能够解决so库的动态加载的相关问题了,那么还有人可能会问,项目中是会存在多处System。load方式的,如果加载的so还不存在怎么办?比如还在下载当中,其实很简单,这个时候我们字节码插桩就派上用场了,只要我们把System。load替换为我们自定义的加载so逻辑,进行一定的逻辑处理就可以了,嘿嘿,因为笔者之前就有写一个字节码插桩的库的介绍,所以在本次就不重复了,可以看Sipder,同时也可以用其他的字节码插桩框架实现,相信这不是一个问题。
  https:github。comTestPlanBSpider总结
  看到这里的读者,相信也能够明白动态加载so的步骤了,最后源代码可以在SillyBoy,当然也希望各位点赞呀!当然,有更好的实现也欢迎评论!!
  https:github。comTestPlanBSillyBoy

SD卡怎么恢复照片?本篇文章中,赤友软件为大家介绍如果SD卡中照片视频不见了,如何通过Mac数据恢复工具查找丢失的照片、视频的办法。在使用相机SD卡时,可能会因为意外或者病毒入侵等原因导致里……毛泽东主席一件睡衣打了73个补丁,背后故事太感人在毛泽东主席的众多遗物中有一件物品十分引人注目那是一件白色泛黄的棉质睡衣这件睡衣材质一般样式普通夹层,香蕉领外侧有两个口袋长141厘米……妻子苦守27年,黄维出狱一年后却让她滚,精神失常后跳河自尽蔡若曙照片1976年,北京护城河之上站了一位微胖白发苍苍的老妇人,神情麻木然后便毅然地跳入了河中,以决绝的方式结束了自己的一生。而这位老太太,正是原国民党高级将领黄维的夫……今天1700,2023武昌古城半程马拉松抽签结果公布5月7日(星期日)7:002023武昌古城半程马拉松将在首义广场鸣枪起跑此次赛事以武昌古城建城1800年为契机将为跑友奉上一场酣畅淋漓的体育文化盛宴……志愿军3年毙伤俘敌718477人,缴获枪炮才77299支,原抗美援朝战争是意义深远的,这一战让我们清晰认识到西方国家军队的作战特点和总体工业实力。在3年的战斗中,志愿军取得了辉煌的战果,共计击毙,击伤和俘虏敌军718477人。其中,击毙……西南匪首夜折香花,被民兵斩断三根手指,仍负隅顽抗在新中国成立的时候,中国的西南还有部分地区依然处于国民党的控制之下。在之前的十几年里,当地部分乡绅和国民党官员相互勾结,这些乡绅会出任保长等职务,然后利用权力欺压百姓。……李铁立功了!体坛反腐带走4大项3个一把手或前任,体育产业链颤北京时间4月4日,席卷中国体坛的反腐行动还在继续,就在今日,又有一个全中国体育界大协会的一把手被带走。此前中国足协的一把手陈戌源已经被正式带走,另外现任的中国田径协会主席……80项智能豪华标配多场景智能豪华电动轿车零跑C01上市9月28日晚,零跑汽车最新旗舰车型零跑C01正式上市。零跑C01定位为多场景智能豪华电动轿车,共推出后驱标准续航版、后驱长续航版、后驱超长续航版、四驱双电机性能版、四驱双电机高……人类高质量考试,从一千多年前就开始了《观榜图》仇英金榜题名明清时期八股文考卷古人用于作弊的衣服一转眼又到了期末考试季,大学生们通宵达旦地背重点,学霸想着突破自己,学渣祈祷着能及格就行,而中……清朝妃子为何总是戴着长指甲套?除了好看,更多的是为了方便皇帝百年盛世三更梦,万里盛世一局棋,中国的历史文化发展就好像一盘巨大的棋局,星河日月,几经辗转,有的时候,下得好了,就是天下太平,秦始皇一扫六合,汉武帝大一统,大唐帝国开元盛世,可……小米产品经理换新机颜值最高的Civi2即将登场今天,小米产品经理魏思琪用新款小米手机发微博,暗示小米新品即将登场。有网友猜测,魏思琪使用的新机应该是Civi2,该机型号为2209129SC,已经获得了3C认证,再获得……震动江南的寿州命案,嘉庆帝颁布近三十道谕旨,朱批可恨至极嘉庆十一年(1806)秋,苏州结芳戏班上演了一出名叫《寿椿园》的新戏,说的是安徽寿州发生了一起命案,原审认定三个死者被投毒谋杀,后经苏州知府周锷复审,查明系误中煤毒身亡,使得冤……
著名作家周立波去世捐出全部遗产,他的妻子忍痛完成遗愿周立波,这是一个传奇的名字。当我第一次读到他的作品《暴风骤雨》时,我被这本气势恢宏的小说深深地震撼了。几千年来重压在中国农村之上的封建统治,被追求解放与自由的农民们……甲午战争的反思天朝为啥总被小国暴打1894年,中日甲午战争爆发,清朝花费巨资,以举国之力建造的亚洲第一海军北洋舰队,被当时所谓的蕞尔小国日本海军一举歼灭。公元1888年,北洋舰队正式成立。最初,舰队拥有二……立秋后,抓住孩子长高ampampquot黄金期ampampq万物的生长都有规律,秋天不光是收获的季节,更是孩子骨骼发育、高速增长的黄金期,家长们一定要抓入立秋后长个关键时,加强对钙和营养的补充,帮助孩子高人一等。老人常说转骨趁秋天……日本战国三杰日本战国三英杰,指的是日本战国时代末期为统一日本做出杰出贡献的三位战国大名,他们分别是:织田信长、丰臣秀吉、德川家康。织田信长织田信长(1534年6月23日1582年6月……30拿下对手!赛前发布会EDG和DK表态打满BO5太累了大家好,这里是锦城,给大家带来S11世界赛的最新资讯。S11总决赛将在后天开打,拳头也是召开了决赛前的线上发布会,EDG和DK两队选手纷纷表露心志,拳头负责人也回应了很多……诸葛亮明知关羽会放走曹操,为何还派关羽去守华容道?伯仲之间见伊吕,指挥若定失萧曹,这首诗就是唐朝诗人杜甫对诸葛亮指挥能力的高度评价,说的是诸葛亮才能和伊尹、吕尚差不多,军事指挥能力则盖过萧何、曹参。公元208年左右……湖广熟,天下足(下)长沙武汉(下)湖北曾侯乙,是周朝分封的曾国国君,姬姓,氏南宫名乙,生于公元前475年。专家们花了几十年才证明曾国就是随国,一国两名(为啥猫叫咪?)。此人比较可……数读3月厂商零售销量比亚迪优势拉开,特斯拉华晨宝马进前十文:懂车帝原创李德喆〔懂车帝原创行业〕日前,乘联会公布了今年3月乘用车市场产销情况。数据显示,今年3月,狭义乘用车零售销量达158万辆,同比微增0。3个百分点;今年一季度……毛主席否定轰炸金门,韩先楚25年后赞叹主席真高明1958年7月18日晚,毛主席召集军委副主席和空军、海军等单位的负责人,布置东南沿海的军事斗争任务。毛主席指出:支援阿拉伯人民的反侵略斗争,不能只限于道义上的支援,还要有……鲁迅后人独子资质平庸,孙子当倒插门女婿,没一个人会写文章鲁迅与许广平的独子周海婴,其实是避孕失败后意外降临到这个世界上的。上世纪20年代的上海,时局动荡不堪。鲁迅和许广平忙于文化救国的大业,压根不想把精力腾出来照料小婴儿,为此……2024年江原道冬青奥会奖牌设计大赛冠军揭晓新华社首尔4月4日电(记者陆睿、孙一然)国际奥委会官方网站4日公布2024年韩国江原道冬季青年奥林匹克运动会奖牌设计大赛冠军作品,巴西建筑设计师丹特阿奇拉乌瓦伊提交的作品闪耀未……容声冰箱抢占超薄嵌入新赛道,技术创新是竞争加速器全国家用电器工业信息中心近日发布的《2022年中国家电行业年度报告》显示,受到大环境的影响,2022年我国家电销售整体增长失速,内销零售额7307。2亿元,同比下滑9。5。但是……
友情链接:易事利快生活快传网聚热点七猫云快好知快百科中准网快好找文好找中准网快软网