专栏电商日志财经减肥爱情
投稿投诉
爱情常识
搭配分娩
减肥两性
孕期塑形
财经教案
论文美文
日志体育
养生学堂
电商科学
头戴业界
专栏星座
用品音乐

58同城Android端最小插件化框架实战和原理分析

  01hr背景
  移动互联网进入存量时代,随着人口红利减退,充分盘活、经营现有流量便成为了各行各业全新的机遇与挑战。各大公司都在内卷发力,对App包大小、启动速度、性能做持续优化。
  App包体积和用户转换率成负相关,包体积越小、用户下载时长越短,用户转换率越高。而随着国内用户的增量见顶,越来越多的应用选择出海,开发对应的海外版,GooglePlay应用市场目前强制要求超过100MB的应用只能使用AAB扩展文件方式上传,GooglePlay会为我们的应用托管AAB扩展文件,进行自定义分发和动态交付。
  (可以看到,排名靠前的App包大小基本是都100M,很多App都上架了极速版)
  58同城App对包大小这块也非常关注,每次发布版本之前都会对包大小进行分析与监控,下图为Android32位包大小变化:
  近期我们在对包大小进行新一轮的梳理,过程中发现:人脸认证库内置了4套框架,当自研框架认证异常时将切换到其他框架,这种策略下内置4套框架对包大小造成了较大的负重。经过分析与调研,达成了共识方案:内置腾讯认证,把自研认证和阿里认证动态化,预计收益可达4M,后期方案落地后逐步推进腾讯认证的动态化,预计可再减少1。66M。
  包大小减少的常用手段非常多,主要分类还是技术手段和业务手段:
  当前选择动态化作为技术选项,是因为我们在技术手段上对包大小做的努力已基本见顶,同时从认证模块的背景考虑,低频、低耦合正适合于插件化场景。动态化又分成了正规军AndroidAppBundle和国内的游击队插件化,至于58同城在插件化、AAB上的探索和实现上的技术选型,后面的章节中会进行讲解。最终效果:
  插件化便是本文章的重点,插件化一般用来做两件事:减少基础包大小和动态更新。插件化是移动端模块化、插件化、组件化三剑客之一,历史也非常久了,网上公开的免费插件化文章很多都是纯概念型或纯方案型,本文章将会从01讲解插件化的知识,配合实战经验,让你有所收获。
  工欲善其事,必先利其器,我们先来了解插件化中涉及到的相关知识点。
  02hr插件化需要了解的知识在正式了解插件化之前,需要了解插件化会涉及到的相关概念,整个文章是一个循序渐进的过程。这样在后面讲到插件化需要解决的问题、现有插件化框架的对比、插件化的实现时可以做到知其然而知其所以然。2。1类加载过程和类加载器2。1。1Java
  首先,我们来了解下类加载的过程和类加载器,以java文件为例,类加载干过程如下:
  加载:这部分涉及到类加载器,将class文件加载到内存,创建对应的Class对象连接:包括验证、准备、解析三部分。验证阶段会检验被加载的类是否有正确的内部结构,并和其他类协调一致;准备阶段则负责为类的静态属性分配内存,并设置默认初始值;解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。初始化:JVM负责对类进行初始化,主要是对静态属性静态块进行初始化。
  那么一个类什么时候会被触发加载过程呢?除去系统类、扩展类外,我们程序的类什么时候执行,主要包括以下几种情况:创建类的实例,如newXXX();调用某个类的静态方法;访问某个类或接口的静态属性,或为该静态属性赋值;通过反射方式来创建某个类或接口对应的java。lang。Class对象,如使用Class。forName(XXX)初始化某个类的子类。初始化子类时,所有的父类都会被初始化。
  那么讲完了Java类的加载过程,我们再来看下它的类加载器:
  类加载器加载类遵循双亲委派模式,这是基于安全和效率方面的考虑,实现委派模式是通过ClassLoader构造器中的parent来做的,我们看一下ClassLoader抽象类:publicabstractclassClassLoader{protectedClasslt;?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{检测是否已装载过该ClassClasslt;?cfindLoadedClass(name);未装载过if(cnull){try{是否有父ClassLoader,有的话使用父ClassLoader尝试加载if(parent!null){cparent。loadClass(name,false);}else{没有父ClassLoader,使用BootstrapClassLoader尝试加载cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){ClassNotFoundExceptionthrownifclassnotfound}if(cnull){上述都没找到,使用自身ClassLoader加载cfindClass(name);}}returnc;}}2。1。2Android
  Android区别于Java的两个核心点:基于Dex文件格式,而非class文件格式,当然Dex里包含class虚拟机为DavilkART,而非JVM
  其实dex和Class本质上都是一样的,都是二进制流文件格式,dex文件是从class文件演变而来的:class文件存在冗余信息,dex文件则去掉了冗余,并且整合了整个工程的类信息,在Android中做插件化和热修复都离不开dex。
  classVSdex:内存占用大,不适合移动端:dex做了各种优化和去冗余堆栈的加载模式,加载速度慢文件IO操作多,类查找慢:Android虚拟机直接IOloaddex,再进行类加载
  Android的类加载器包括:
  ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。SecureClassLoader拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。URLClassLoader它继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。BootClassLoader是ClassLoader的内部类,用于加载一些系统Framework层级需要的类。BaseDexClassLoader继承自ClassLoader,是抽象类ClassLoader的具体实现类,PathClassLoader和DexClassLoader都继承它。PathClassLoader加载系统类和应用程序的类,如果是加载非系统应用程序类,则会加载dataapp目录下的dex文件以及包含dex的apk文件或jar文件(已安装)DexClassLoader可以加载自定义的dex文件以及包含dex的apk文件或jar文件,也支持从SD卡进行加载InMemoryDexClassLoader是Android8。0新增的类加载器,继承自BaseDexClassLoader,用于加载内存中的dex文件。
  这里需要注意一点,我们的应用程序的默认ClassLoader为PathClassLoader,而PathClassLoader的父ClassLoader为BootClassLoader,这也是为什么bugly上一些ClassNotFound堆栈顶部为BootClassLoader:ClassLoader。javaprivatestaticClassLoadercreateSystemClassLoader(){StringclassPathSystem。getProperty(java。class。path,。);StringlibrarySearchPathSystem。getProperty(java。library。path,);returnnewPathClassLoader(classPath,librarySearchPath,BootClassLoader。getInstance());}
  在AndroidClassLoader中,Dex最终会被运行解析成DexElements,我们查看Android源码BaseDexClassLoader,可以看到:packagedalvik。system;publicclassBaseDexClassLoaderextendsClassLoader{privatefinalDexPathListpathList;publicBaseDexClassLoader(StringdexPath,FileoptimizedDirectory,StringlibraryPath,ClassLoaderparent){super(parent);this。pathListnewDexPathList(this,dexPath,libraryPath,optimizedDirectory);}OverrideprotectedClasslt;?findClass(Stringname)throwsClassNotFoundException{ClasscpathList。findClass(name,suppressedExceptions);if(cnull){throwerr;}returnc;}OverrideprotectedURLfindResource(Stringname){returnpathList。findResource(name);}OverrideprotectedEnumerationURLfindResources(Stringname){returnpathList。findResources(name);}OverridepublicStringfindLibrary(Stringname){returnpathList。findLibrary(name);}}
  通过dexPath(dex路径)、libraryPath(so路径)、optimizedDirectory(oat优化存储目录)构建了DexPathList,而classloader的findClass、findLibrary、findResources都委托给了它去查找,也是AndroidClassLoader使用Dex加载的实现部分(区别于JavaClassLoader),最终loadClass会调用到DexFile的:privatestaticnativeClassdefineClassNative(Stringname,ClassLoaderloader,intcookie)throwsClassNotFoundException,NoClassDefFoundError;
  2。2ClassLoader的findClass、findLibrary、findResource
  上面我们了解了ClassLoader相关的知识,那么在Android插件化中,我们还需要了解相关的几个常用方法。2。2。1findClass
  根据类完整名称去查找Class对象,如findClass(com。xx。Test),同时需要注意,在ClassLoader中关于class加载的有以下几个方法:
  在打印App默认的PathClassLoader对象时,可以看到当前的类查找路径,路径一般以。apk、。jar结尾,ClassLoader变化在这些路径进行类查找,在插件化中,合并插件的dex路径也会出现在当中,如果是独立的ClassLoader,则里面只有插件本身的路径:DexPathList〔〔zipfiledataappcom。wubav5PkwKJhGUzCf2aDtJEQAQbase。apk,zipfiledatadatacom。wubalibraryhousehsgmainpluginall0。1。0hsgmainpluginrelease。jar〕
  2。2。2findLibrary
  查找、加载so库使用,我们在打印App默认的PathClassLoader对象时,可以看到当前可查找的so的路径,ClassLoader便会从这些路径进行so查找,在插件化中,合并插件的so路径也会出现在当中,如果是独立的ClassLoader,则里面只有插件so本身的路径:nativeLibraryDirectories〔dataappcom。wubav5PkwKJhGUzCf2aDtJEQAQlibarm,dataappcom。wubav5PkwKJhGUzCf2aDtJEQAQbase。apk!libarmeabiv7a,systemlib〕〕
  关于ClassLoader中so查找有以下1个需要关注的方法:StringfindLibrary(Stringlibname)
  findLibrary是根据so名称去查找它所在的完整路径,当代码触发System。loadLibrary(libName)时触发,findLibrary实现publicStringfindLibrary(StringlibraryName){StringfileNameSystem。mapLibraryName(libraryName);for(Filedirectory:nativeLibraryDirectories){StringpathnewFile(directory,fileName)。getPath();if(IoUtils。canOpenReadOnly(path)){returnpath;}}returnnull;}
  Framework一般不会让用户直接通过dlopen去加载动态链接库,而是封装了以下两种方式:publicfinalclassSystem{方式一:通过so文件路径加载publicstaticvoidload(Stringfilename){Runtime。getRuntime()。load0(VMStack。getStackClass1(),filename);}方式二:通过so库名加载publicstaticvoidloadLibrary(Stringlibname){Runtime。getRuntime()。loadLibrary0(VMStack。getCallingClassLoader(),libname);}}
  System。loadLibrary()最终通过dlopen()来实现:
  SysytemloadLibrarySysytemloadRuntimenativeLoadJavaNativedvmLoadNativeCodedlopen打开一个so文件,创建一个handle2。2。3findResourceURLfindResource(Stringname)这个方法用于查找资源,如findResource(file:D:workspaces),在Android中基本无用,Android有属于自己的一套资源管理方案。2。3DexClassLoader的oat配置
  如果需要使用多ClassLoader时,需要自己构造DexClassLoader:classPluginDexClassLoaderextendsBaseDexClassLoader{privatePluginDexClassLoader(ListStringdexPaths,FileoptimizedDirectory,StringlibrarySearchPath,ClassLoaderparent)throwsThrowable{super((dexPathsnull)?:TextUtils。join(File。pathSeparator,dexPaths),optimizedDirectory,librarySearchPath,parent);}}
  需要传入dex路径集合、so库目录、dex优化目录、以及父ClassLoader。DexClassLoader提供了optimizedDirectory,而PathClassLoader则没有(系统会自动生成以后缓存目录,即datadalvikcache,不同厂商不一样),optimizedDirectory是用来存放odex文件的地方,所以可以利用DexClassLoader实现动态加载。
  这边简单介绍下几个概念:dex:java程序编译成class后,dx工具将所有class文件合成dex文件。odex(Android5。0之前):OptimizedDEX,即优化过的dex。Android5。0之前APP在安装时会进行验证和优化,为了校验代码合法性及优化代码执行速度,验证和优化后,会产生odex文件,运行apk的时候,直接加载ODEX,避免重复验证和优化,加快了apk的响应时间。oat(Android5。0之后):oat是ART虚拟机运行的文件,是ELF格式二进制文件,包含DEX和编译的本地机器指令,oat文件包含dex文件,因此比odex文件占用空间更大。Android5。0dex2oat默认会把classes。dex翻译成本地机器指令,生成ELF格
  式的oat文件。不过android5。0之后oat文件还是以。odex后缀结尾,但是已经不是android5。0之前的文件格式,而是ELF格式封装的本地机器码。vdex:Android8。0以后加入,包含apk的未压缩dex代码,另外还有一些旨在加快验证速度的元数据。2。4LoadedApk
  在Android中,我们通过context。getClassLoader()即可获取到程序默认的类加载器,当然这个加载器在没有任何处理的时候为PathClassLoader,那么如果我们想对其进行替换扩展该如何处理呢?首先我们需要找到它具体的持有者ContextImpl。java:packageandroid。app;classContextImplextendsContext{finalNonNullLoadedApkmPackageInfo;}
  再看看LoadedApk,LoadedApk对象是apk文件在内存中的表示,在启动我们的应用进程后,经过systemserver的层层调用,最终会创建LoadedApk,可以看到下面代码,它持有了很多重要的信息,如主线程、包信息、Resources、ClassLoader、Application等:packageandroid。app;publicfinalclassLoadedApk{privatefinalActivityThreadmActivityThread;finalStringmPackageName;privateApplicationInfomApplicationInfo;privateStringmAppDir;privateStringmResDir;privateStringmDataDir;privateStringmLibDir;privateFilemDataDirFile;privatefinalClassLoadermBaseClassLoader;ResourcesmResources;privateClassLoadermClassLoader;privateApplicationmApplication;privateString〔〕mSplitNames;privateString〔〕mSplitAppDirs;privateString〔〕mSplitResDirs;privateString〔〕mSplitClassLoaderNames;
  在插件化中,对ClassLoader、Resources的处理都可以通过它,它在一个进程内是全局唯一的。VirtualApp处理资源采用的就是替换LoadedApk的Resource对象。而替换默认的ClassLoader也可以通过反射替换掉LoadedApk中的mClassLoader,这个api相对来说很稳定,各Android版本没有做变更。2。5AssetManager、Resources
  插件化中,除了class、libs相关的加载,另一个重点就是资源,在Android中与资源加载相关的两个类便是AssetManager、Resources。Resources用来获取res目录下的各种与设备相关的资源,而AssetManager则用来获取assets目录下的资源。
  AssetManager属于Resources的一个属性:packageandroid。content。res;publicclassResources{privateResourcesImplmResourcesImpl;publicResources(AssetManagerassets,DisplayMetricsmetrics,Configurationconfig){this(null);mResourcesImplnewResourcesImpl(assets,metrics,config,newDisplayAdjustments());}publicfinalAssetManagergetAssets(){returnmResourcesImpl。getAssets();}}
  可以看到构造Resources对象时,需要传入AssetManager对象,我们再来看看AssetManager:packageandroid。content。res;publicfinalclassAssetManagerimplementsAutoCloseable{AssetManager构造器使用UnsupportedAppUsage注解UnsupportedAppUsagepublicAssetManager(){}AssetManageraddAssetPath使用UnsupportedAppUsage注解UnsupportedAppUsagepublicintaddAssetPath(Stringpath){returnaddAssetPathInternal(path,falseoverlay,falseappAsLib);}可获取已安装的资源路径,使用UnsupportedAppUsage注解UnsupportedAppUsagepublicNonNullApkAssets〔〕getApkAssets(){synchronized(this){if(mOpen){returnmApkAssets;}}returnsEmptyApkAssets;}}
  AssetManager不允许App代码直接对其进行构造,所以在插件化过程中,如果要使用独立资源模式构建插件AssetManager需要用到反射,同时AssetManager添加资源查找路径的方法addAssetPath也不允许App代码直接访问,插件化中添加插件资源路径需要对其进行反射。AssetManager的资源路径一般包含以下几类:
  而通过AssetManager的getApkAssets的getAssetPath方法可以获取到该AssetManager的资源路径(数组),不过这些方法对于App层都无法直接调用,需要使用反射。
  Android应用中,Application、Activity、Service都可以获取到AssetManager和Resources对象:publicclassAppextendsApplication{OverridepublicResourcesgetResources(){returnsuper。getResources();}OverridepublicAssetManagergetAssets(){returnsuper。getAssets();}}publicclassTestActivityextendsActivity{OverridepublicResourcesgetResources(){returnsuper。getResources();}OverridepublicAssetManagergetAssets(){returnsuper。getAssets();}}publicclassTestServiceextendsService{OverridepublicResourcesgetResources(){returnsuper。getResources();}OverridepublicAssetManagergetAssets(){returnsuper。getAssets();}}
  其实它们都指向当前Context中的LoadedApk的Resources,可以说是当前应用唯一的,在插件化中,我们就可以针对上述这些资源相关的类和方法进行处理。
  03hr插件化需要解决的核心问题了解了插件化需要掌握的知识点后,我们再来了解一下插件化需要解决的核心问题。
  可以看到上图宿主与插件的结构图,插件化需要解决的核心问题也是从这几块入手。3。1插件化的安全性和稳定性为什么Android包括iOS,以及当前流行的跨端框架Flutter都不允许对已安装的应用做插件动态更新,主要是基于对安全性和性能的顾虑。如绕过应用市场检测,给用户App进行木马插件等风险性的动态更新,同时对于运行性能会有影响,缺少各种对宿主包的优化。当然AndroidiOS允许JS的动态更新,毕竟JS文件可明文查看,而GooglePlay近两年也推出了官方的插件化能力AndroidAppBundle。
  抛开这些,我们看看国内的插件化,在安全性和稳定性上存在问题的原因主要如下:安全性:
  插件Apk的安全性问题,如被劫持篡改所以插件化框架一般都需要对插件Apk做签名和摘要验证
  稳定性:
  主要涉及到私有api的反射,不同Android版本、不同厂商对class、so、resources涉及到的隐私api都会存在差异,特别是需要将插件合并到宿主运行环境的情况
  同时一些插件化框架为了绕过四大组件未安装的校验,做了很多的hook和反射,稳定性和性能都需要做大量的适配工作3。2class和so加载
  对于class和so的加载,有两种模式:合并式和独立式。合并式
  优点:
  宿主与插件可直接互相访问
  缺点:
  稳定差,需要做大量适配
  宿主与插件相同库如果出现不兼容,会出现对应class加载异常
  独立式
  优点:
  几乎无反射,稳定性强,只需要hook一处用于扩展默认的PathClassLoader
  不用处理宿主和插件相同库版本不兼容问题,宿主和插件ClassLoader分离
  缺点:
  相互访问比较麻烦,主要在于宿主和插件之间的访问,不过都可以通过拦截各自
  ClassLoader的findClass、findLibrary来处理
  由此可见,如果是独立的模块,不使用宿主包的能力,其实用独立插件很合适,但如果涉及到大量宿主能力的调用(不推荐,这样插件Apk过于依赖宿主的相关库的向下兼容性),需要对ClassLoader做更多的处理。3。2。1合并式
  顾名思义,就是将插件dex路径合并到宿主的dex路径中,so路径合并宿主的so路径中,到主要通过classloader实现。
  dex路径合并:
  (1)Android6。0及其以上
  反射classloader的pathList,扩展其dexElements,扩展时反射makeDexElements(List,File,List)
  (2)Android4。4。26。0
  反射classloader的pathList,扩展其dexElements,扩展时反射makeDexElements(ArrayList,File,ArrayList)
  (3)Android4。04。4。2
  反射classloader的pathList,扩展其dexElements,扩展时反射makeDexElements(ArrayList,File)
  可以看到,classloader。pathList。dexElements是稳定的私有api,主要区别在于扩展DexElements用到的makeDexElements方法签名不同,不过目前大多数App的最低运行版本已经升到了Android5。0
  对于合并式,会出现类版本兼容性问题:可以看下图,插件和宿主都引用了同一个库,但是当宿主升级此库后,由于它内部未做向下兼容,删除了某些类或者修改了对外方法,如果此时插件不进行同步更新打包,那么运行将会出现问题。
  又或者宿主和插件打包分离,都引用了同一个库,但这个库版本不兼容时,也会出现这个问题
  如何解决?方法1:参考AAB打包规则,插件参与宿主打包过程,将相同依赖库打包到宿主,同时可判断插件是否需要做对应的更新方法2:分开打包,但需要制定相同依赖库升级的规则,不过这种方式会使插件包体积变大,存在冗余
  so路径合并:
  (1)Android7。1及其以上
  反射classloader的pathList,扩展其nativeLibraryDirectories,需要使用pathList的systemNativeLibraryDirectories、makePathElements、nativeLibraryPathElements等几个函数去扩展
  (2)Android6。07。1
  与7。1及其以上一致,区别在于makePathElements的方法签名不同
  (3)Android4。06。0
  反射classloader的pathList,扩展其nativeLibraryDirectories,直接构建新的ArrayList替换nativeLibraryDirectories即可
  与dex路径合并一致,在不同版本之前存在一些差别。3。2。2独立式
  独立式就是插件中的class和so使用独立的ClassLoader加载:publicclassPluginDexClassLoaderextendsBaseDexClassLoader{publicPluginDexClassLoader(ListStringdexPaths,FileoptimizedDirectory,StringlibrarySearchPath,ClassLoaderparent){super((dexPathsnull)?:TextUtils。join(File。pathSeparator,dexPaths),optimizedDirectory,librarySearchPath,parent);}}
  可以看到,独立ClassLoader无反射,一般,我们会扩展程序默认的ClassLoader(一处反射:替换掉loadedApk中的classloader对象),在默认ClassLoaderfindClass、findLibrary异常时,再使用独立的ClassLoader去加载。3。3资源加载和资源id冲突
  上面讲完了插件Apk中class和so的加载,我们再来看下资源如何加载处理。同样,资源加载也分为合并式和独立式:合并式
  优点:
  宿主与插件可直接互相访问资源
  缺点:
  稳定差,需要做大量适配
  需要解决资源id冲突问题
  需要解决引用的相同第三库资源变更问题
  独立式
  优点:
  反射少,仅需反射AssetManager创建、添加路径的方法
  无需关心资源id冲突问题
  无需关心第三方库资源变更问题
  缺点:
  资源相互访问比较麻烦
  独立式意味着如果需要引用第三方库的资源,要将第三方库单独打包到插件中,而宿主如果也引用了此第三方库,势必会造成插件包体积增大,存在冗余
  由此可见,如果是独立的模块,不使用宿主包的资源,其实用独立式很合适,但如果需要访问宿主资源,则需要考虑合并式,否则要做大量的处理,毕竟你无法轻松地控制那些第三方库。3。3。1合并式
  合并式是将插件的路径合并到默认的Resouces中:
  Android5。0及其以上:获取到当前的Resources对象
  获取该Resources的AssetManager,反射调用其addAssetPath()添加插件路径
  Android5。0以下:获取到旧的Resources对象和当前的Context对象
  构建新的Resource对象,将旧的Resources已有的资源路径、插件路径都合并进去
  替换掉旧的Resources(这一步有大量的适配),主要是判断当前的Context类型:
  (1)Context为ContextThemeWrapper,做对应替换处理
  (2)Context的baseContext为android。app。ContextImpl类型,做对应替换处理
  (3)个别rom的定制,处理Context的baseContext的mResources和mTheme
  除此之外,合并式还需要解决插件资源id和宿主资源id冲突问题,主要是Apk中的resources。arsc索引冲突,这个非常容易出现:
  合并式资源id冲突问题:
  我们知道可以通过R。id。xxxR。string来非常方便的访问应用程序的资源,在编译的时候,Android编译工具aapt会扫描你所定义的所有资源,然后给它们指定不同的资源ID。
  资源ID是一个16进制的数字,格式是PPTTNNNN,如0x7f010001:PP代表资源所属的包(packageID),对于应用程序的资源来说,PP的取值是077TT代表资源的类型(typeID)NNNN代表这个类型下面的资源的名称
  一旦资源被编译成二进制文件的时候,aapt会生成R。java文件和resources。arsc文件,R。java用于代码的编译,而resources。arsc则包含了全部的资源名称、资源ID和资源的内容(对于单独文件类型的资源,这个内容代表的是这个文件在其。apk文件中的路径信息),这样就把运行环境中的资源id和具体的资源对应起来了。
  插件Apk和宿主Apk的资源id很容易发生重复,造成资源合并冲突,那么针对于这个问题,目前的插件化框架有以下几种解决方案:
  3。3。2独立式
  上述讲了合并式资源的方案和问题,而资源处理还有另一种方案,便是独立式,独立式资源不会有资源id冲突的问题,但是宿主和插件之间的资源访问比较麻烦,适用于业务比较独立的插件,插件只使用插件自身的资源,方法很简单:构建新的AssetManagerAssetManagerassetManagerAssetManager。class。newInstance();添加插件资源路径MethodaddAssetPathMethodHiddenApiReflection。findMethod(AssetManager。class,addAssetPath,String。class);addAssetPathMethod。invoke(assetManager,pluginApk。getAbsolutePath());构建新的ResoucesResourcesnewResourcesnewResources(assetManager,preResources。getDisplayMetrics(),preResources。getConfiguration());
  让插件使用这个新的Resources有两种方式:
  3。4四大组件
  Android的四大组件:Activity、Receiver、Service、ContentProvider需要在AndroidManifest。xml中进行注册。Android的四大组件其实有挺多的共通之处,比如它们都接受ActivityManagerService(AMS)的管理,它们的请求流程也是基本相通的。目前网上有很多关于四大组件启动流程和原理的分析,篇幅很长,我们这边就直接讲述插件化如何支持四大组件,通过系统服务的注册校验。
  系统安装好宿主Apk后,会解析Apk中的AndroidManifest。xml,生成组大组件的信息,如果插件的四大组件未在宿主AndroidManifest。xml中注册,会出现启动组件崩溃问题。
  方案总体可以分成以下3类:3。4。1动态替换方案
  以Activity为例,主要是对Android底层代码进行Hook,使在App启动Activity中进行欺骗ActivityManagerService,以达到加载插件中组件的目的。
  3。4。2静态代理方案
  静态代理方案相对来说,会比较好理解一点。因为它不需要去Hook任何代码,主要在宿主中创建一个代理的Activity,叫ProxyActivity,ProxyActivity内部有一个对插件Activity的引用,让ProxyActivity的任何生命周期函数都调用插件中的Activity中同名的函数。这是dynamicloadapk插件化框架所创。
  以上方案适用于Activity、Receiver、Service、ContentProvider,但ContentProvider需要注意一点:3。4。3提前预埋插件所用的四大组件
  还有最后一种方案,是我认为最简单的,即提前预埋好四大组件,如预埋多个Activity(区分不同启动模式)、Service等,插件对这些预埋的组件进行实现。其实插件所用的组件一般比较固定,我们要做的是做好不同插件使用的组件管理。这种方案可以避免掉上述四大组件的各种问题,无需多余的hook、反射处理。如AAB机制就是会提前讲宿主和dynamicfeature的AndroidManifest文件进行合并,放置在宿主包中,不允许插件对这些四大组件配置进行动态更新。
  这种方案需要注意一点就是ContentProvider:
  应用程序在创建Application的过程中,会执行handleBindApplication(),将AndroidManifest中ContentProvider进行安装,所以ContentProvider的初始化时机是非常早的。这时如果插件Apk没有安装,则会导致这些ContentProvider找不到实现类,出现崩溃。我们可以用一个空的ContentProvider骗过App启动校验,插件安装完成后再对真实的ContentProvider进行初始化3。5现有插件化框架技术方案对比
  讲完了插件化需要解决的几个核心问题,那么我们最后来看下目前市面上的插件化框架对这些问题分别是如何选型处理的:
  总结:
  这些插件化框架都很优秀,是行业的先驱,功能也很完备,但是实际落地过程中有一些问题,如:不再维护,部分框架还停留在15年,gradle插件、打包适配等比较陈旧功能庞杂,包含多种加载模式,以及大量衍生功能地适配处理,导致稳定性、接入成本剧增,后期维护成本高打包功能侵入宿主App,适配成本高只适合于特定的业务场景,如AAB,需要每次基于基础包重新构建发布使用了一些隐私API,有政府整改风险
  04hr58App最小插件化实现在背景所有说的业务中,我们使用插件化作为技术方案的原因是为了减少包大小。不需要完整插件化框架这么多功能,如新增组件能力、多种加载模式的切换,以及一些其他的边缘能力,我们只需要最核心的插件安装、加载能力。其实Shadow、dynamicloadapk就是如此,适用于独立的业务模块,反射少,独立ClassLoader和Resources,四大组件使用静态代理模式进行生命周期分发,但即使这些框架,仍然具有代码复杂、接入成本高的问题,或者项目太老,很多环境和代码未做适配。如果有一个具备完全可运行的、接入成本低的、稳定高的插件化框架,其实更利于落地推广。
  58同城Android端之前已使用基于AAB的动态化框架进行了落地:厂商包:包大小控制在50M以内市场包:基于版本级别的线上AB测剪包:招聘和房产剪包,用于外链投放
  此框架之前在58App上线动态更新十余次,单次更新用户最高800w,那为什么信安人脸认证动态化不继续使用此框架呢?主要有以下两个原因:
  以上就是关于信安人脸动态化的技术选型,对于插件化的几个痛点,解决方案如下:
  接下来,我们来看下58App最小插件化框架的设计和实现。4。1框架设计
  可以看到结构非常简单:
  编译期:插件打包上传能力插件资源处理能力
  运行期:插件管理:包含插件的版本、路径、apk、libs的管理插件安装:插件下载、校验,插件的apk拷贝、libs抽取存储,安装标记等插件加载:dexso加载,资源加载4。2插件打包
  主要处理插件资源和插件打包上传,无需侵入宿主打包流程,执行:。。gradleuploadPluginRelease
  成功后,在buildoutputsapkdebug(release)下会生成:buildoutputsapkdebug(release)pluginuploadinfos。json(上传的插件版本、md5、url)pluginmanifest。xml(清单文件,需要将内容拷贝合并到宿主接入SDK的清单文件)arm64v8a。apkarmeabiv7a。apkuniversal。apk
  pluginuploadinfos。json内容如下,eg:{version:1。0,infos:〔{abi:armeabiv7a,url:https:wos2。58cdn。com。cnFgHcBazYFgLicutpackagePluginApparmeabiv7adebug1653323406181。apk,md5:0804443b61a079262ff760f33f76c077},{abi:arm64v8a,url:https:wos2。58cdn。com。cnFgHcBazYFgLicutpackagePluginApparm64v8adebug1653323409379。apk,md5:34767a82a47a240c1bbd363ea6e615ea}〕}
  资源处理这块,对插件Activity、Service资源获取方法做编译织入PluginResourcesManager。getResources(pluginName):
  PluginResourcesManager如下:publicfinalclassPluginResourcesManager{privatestaticfinalMapString,ResourcesresourcesMapnewHashMap();privatestaticfinalMapString,AssetManagerassetManagerMapnewHashMap();publicstaticResourcesgetResources(StringpluginName){if(resourcesMap。containsKey(pluginName)){returnresourcesMap。get(pluginName);}try{AssetManagerassetManagerAssetManager。class。newInstance();MethodaddAssetPathMethodHiddenApiReflection。findMethod(AssetManager。class,addAssetPath,String。class);ApkPluginapkPluginnewApkPlugin(pluginName,,);FilepluginApkPluginPathManager。getInstance()。getPluginApk(apkPlugin);addAssetPathMethod。invoke(assetManager,pluginApk。getAbsolutePath());ResourcespreResourcesWBPluginLoader。getContext()。getResources();ResourcesnewResourcesnewResources(assetManager,preResources。getDisplayMetrics(),preResources。getConfiguration());resourcesMap。put(pluginName,newResources);assetManagerMap。put(pluginName,assetManager);returnnewResources;}catch(Throwablee){returnWBPluginLoader。getContext()。getResources();}}publicstaticAssetManagergetAssetManager(StringpluginName){if(assetManagerMap。containsKey(pluginName)){returnassetManagerMap。get(pluginName);}ResourcesresourcesgetResources(pluginName);returnresources。getAssets();}}
  4。3插件管理
  datadata{packageName}appwbpluginspluginName1codecache(代码缓存)nativeLib(so目录)arm64v8aarmeabiv7aoat(oat优化目录)base。apk(插件apk)mark。json(安装标记,包含版本信息)pluginName2。。。4。4插件安装
  插件安装这一块,流程如下:
  安装标记如下,eg:{name:TestPlugin,version:1。0,abi:armeabiv7a}
  4。5插件加载
  加载dexso
  插件ClassLoader:finalclassPluginDexClassLoaderextendsBaseDexClassLoader{privatePluginDexClassLoader(ListStringdexPaths,FileoptimizedDirectory,StringlibrarySearchPath,ClassLoaderparent)throwsThrowable{super((dexPathsnull)?:TextUtils。join(File。pathSeparator,dexPaths),optimizedDirectory,librarySearchPath,parent);UnKnownFileTypeDexLoader。loadDex(this,dexPaths,optimizedDirectory);}staticPluginDexClassLoadercreate(ListStringdexPaths,FileoptimizedDirectory,FilelibrarySearchFile)throwsThrowable{PluginDexClassLoaderclnewPluginDexClassLoader(dexPaths,optimizedDirectory,librarySearchFilenull?null:librarySearchFile。getAbsolutePath(),PluginDexClassLoader。class。getClassLoader());returncl;}}
  重写App默认的PathClassLoader的双亲委派模式:第一步,获取App运行的默认ClassLoader扩展默认ClassLoader的class、library加载,优先使用原始默认的ClassLoader加载,加载失败则使用插件ClassLoader加载设置此新的ClassLoader为App运行的默认ClassLoader(替换context。mPackageInfo的ClassLoader,时机需要在Application的attachBaseContext())publicfinalclassPluginDelegateClassloaderextendsPathClassLoader{privatestaticBaseDexClassLoaderoriginClassLoader;PluginDelegateClassloader(ClassLoaderparent){super(,parent);originClassLoader(BaseDexClassLoader)parent;}privatestaticvoidreflectPackageInfoClassloader(ContextbaseContext,ClassLoaderreflectClassLoader)throwsException{ObjectpackageInfoHiddenApiReflection。findField(baseContext,mPackageInfo)。get(baseContext);if(packageInfo!null){HiddenApiReflection。findField(packageInfo,mClassLoader)。set(packageInfo,reflectClassLoader);}}publicstaticvoidinject(ClassLoaderoriginalClassloader,ContextbaseContext)throwsException{ContextctxbaseContext;while(ctxinstanceofContextWrapper){ctx((ContextWrapper)ctx)。getBaseContext();}PluginDelegateClassloaderclassloadernewPluginDelegateClassloader(originalClassloader);reflectPackageInfoClassloader(ctx,classloader);}OverrideprotectedClasslt;?findClass(Stringname)throwsClassNotFoundException{try{returnoriginClassLoader。loadClass(name);}catch(ClassNotFoundExceptionerror){SetPluginDexClassLoadersplitDexClassLoadersPluginClassLoaders。getInstance()。getClassLoaders();for(PluginDexClassLoaderloader:splitDexClassLoaders){Classlt;?clazzloader。loadClassItself(name);if(clazz!null){returnclazz;}}throwerror;}}OverridepublicClasslt;?loadClass(Stringname)throwsClassNotFoundException{returnfindClass(name);}OverridepublicStringfindLibrary(Stringname){StringlibNameoriginClassLoader。findLibrary(name);if(libNamenull){SetPluginDexClassLoadersplitDexClassLoadersPluginClassLoaders。getInstance()。getClassLoaders();for(PluginDexClassLoaderclassLoader:splitDexClassLoaders){libNameclassLoader。findLibraryItself(name);if(libName!null){break;}}}returnlibName;}}
  资源加载
  资源加载请见插件打包的PluginResourcesManager4。6遇到的问题1。启动阿里认证报Fatalsignal11(SIGSEGV),code1(SEGVMAPERR),faultaddr0x0intid28105(SGBackgroud)
  这个问题前后排查1周多,阿里认证使用了安全套件,其本身so为apk插件格式:
  反编译相关代码其使用的也是独立ClassLoader加载,最后经过二分、查看apk等方式,查找到原因,编译出的demoApk的METAINFO中没有签名信息,阿里安全套件对签名文件有检测。最后通过在demoapp中显示指定签名文件解决。2。独立资源appcompat库问题
  androidx。appcompat:appcompat
  阿里认证库已适配AndroidX,动态包由于使用的是独立的ClassLoader和Resources,如果插件包依赖appcompat会出现以下问题:
  最终,选取了插件包不依赖appcompat,统一交由宿主进行依赖,目前大多数App均已适配AndroidX,这种方案无需维护插件appcompat和宿主appcompat的版本,同时也能对插件本身进行瘦身。
  05hr总结插件化的原理其实不难,核心点就几个。各种插件化框架对于这些核心痛点也已经有了成熟的解决方案,目前插件化能在58App落地也是站在先驱的肩膀上,找到了最合适的方案进行微创新与落地。
  作者:况众文
  来源:微信公众号:58技术
  出处:https:mp。weixin。qq。comszKpjaHPMjKYjqBd4co4Itg

香港一日游最佳路线有哪些?很多人出差广东时,会利用一天的时间游玩一下香港。那么如何在有限的时间,尽可能的玩转香港,我给您推荐一条以游玩为主,逛街购物为辅的路线尖沙咀(海港城)星光大道天星小轮铜锣湾中环天平山车晓前夫如何败光百亿家产?22岁,他成为全国最年轻的民企掌门人23岁,他的企业成为全国民企中的第一纳税大户27岁,他身价125亿,成为最年轻的山西首富29岁,他在胡润百富中排名第85位商场得意情场更得意,他转年9月退休,当年还有必要参评高级教师吗?明年九月退休,今年参评高级教师有用吗?目前我国的中小学教师是全额事业编专业技术岗位。走的是专业职称路线,也就是说职称越高工资越高。基本工资是由岗位工资薪级工资工作津贴生活补贴四部分人类有没有可能被设计出来?人类有没有可能是被设计出来的?从地球诞生一直到人类诞生,期间经过了无数个物种,甚至有着比人类更加古老的生物,那么为何只有人类会脱颖而出?人类未来又将如何发展?都是一连串的谜团简单的人类有史以来最昂贵的太空望远镜即将升空!它的实力到底有多强?中国太空望远镜(CSST)也称载人空间站空天巡天望远镜,预计2024年发射后进入近地轨道,将和中国空间站在同一个轨道绕地飞行,暂定运行期10年。我国太空望远镜究竟实力有多强大?!达宇宙是客观存在的,还是神造的?我不谈什么宇宙!我只说这一个字,神!我实话实说,神,是存在的!可能有许多人不赞同这个说法!但是无论你们怎么不赞同我的说法?我是承认的!因为我多次的经历!是实实在在的存在!这是现代科有没有可能失踪的一些人,是被坐飞碟研究地球的外星人弄走了?完全可能!现在科学已证实蚂蚁生活在二维空间,你把它面前的食物提到它的头顶以上,对于蚂蚁而言这个食物就是凭空消失。据科学论断,宇宙有N个空间。人类生存在三维空间所看到的景象,就像蚂蚁南宁有哪些专科学校?来看下面的图吧,每个都列出来了!南宁的专科院校基本就这些了,有的学校已经由原本的专科升格为本科院校了,如果还有遗漏没写上去的欢迎大家补充。上面的这些学校,如果你要选择其中的报考志愿为什么在我们物理规则当中,破坏一个物体比建设一个物体容易?宇宙中有没有相反规则?这个问题看似简单,其实涉及到我们的宇宙是怎样搭建的,以及运作运作中最深沉的物理规则。这里我的答案只能算是抛砖引玉吧。破坏容易建设难破坏一个物体比建设一个物体容易。确实,我们在生活中如果孩子高一期间数学和物理考试都不及格,该怎么办?谢谢你的邀请,高一数学和物理都不及格,也不知道这个学生在初中时的,数学物理怎么样。如果在初中时数学物理的成绩还不错,到了高中一年级两门课的成绩不及格,说明这个学生对高中数学物理两门武侠版吃鸡来了,和传统大逃杀有什么异同?感觉一般般,首先奖励很一般,假如万灵金券也好啊,一开始,有啥装备带啥装备,别挑,活着才是资本,你要是没把握不是高玩,就猥琐拖到最后,注意搜集三种针和隐身,活用隐身,异宝出现了距离特
西北工业大学遭美国NSA网络攻击事件调查报告2022年6月22日,西北工业大学发布公开声明称,该校遭受境外网络攻击。陕西省西安市公安局碑林分局随即发布警情通报,证实在西北工业大学的信息网络中发现了多款源于境外的木马和恶意程序升级Android13,三星GalaxySIIINoteII用上LineageOS20IT之家9月27日消息,谷歌Android13已经推出一个多月了,但用上的机型目前还不多。不过好在AOSP的Android13蓬勃发展,近日,三星GalaxySIII和NoteII特斯拉机器人或已造出,马斯克十年内可买来送父母环球网科技综合报道有个机器人管家为你做早餐搬行李开车甚至照顾父母是什么体验?据特斯拉方面透露,北美时间2022年9月30日(预计北京时间10月1日),特斯拉2022AIDay活动将我的外卖可能正由机器人制作?预制菜外卖让餐饮机器人有了更多用武之地。对于餐饮这个相对传统的行业而言,解决厨师制作与售卖的问题,就满足了餐饮基础的发展条件。尤其在疫情下,节省人力成本和提升运营效率得到了越来越多上汽人工智能实验室与小马智行达成无人车运营服务合作出品搜狐汽车9月27日,小马智行官微发布消息,上汽集团人工智能实验室今日与其达成合作,共同探索自动驾驶技术研发及无人车运营服务落地等领域。双方已联合打造基于上汽MarvelR车型的9月27日每日一只短线强势股数据中心5G机器人硅能源区块链关于BS点(股票买点与股票卖点)研究参考文章BS点确认选股失效判断详解!BS点的研究请点击本段链接进入详阅!9月27日每日一只短线强势股XYKJ(静态B点13。680。06元)北京提振我国经济发展的几项重大突破光刻机人民币TSN铁路我在头条搞创作第二期西方的垄断曾让我国吃尽了苦头。我国在前行中总结经验提前布局,让人刮目相看,不得不服软。近期就有重大突破让人振奋。1。曾被不准向我国出口光刻机的荷兰主动向我国出售6则新发现,改变我们对人类起源的认知过去,我们对古人类的研究很容易让我们把自己描绘成比我们的祖先更聪明更有智慧和更有爱心的形象。但在过去的几年间,一些发现颠覆了这种观点。古人类学家PennySpikins认为,这些新时隔四个月再发声!贾跃亭回应与合伙人夺回FF控制权拨乱反正Tech星球9月27日消息,日前,贾跃亭率合伙人公司重组FF董事会,再获1亿美元融资的消息引发热议。今天中午,时隔四个月,贾跃亭个人社交账号发文称拨乱反正重回正轨,这是FF又一个重美元周期割韭菜的真正逻辑美元是全球货币,每一轮的加息周期和降息周期,都会引发全球流动性的变化,美国每次自身发展遇到问题,就会利用手上这个工具去收割全球,达到转移危机的目的。这顿骚操作,也被誉为美元潮汐,去创新科技赋能智慧医院建设5G为远程服务插上翅膀视频加载中面对我国当前医疗资源分布不均医疗服务供需缺口大等痛点,远程医疗服务是有效的解决途径。近些年,国家在远程医疗方面不断出台相关政策,持续释放政策利好。就在不久前,国务院办公厅
友情链接:快好找快生活快百科快传网中准网文好找聚热点快软网