安庆大理运城常德铜陵江西
投稿投诉
江西南阳
嘉兴昆明
铜陵滨州
广东西昌
常德梅州
兰州阳江
运城金华
广西萍乡
大理重庆
诸暨泉州
安庆南充
武汉辽宁

Android插件化的前世今生大揭秘

11月11日 龙凤殿投稿
  预备知识了解android基本开发。了解android四大组件基本原理。了解ClassLoader相关知识。看完本文可以达到什么程度了解插件化常见的实现原理阅读前准备工作
  1、cloneCommonTec项目
  https:github。com5A59androidtrainingtreemastercommontecCommonTec文章概览
  一、插件化框架历史
  整个插件化框架历史部分参考了包建强在2016GMTC全球开发大会上的演讲。
  https:www。infoq。cnarticleandroidpluginsfromentrytogiveup
  2012年AndroidDynamicLoader给予Fragment实现了插件化框架,可以动态加载插件中的Fragment实现页面的切换。
  https:github。commmin18AndroidDynamicLoader
  2013年23Code提供了一个壳,可以在壳里动态化下载插件然后运行。
  2013年阿里技术沙龙上,伯奎做了Atlas插件化框架的分享,说明那时候阿里已经在做插件化的运用和开发了。
  2014年任玉刚开源了dynamicloadapk,通过代理分发的方式实现了动态化,如果看过Android开发艺术探索这本书,应该会对这个方式有了解。
  https:github。comsingwhatiwannadynamicloadapk
  2015年张勇发布了DroidPlugin,使用hook系统方式实现插件化。
  https:github。comDroidPluginTeamDroidPlugin
  2015年携程发布DynamicApk。
  https:github。comCtripMobileDynamicAPK
  20152016之间(这块时间不太确定),Lody发布了VirtualApp,可以直接运行未安装的apk,基本上还是使用hook系统的方式实现的,不过里面的实现要精致很多,实现了自己的一套AMS来管理插件Activity等等。
  https:github。comasLodyVirtualApp
  2017年阿里推出Atlas。
  https:github。comapacheatlas
  2017年360推出RePlugin。
  https:github。comQihoo360RePlugin
  2017年滴滴推出VirtualApk。
  https:github。comdidiVirtualAPK
  2019年腾讯推出了Shadow,号称是零反射,并且框架自身也可实现动态化,看了代码以后发现,其实本质上还是使用了代理分发生命周期实现四大组件动态化,然后抽象接口来实现框架的动态化。后面有机会可以对其做一下分析。
  https:github。comTencentShadow
  这基本上就是插件化框架的历史,从2012至今,可以说插件化技术基本成型了,主要就是代理和hook系统两种方式(这里没有统计热修复的发展,热修复其实和插件化还是有些相通的地方,后面的文章会对热修复进行介绍)。如果看未来的话,斗胆预测,插件化技术的原理,应该不会有太大的变动了。二、名词解释
  在插件化中有一些专有名词,如果是第一次接触可能不太了解,这里解释一下。
  宿主
  负责加载插件的apk,一般来说就是已经安装的应用本身。
  StubActivity
  宿主中的占位Activity,注册在宿主Manifest文件中,负责加载插件Activity。
  PluginActivity
  插件Activity,在插件apk中,没有注册在Manifest文件中,需要StubActivity来加载。三、使用gradle简化插件开发流程
  在学习和开发插件化的时候,我们需要动态去加载插件apk,所以开发过程中一般需要有两个apk,一个是宿主apk,一个是插件apk,对应的就需要有宿主项目和插件项目。
  在CommonTec这里创建了app作为宿主项目,plugin为插件项目。为了方便,我们直接把生成的插件apk放到宿主apk中的assets中,apk启动时直接放到内部存储空间中方便加载。
  这样的项目结构,我们调试问题时的流程就是下面这样:
  修改插件项目编译生成插件apk拷贝插件apk到宿主assets修改宿主项目编译生成宿主apk安装宿主apk验证问题。
  如果每次我们修改一个很小的问题,都经历这么长的流程,那么耐心很快就耗尽了。最好是可以直接编译宿主apk的时候自动打包插件apk并拷贝到宿主assets目录下,这样我们不管修改什么,都直接编译宿主项目就好了。如何实现呢?还记得我们之前讲解过的gradle系列么?现在就是学以致用的时候了。
  首先在plugin项目的build。gradle添加下面的代码:project。afterEvaluate{project。tasks。each{if(it。nameassembleDebug){it。doLast{copy{fromnewFile(project。getBuildDir(),outputsapkdebugplugindebug。apk)。absolutePathintonewFile(project。getRootProject()。getProjectDir(),appsrcmainassets)renameplugindebug。apk,plugin。apk}}}}}
  这段代码是在afterEvaluate的时候,遍历项目的task,找到打包task也就是assembleDebug,然后在打包之后,把生成的apk拷贝到宿主项目的assets目录下,并且重命名为plugin。apk。然后在app项目的build。gradle添加下面的代码:project。afterEvaluate{project。tasks。each{if(it。namemergeDebugAssets){it。dependsOn:plugin:assembleDebug}}}
  找到宿主打包的mergeDebugAssets任务,依赖插件项目的打包,这样每次编译宿主项目的时候,会先编译插件项目,然后拷贝插件apk到宿主apk的assets目录下,以后每次修改,只要编译宿主项目就可以了。四、ClassLoader
  ClassLoader是插件化中必须要掌握的,因为插件是未安装的apk,系统不会处理其中的类,所以需要我们自己来处理。
  4。1java中的ClassLoader
  BootstrapClassLoader
  负责加载JVM运行时的核心类,比如JAVAHOMElibrt。jar等等。
  ExtensionClassLoader
  负责加载JVM的扩展类,比如JAVAHOMElibext下面的jar包。
  AppClassLoader
  负责加载classpath里的jar包和目录。
  4。2android中的ClassLoader
  在这里,我们统称dex文件,包含dex的apk文件以及jar文件为dex文件PathClassLoader用来加载系统类和应用程序类,可以加载已经安装的apk目录下的dex文件。
  DexClassLoader用来加载dex文件,可以从存储空间加载dex文件。
  我们在插件化中一般使用的是DexClassLoader。
  4。3双亲委派机制
  每一个ClassLoader中都有一个parent对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果parent为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。下面是ClassLoader的loadClass方法的具体实现。protectedC?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{First,checkiftheclasshasalreadybeenloadedC?cfindLoadedClass(name);if(cnull){try{if(parent!null){先从父类加载器中进行加载cparent。loadClass(name,false);}else{cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){ClassNotFoundExceptionthrownifclassnotfoundfromthenonnullparentclassloader}if(cnull){没有找到,再自己加载cfindClass(name);}}}
  4。4如何加载插件中的类
  要加载插件中的类,我们首先要创建一个DexClassLoader,先看下DexClassLoader的构造函数需要哪些参数。publicclassDexClassLoaderextendsBaseDexClassLoader{publicDexClassLoader(StringdexPath,StringoptimizedDirectory,StringlibrarySearchPath,ClassLoaderparent){。。。}}
  构造函数需要四个参数:
  dexPath是需要加载的dexapkjar文件路径。
  optimizedDirectory是dex优化后存放的位置,在ART上,会执行oat对dex进行优化,生成机器码,这里就是存放优化后的odex文件的位置。
  librarySearchPath是native依赖的位置。
  parent就是父类加载器,默认会先从parent加载对应的类。
  创建出DexClassLaoder实例以后,只要调用其loadClass(className)方法就可以加载插件中的类了。具体的实现在下面:从assets中拿出插件apk放到内部存储空间privatefunextractPlugin(){varinputStreamassets。open(plugin。apk)File(filesDir。absolutePath,plugin。apk)。writeBytes(inputStream。readBytes())}privatefuninit(){extractPlugin()pluginPathFile(filesDir。absolutePath,plugin。apk)。absolutePathnativeLibDirFile(filesDir,pluginlib)。absolutePathdexOutPathFile(filesDir,dexout)。absolutePath生成DexClassLoader用来加载插件类pluginClassLoaderDexClassLoader(pluginPath,dexOutPath,nativeLibDir,this::class。java。classLoader)}五、插件化需要解决的难点
  插件化,就是从插件中加载我们想要的类并运行,如果这个类是一个普通类,那么使用上面说到的DexClassLoader就可以直接加载了,如果这个类是特殊的类,比如说Activity等四大组件,那么就需要一些特殊的处理,因为四大组件是需要和系统进行交互的。插件化中,四大组件需要解决的难点如下:
  Activity生命周期如何调用。如何使用插件中的资源。
  Service生命周期如何调用。
  BroadcastReceiver静态广播和动态广播的注册。
  ContentProvider如何注册插件Provider到系统。六、Activity的插件化实现
  6。1难点分析
  我们之前说到Activity插件化的难点,我们先来理顺一下为什么会有这两个问题。
  因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的Manifest文件中,而没有在Manifest中注册的四大组件是不能和系统直接进行交互的。
  可能有些同学会问,那为什么不能直接把插件的Activity注册到宿主Manifest里呢?这样是可以,不过就失去了插件化的动态特性,如果每次插件中新增Activity都要修改宿主Manifest并且重新打包,那就和直接写在宿主中没什么区别了。
  我们再来说一下为什么没有注册的Activity不能和系统交互。
  这里的不能直接交互的含义有两个:
  1、系统会检测Activity是否注册如果我们启动一个没有在Manifest中注册的Activity,会发现报如下error:android。content。ActivityNotFoundException:Unabletofindexplicitactivityclass{com。zy。commonteccom。zy。plugin。PluginActivity};haveyoudeclaredthisactivityinyourAndroidManifest。xml?
  这个log在Instrumentation的checkStartActivityResult方法中可以看到:publicclassInstrumentation{publicstaticvoidcheckStartActivityResult(intres,Objectintent){if(!ActivityManager。isStartResultFatalError(res)){}switch(res){caseActivityManager。STARTINTENTNOTRESOLVED:caseActivityManager。STARTCLASSNOTFOUND:if(intentinstanceofIntent((Intent)intent)。getComponent()!null)thrownewActivityNotFoundException(Unabletofindexplicitactivityclass((Intent)intent)。getComponent()。toShortString();haveyoudeclaredthisactivityinyourAndroidManifest。xml?);thrownewActivityNotFoundException(NoActivityfoundtohandleintent);。。。}}}
  Activity的生命周期无法被调用其实一个Activity主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了Manifest注册文件,启动Activity被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件Activity也就不能正常运行了。
  2、其实上面两个问题,最终都指向同一个难点,那就是插件中的Activity的生命周期如何被调用。解决问题之前我们先看一下正常系统是如何启动一个Activity的。
  这里对Activity的启动流程进行一些简单的介绍,具体的流程代码就不分析了,因为分析的话大概又能写一篇文章了,而且其实关于Activity的启动过程也有不少文章有分析了。这里放一张简图说明一下:
  整个调用路径如下:Activity。startActivityInstrumentation。execStartActivityBinderAMS。startActivityActivityStarter。startActivityMayWaitstartActivityLockedstartActivityUnCheckedActivityStackSupervisor。resumeFocusedStackTopActivityLockedActivityStatk。resumeTopAcitivityUncheckLockedresumeTopActivityInnerLockedActivityStackSupervisor。startSpecificActivityLockedrealStartActivityLockedBinderApplictionThread。scheduleLauchActivityHActivityThread。scheduleLauchActivityhandleLaunchActivityperformLaunchActivityInstrumentation。newActivity创建ActivitycallActivityOnCreate一系列生命周期
  其实我们可以把AMS理解为一个公司的背后大Boss,Activity相当于小职员,没有权限直接和大Boss说话,想做什么事情都必须经过秘书向上汇报,然后秘书再把大BossAMS的命令传达下来。而且大Boss那里有所有职员的名单,如果想要混入非法职员是不可能的。而我们想让没有在大Boss那里注册的编外人员执行任务,只有两种方法,一种是正式职员领取任务,再分发给编外人员,另一种就是欺骗Boss,让Boss以为这个职员是已经注册的。
  对应到实际的解决方法就是:我们手动去调用插件Activity的生命周期。欺骗系统,让系统以为Activity是注册在Manifest中的。
  说完生命周期的问题,再来看一下资源的问题。
  在Activity中,基本上都会展示界面,而展示界面基本上都要用到资源。
  在Activity中,有一个mResources变量,是Resources类型。这个变量可以理解为代表了整个apk的资源。
  在宿主中调用的Activity,mResources自然代表了宿主的资源,所以需要我们对插件的资源进行特殊的处理。
  我们先看一下如何生成代表插件资源的Resources类。
  首先要生成一个AssetManager实例,然后通过其addAssetPath方法添加插件的路径,这样AssetManager中就包含了插件的资源。然后通过Resources构造函数生成插件资源。具体代码如下:privatefunhandleResources(){try{首先通过反射生成AssetManager实例pluginAssetManagerAssetManager::class。java。newInstance()然后调用其addAssetPath把插件的路径添加进去。valaddAssetPathMethodpluginAssetManager?。javaClass?。getMethod(addAssetPath,String::class。java)addAssetPathMethod?。invoke(pluginAssetManager,pluginPath)}catch(e:Exception){}调用Resources构造函数生成实例pluginResourcesResources(pluginAssetManager,super。getResources()。displayMetrics,super。getResources()。configuration)}
  前期准备的知识点差不多介绍完了,我们接着就看看具体的实现方法。
  6。2手动调用Activity生命周期
  手动调用生命周期原理如下图:
  我们手动调用插件Activity生命周期时,需要在正确的时机去调用,如何在正确的时机调用呢?那就是启动一个真正的Activity,我们俗称占坑Activity(StubActivity),然后在StubActivity的生命周期里调用插件Activity对应的生命周期,这样就间接的启动了插件Activity。
  在StubActivity中调用插件Activity生命周期的方法有两种,一种是直接反射其生命周期方法,粗暴简单,唯一的缺点就是反射的效率问题。另外一种方式就是生成一个接口,接口里对应的是生命周期方法,让插件Activity实现这个接口,在StubActivity里就能直接调用接口方法了,从而避免了反射的效率低下问题。
  具体的代码实现在CommonTec项目里可以找到,这里贴一下主要的实现(这里的实现和CommonTec里的可能会有些区别,CommonTec里有些代码做了一些封装,这里主要做原理的解释)。
  6。2。1通过反射调用Activity生命周期
  具体的实现见反射调用生命周期,下面列出了重点代码。
  https:github。com5A59androidtrainingtreemastercommontecCommonTecappsrcmainjavacomzycommontecactivityreflectclassStubReflectActivity:Activity(){protectedvaractivityClassLoader:ClassLoader?nullprotectedvaractivityNameprivatevarpluginPathprivatevarnativeLibDir:String?nullprivatevardexOutPath:String?nulloverridefunonCreate(savedInstanceState:Bundle?){super。onCreate(savedInstanceState)nativeLibDirFile(filesDir,pluginlib)。absolutePathdexOutPathFile(filesDir,dexout)。absolutePathpluginPathintent。getStringExtra(pluginPath)activityNameintent。getStringExtra(activityName)创建插件ClassLoaderactivityClassLoaderDexClassLoader(pluginPath,dexOutPath,nativeLibDir,this::class。java。classLoader)}以onCreate方法为例,其他onStart等生命周期方法类似funonCreate(savedInstanceState:Bundle?){获取插件Activity的onCreate方法并调用getMethod(onCreate,Bundle::class。java)?。invoke(activity,savedInstanceState)}fungetMethod(methodName:String,varargparams:Class):Method?{returnactivityClassLoader?。loadClass(activity)?。getMethod(methodName,params)}}
  6。2。2通过接口调用Activity生命周期
  具体的实现见接口调用生命周期,下面列出了重点代码。通过接口调用Activity生命周期的前提是要定义一个接口IPluginActivity。
  https:github。com5A59androidtrainingtreemastercommontecCommonTecappsrcmainjavacomzycommontecactivityainterfaceinterfaceIPluginActivity{funattach(proxyActivity:Activity)funonCreate(savedInstanceState:Bundle?)funonStart()funonResume()funonPause()funonStop()funonDestroy()}
  然后在插件Activity中实现这个接口。openclassBasePluginActivity:Activity(),IPluginActivity{varproxyActivity:Activity?nulloverridefunattach(proxyActivity:Activity){this。proxyActivityproxyActivity}overridefunonCreate(savedInstanceState:Bundle?){if(proxyActivitynull){super。onCreate(savedInstanceState)}}。。。}
  在StubActivity通过接口调用插件Activity生命周期。classStubInterfaceActivity:StubBaseActivity(){protectedvaractivityClassLoader:ClassLoader?nullprotectedvaractivityNameprivatevarpluginPathprivatevaractivity:IPluginActivity?nulloverridefunonCreate(savedInstanceState:Bundle?){super。onCreate(savedInstanceState)nativeLibDirFile(filesDir,pluginlib)。absolutePathdexOutPathFile(filesDir,dexout)。absolutePathpluginPathintent。getStringExtra(pluginPath)activityNameintent。getStringExtra(activityName)生成插件ClassLoaderactivityClassLoaderDexClassLoader(pluginPath,dexOutPath,nativeLibDir,this::class。java。classLoader)加载插件Activity类并转化成IPluginActivity接口activityactivityClassLoader?。loadClass(activityName)?。newInstance()asIPluginActivity?activity?。attach(this)通过接口直接调用对应的生命周期方法activity?。onCreate(savedInstanceState)}}
  6。2。3资源处理方式
  由于手动调用生命周期的方式,会重写大量的Activity生命周期方法,所以我们只要重写getResources方法,返回插件的资源实例就可以了。下面是具体代码。openclassStubBaseActivity:Activity(){protectedvaractivityClassLoader:ClassLoader?nullprotectedvaractivityNameprivatevarpluginPathprivatevarpluginAssetManager:AssetManager?nullprivatevarpluginResources:Resources?nullprivatevarpluginTheme:Resources。Theme?nullprivatevarnativeLibDir:String?nullprivatevardexOutPath:String?nulloverridefunonCreate(savedInstanceState:Bundle?){super。onCreate(savedInstanceState)nativeLibDirFile(filesDir,pluginlib)。absolutePathdexOutPathFile(filesDir,dexout)。absolutePathpluginPathintent。getStringExtra(pluginPath)activityNameintent。getStringExtra(activityName)activityClassLoaderDexClassLoader(pluginPath,dexOutPath,nativeLibDir,this::class。java。classLoader)handleResources()}overridefungetResources():Resources?{这里返回插件的资源,这样插件Activity中使用的就是插件资源了returnpluginResources?:super。getResources()}overridefungetAssets():AssetManager{returnpluginAssetManager?:super。getAssets()}overridefungetClassLoader():ClassLoader{returnactivityClassLoader?:super。getClassLoader()}privatefunhandleResources(){try{生成AssetManagerpluginAssetManagerAssetManager::class。java。newInstance()添加插件apk路径valaddAssetPathMethodpluginAssetManager?。javaClass?。getMethod(addAssetPath,String::class。java)addAssetPathMethod?。invoke(pluginAssetManager,pluginPath)}catch(e:Exception){}生成插件资源pluginResourcesResources(pluginAssetManager,super。getResources()。displayMetrics,super。getResources()。configuration)}}
  6。3hook系统相关实现的方式欺骗系统,让系统调用生命周期6。3。1hookInstrumentation
  上面讲了如何通过手动调用插件Activity的生命周期方法来启动插件Activity,现在来看一下欺骗系统的方法。
  上面简单介绍了Activity的启动流程,我们可以看到,其实Android系统的运行是很巧妙的,AMS是系统服务,应用通过Binder和AMS进行交互,其实和我们日常开发中客户端和服务端交互有些类似,只不过这里使用了Binder做为交互方式,关于Binder,可以简单看看这篇文章。我们暂时只要知道通过Binder应用可以和AMS进行对话就行。
  https:juejin。cnpost6844903882133356558
  这种架构的设计方式,也为我们提供了一些机会。理论上来说,我们只要在启动Activity的消息到达AMS之前把Activity的信息就行修改,然后再消息回来以后再把信息恢复,就可以达到欺骗系统的目的了。
  在这个流程里,有很多hook点可以进行,而且不同的插件化框架对于hook点的选择也不同,这里我们选择hookInstrumentation的方式进行介绍(原因是个人感觉这种方式要简单一点)。
  简化以后的流程如下:
  Instrumentation相当于Activity的管理者,Activity的创建,以及生命周期的调用都是AMS通知以后通过Instrumentation来调用的。
  我们上面说到,AMS相当于一个公司的背后大Boss,而Instrumentation相当于秘书,Activity相当于小职员,没有权限直接和大Boss说话,想做什么事情都必须经过秘书向上汇报,然后Instrumentation再把大BossAMS的命令传达下来。而且大Boss那里有所有职员的名单,如果想要混入非法职员是不可能的。
  不过在整个过程中,由于java的语言特性,大Boss在和秘书Instrumentation对话时,不会管秘书到底是谁,只会确认这个人是不是秘书(是否是Instrumentation类型)。
  我们加载插件中的Activity,相当于让一个不在Boss名单上的编外职员去申请执行任务。在正常情况下,大Boss会检查职员的名单,确认职员的合法性,一定是通过不了的。但是上有政策,下有对策,我们悄悄的替换了秘书,在秘书和Boss汇报时,把职员名字改成大Boss名单中的职员,在Boss安排工作以后,秘书再把名字换回来,让编外职员去执行任务。
  而我们hook的方式就是替换调Instrumentation,修改Activity类名,达到隐瞒AMS的效果。
  hook方式原理图
  接下来看看具体的代码实现。具体的实现见hook实现插件化,下面主要讲解重点代码。
  替换Instrumentation之前,首先我们要实现一个我们自己的Instrumentation,具体实现如下:
  https:github。com5A59androidtrainingtreemastercommontecCommonTecappsrcmainjavacomzycommontecactivityhookclassAppInstrumentation(varrealContext:Context,varbase:Instrumentation,varpluginContext:PluginContext):Instrumentation(){privatevalKEYCOMPONENTcommonteccomponentcompanionobject{funinject(activity:Activity,pluginContext:PluginContext){hook系统,替换Instrumentation为我们自己的AppInstrumentation,Reflect是从VirtualApp里拷贝的反射工具类,使用很流畅varreflectReflect。on(activity)varactivityThreadreflect。get(mMainThread)varbaseReflect。on(activityThread)。getInstrumentation(mInstrumentation)varappInstrumentationAppInstrumentation(activity,base,pluginContext)Reflect。on(activityThread)。set(mInstrumentation,appInstrumentation)Reflect。on(activity)。set(mInstrumentation,appInstrumentation)}}overridefunnewActivity(cl:ClassLoader,className:String,intent:Intent):Activity?{创建Activity的时候会调用这个方法,在这里需要返回插件Activity的实例valcomponentNameintent。getParcelableExtraComponentName(KEYCOMPONENT)varclazzpluginContext。classLoader。loadClass(componentName。className)intent。componentcomponentNamereturnclazz。newInstance()asActivity?}privatefuninjectIntent(intent:Intent?){varcomponent:ComponentName?nullvaroldComponentintent?。componentif(componentnullcomponent。packageNamerealContext。packageName){替换intent中的类名为占位Activity的类名,这样系统在Manifest中查找的时候就可以找到ActivitycomponentComponentName(com。zy。commontec,com。zy。commontec。activity。hook。HookStubActivity)intent?。componentcomponentintent?。putExtra(KEYCOMPONENT,oldComponent)}}funexecStartActivity(who:Context,contextThread:IBinder,token:IBinder,target:Activity,intent:Intent,requestCode:Int):Instrumentation。ActivityResult?{启动activity的时候会调用这个方法,在这个方法里替换Intent中的ClassName为已经注册的宿主ActivityinjectIntent(intent)returnReflect。on(base)。call(execStartActivity,who,contextThread,token,target,intent,requestCode)。get()}。。。}
  在AppInstrumentation中有两个关键点,execStartActivity和newActivity。
  execStartActivity是在启动Activity的时候必经的一个过程,这时还没有到达AMS,所以,在这里把Activity替换成宿主中已经注册的StubActivity,这样AMS在检测Activity的时候就认为已经注册过了。newActivity是创建Activity实例,这里要返回真正需要运行的插件Activity,这样后面系统就会基于这个Activity实例来进行对应的生命周期的调用。
  6。3。2hook系统的资源处理方式
  因为我们hook了Instrumentation的实现,还是把Activity生命周期的调用交给了系统,所以我们的资源处理方式和手动调用生命周期不太一样,这里我们生成Resources以后,直接反射替换掉Activity中的mResource变量即可。下面是具体代码。classAppInstrumentation(varrealContext:Context,varbase:Instrumentation,varpluginContext:PluginContext):Instrumentation(){privatefuninjectActivity(activity:Activity?){valintentactivity?。intentvalbaseactivity?。baseContexttry{反射替换mResources资源Reflect。on(base)。set(mResources,pluginContext。resources)Reflect。on(activity)。set(mResources,pluginContext。resources)Reflect。on(activity)。set(mBase,pluginContext)Reflect。on(activity)。set(mApplication,pluginContext。applicationContext)fornativeactivityvalcomponentNameintent!!。getParcelableExtraComponentName(KEYCOMPONENT)valwrapperIntentIntent(intent)wrapperIntent。setClassName(componentName。packageName,componentName。className)activity。intentwrapperIntent}catch(e:Exception){}}overridefuncallActivityOnCreate(activity:Activity?,icicle:Bundle?){在这里进行资源的替换injectActivity(activity)super。callActivityOnCreate(activity,icicle)}}publicclassPluginContextextendsContextWrapper{privatevoidgenerateResources(){try{反射生成AssetManager实例assetManagerAssetManager。class。newInstance();调用addAssetPath添加插件路径MethodmethodassetManager。getClass()。getMethod(addAssetPath,String。class);method。invoke(assetManager,pluginPath);生成Resources实例resourcesnewResources(assetManager,context。getResources()。getDisplayMetrics(),context。getResources()。getConfiguration());}catch(Exceptione){e。printStackTrace();}}}
  讲完上面两种方法,我们这里对比一下这两种方法的优缺点:
  七、Service的插件化实现
  Service比起Activity要简单不少,Service没有太复杂的生命周期需要处理,类似的onCreate或者onStartCommand可以直接通过代理分发。可以直接在宿主app里添加一个占位Service,然后在对应的生命周期里调用插件Service的生命周期方法即可。classStubService:Service(){varserviceName:String?nullvarpluginService:Service?nullcompanionobject{varpluginClassLoader:ClassLoader?nullfunstartService(context:Context,classLoader:ClassLoader,serviceName:String){pluginClassLoaderclassLoadervalintentIntent(context,StubService::class。java)intent。putExtra(serviceName,serviceName)context。startService(intent)}}overridefunonStartCommand(intent:Intent?,flags:Int,startId:Int):Int{valressuper。onStartCommand(intent,flags,startId)serviceNameintent?。getStringExtra(serviceName)pluginServicepluginClassLoader?。loadClass(serviceName)?。newInstance()asServicepluginService?。onCreate()returnpluginService?。onStartCommand(intent,flags,startId)?:res}overridefunonDestroy(){super。onDestroy()pluginService?。onDestroy()}overridefunonBind(intent:Intent?):IBinder?{returnnull}}八、BroadcastReceiver的插件化实现
  动态广播的处理也比较简单,也没有复杂的生命周期,也不需要在Manifest中进行注册,使用的时候直接注册即可。所以只要通过ClassLoader加载插件apk中的广播类然后直接注册就好。classBroadcastUtils{companionobject{privatevalbroadcastMapHashMapString,BroadcastReceiver()funregisterBroadcastReceiver(context:Context,classLoader:ClassLoader,action:String,broadcastName:String){valreceiverclassLoader。loadClass(broadcastName)。newInstance()asBroadcastReceivervalintentFilterIntentFilter(action)context。registerReceiver(receiver,intentFilter)broadcastMap〔action〕receiver}fununregisterBroadcastReceiver(context:Context,action:String){valreceiverbroadcastMap。remove(action)context。unregisterReceiver(receiver)}}}
  静态广播稍微麻烦一点,这里可以解析Manifest文件找到其中静态注册的Broadcast并进行动态注册,这里就不对Manifest进行解析了,知道其原理即可。九、ContentProvider的插件化实现
  其实在日常开发中对于插件化中的ContentProvider使用还是比较少的,这里只介绍一种比较简单的ContentProvider插件化实现方法,就是类似Service,在宿主app中注册占位ContentProvider,然后转发相应的操作到插件ContentProvider中。代码如下:classStubContentProvider:ContentProvider(){privatevarpluginProvider:ContentProvider?nullprivatevaruriMatcher:UriMatcher?UriMatcher(UriMatcher。NOMATCH)overridefuninsert(uri:Uri?,values:ContentValues?):Uri?{loadPluginProvider()returnpluginProvider?。insert(uri,values)}overridefunquery(uri:Uri?,projection:ArrayoutString?,selection:String?,selectionArgs:ArrayoutString?,sortOrder:String?):Cursor?{loadPluginProvider()if(isPlugin1(uri)){returnpluginProvider?。query(uri,projection,selection,selectionArgs,sortOrder)}returnnull}overridefunonCreate():Boolean{uriMatcher?。addURI(com。zy。stubprovider,plugin1,0)uriMatcher?。addURI(com。zy。stubprovider,plugin2,0)returntrue}overridefunupdate(uri:Uri?,values:ContentValues?,selection:String?,selectionArgs:ArrayoutString?):Int{loadPluginProvider()returnpluginProvider?。update(uri,values,selection,selectionArgs)?:0}overridefundelete(uri:Uri?,selection:String?,selectionArgs:ArrayoutString?):Int{loadPluginProvider()returnpluginProvider?。delete(uri,selection,selectionArgs)?:0}overridefungetType(uri:Uri?):String{loadPluginProvider()returnpluginProvider?。getType(uri)?:}privatefunloadPluginProvider(){if(pluginProvidernull){pluginProviderPluginUtils。classLoader?。loadClass(com。zy。plugin。PluginContentProvider)?。newInstance()asContentProvider?}}privatefunisPlugin1(uri:Uri?):Boolean{if(uriMatcher?。match(uri)0){returntrue}returnfalse}}
  这里面需要处理的就是,如何转发对应的Uri到正确的插件Provider中呢,解决方案是在Uri中定义不同的插件路径,比如plugin1的Uri对应就是content:com。zy。stubproviderplugin1,plugin2对应的uri就是content:com。zy。stubproviderplugin2,然后在StubContentProvider中根据对应的plugin分发不同的插件Provider。总结
  本文介绍了插件化的相关实现,主要集中在Activity的实现上。重点如下:
  推荐大家在学习插件化的同时,也去学习一些四大组件以及Binder的系统实现最后
  在这里还分享一份由大佬亲自收录整理的学习PDF架构视频面试文档源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料
  这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。
  当然,你也可以拿去查漏补缺,提升自身的竞争力。
  真心希望可以帮助到大家,Android路漫漫,共勉!
  如果你有需要的话,只需私信我【进阶】即可获取
投诉 评论 转载

西媒报道中国在电动汽车赛道遥遥领先参考消息网3月1日报道西班牙《阿贝赛报》网站2月27日发表题为《中国攻克电动汽车极点》的报道,报道称,与以往的工业革命不同,这一次中国正处于主导地位。全文摘编如下:引擎盖……中国最神秘车企,未卖出一辆车却已有几千亿市值,恒大是怎样做的这次的上海车展,焦点不仅有特斯拉,还有恒驰汽车,一直被外界传言是PPT造车的恒大,直接带着9款汽车亮相,引起了许多人的关注,自从恒大宣布造车以来,它的争议就从未断过,而这家未卖……1450元起售?红米6A同款四核芯片,TCL新机正式发布作为传统家电厂商,TCL早早布局智能手机市场,可惜多重原因影响未能站稳脚跟,四五年前基本淡出人们的视野。最新消息显示,TCL旗下一款入门手机发布,搭载联发科多年前的四核低端芯片……为什么越来越多的货主企业选择加入笛笛叫船?近年来,物流业已经成为新技术、新模式、新业态的最好的孵化地,科技作为催化剂的能量正在全面展现。而随着互联网信息技术的变革与发展,传统物流正在向着网络货运迈进,逐步去解决传统物流……程序员喜欢的编辑器有哪些?今天来给大家推荐的是程序员常用的编辑器,一个编辑器能决定你写代码的心情。以下推荐一些比较好用的编辑器希望可以提高你的开发效率。SublimetextSublimet……NASA好奇号在火星上探测到新有机分子科技日报北京11月4日电(记者刘霞)美国国家航空航天局(NASA)戈达德航天飞行中心的一个国际研究小组利用好奇号火星车上的一项新实验,在火星上发现了以前未知的有机分子。研究结果……Android插件化的前世今生大揭秘预备知识了解android基本开发。了解android四大组件基本原理。了解ClassLoader相关知识。看完本文可以达到什么程度了解插件化常见的实现原理阅读前准备工作……中国广电会不会变成下一个中国移动?近日,中国广播电视网络集团有限公司发生工商变更,经营范围新增5G通信技术服务、移动通信设备销售等,这进一步预示中国广电即将放号运营。当前市场普遍预期,在今年的517世界电……微信只清理聊天记录和没删一样,学会这一招,多清理几倍,涨知识大家好,我是妙妙,我们平常都会使用微信进行聊天,出门的时候也会使用微信的电子支付。但是我们的微信用久了之后,大家也可以发现它里边产生的聊天垃圾是很多的。这样就会很占我们的……用好患者评价机制推动医院高质量发展来源:北京青年报据报道,在部分有评价功能的互联网平台上,患者对医院的评价越来越引人注目,一些影响大、知名度高的三甲医院、专科医院不时遭遇差评,有些医院的评论区内,医生也频频中招……为什么很多人都说百度是家缺德的公司?我不是IT界人士,但听到很多此类说法。比如这里有篇文章http:www。williamlong。infoarchives376。html,韩寒因为版权的问题也骂过李彦宏。但想知……云端争霸是场持久战本周,国际研究机构高德纳(Gartner)发布最新报告,全面评估全球顶级云厂商的整体能力。其中,阿里云拿下IaaS基础设施能力全球第一,在计算、存储、网络、安全四项核心评比中均……
一些奇奇怪怪的脑洞海信创维TCL等参展CES2022,新兴显示技术再争艳追求人际交流慢生活法国里昂拟限制网购店铺数量宇宙的真实物理模型解析电子城中的有些电脑都修改了cpu型号,他们是怎么改的,用软件有没有闲人可以做的兼职推荐?英国在华为内部安插间谍被实锤,被查了个遍的华为有何秘密?为什么显卡厂商有这么多,CPU厂商只有inter和amd?谁是Z世代的脑白金?雷柏ralemoPre5慕斯机械键盘,属于新女性的艺术品国产操作系统厂商统信发力生态建设,已在金融教育行业规模化推广小巧美丽,功能强大,520礼物首选坚果G9张勇后创业者的移动安全梦致女生的唯美说说每个女人都应该学会该如何转身在适美不胜收的馒头妈妈孩子吃完开心一整天心情文案不乱于心,不困于情。不畏将来,不念过往。如此,安好童年趣事历年江苏省中考满分作文春天,毕竟是春天在工作的感受是怎样的没想到我能成功特斯拉计划进军印度市场明年1月或上线订单配置功能怎么样洗脸可以收缩毛孔呢浅谈领导讲话稿的写作曾定有《坐井观天》教案

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找七猫云易事利