范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

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

  预备知识了解 android 基本开发。  了解 android 四大组件基本原理。  了解 ClassLoader 相关知识。  看完本文可以达到什么程度了解插件化常见的实现原理  阅读前准备工作
  1、clone  CommonTec  项目
  https://github.com/5A59/android-training/tree/master/common-tec/CommonTec  文章概览
  一、插件化框架历史
  整个插件化框架历史部分参考了 包建强在 2016GMTC 全球开发大会上的演讲 。
  https://www.infoq.cn/article/android-plug-ins-from-entry-to-give-up/
  2012 年  AndroidDynamicLoader  给予 Fragment 实现了插件化框架,可以动态加载插件中的 Fragment 实现页面的切换。
  https://github.com/mmin18/AndroidDynamicLoader
  2013 年 23Code 提供了一个壳,可以在壳里动态化下载插件然后运行。
  2013 年 阿里技术沙龙上,伯奎做了 Atlas 插件化框架的分享,说明那时候阿里已经在做插件化的运用和开发了。
  2014 年 任玉刚开源了  dynamic-load-apk ,通过代理分发的方式实现了动态化,如果看过 Android 开发艺术探索这本书,应该会对这个方式有了解。
  https://github.com/singwhatiwanna/dynamic-load-apk
  2015 年 张勇 发布了  DroidPlugin ,使用 hook 系统方式实现插件化。
  https://github.com/DroidPluginTeam/DroidPlugin
  2015 年 携程发布  DynamicApk 。
  https://github.com/CtripMobile/DynamicAPK
  2015 - 2016 之间(这块时间不太确定),Lody 发布了  VirtualApp ,可以直接运行未安装的 apk,基本上还是使用 hook 系统的方式实现的,不过里面的实现要精致很多,实现了自己的一套 AMS 来管理插件 Activity 等等。
  https://github.com/asLody/VirtualApp
  2017 年阿里推出  Atlas 。
  https://github.com/apache/atlas
  2017 年 360 推出  RePlugin 。
  https://github.com/Qihoo360/RePlugin
  2017 年滴滴推出  VirtualApk 。
  https://github.com/didi/VirtualAPK
  2019 年腾讯推出了  Shadow ,号称是零反射,并且框架自身也可实现动态化,看了代码以后发现,其实本质上还是使用了代理分发生命周期实现四大组件动态化,然后抽象接口来实现框架的动态化。后面有机会可以对其做一下分析。
  https://github.com/Tencent/Shadow
  这基本上就是插件化框架的历史,从 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.name == "assembleDebug") {             it.doLast {                 copy {                     from new File(project.getBuildDir(), "outputs/apk/debug/plugin-debug.apk").absolutePath                     into new File(project.getRootProject().getProjectDir(), "app/src/main/assets")                     rename "plugin-debug.apk", "plugin.apk"                 }             }         }     } }
  这段代码是在   afterEvaluate    的时候,遍历项目的 task,找到打包 task 也就是assembleDebug   ,然后在打包之后,把生成的 apk 拷贝到宿主项目的 assets 目录下,并且重命名为 plugin.apk   。然后在 app 项目的 build.gradle    添加下面的代码:project.afterEvaluate {     project.tasks.each {         if (it.name == "mergeDebugAssets") {             it.dependsOn ":plugin:assembleDebug"         }     } }
  找到宿主打包的   mergeDebugAssets    任务,依赖插件项目的打包,这样每次编译宿主项目的时候,会先编译插件项目,然后拷贝插件 apk 到宿主 apk 的 assets 目录下,以后每次修改,只要编译宿主项目就可以了。四、ClassLoader
  ClassLoader 是插件化中必须要掌握的,因为插件是未安装的 apk,系统不会处理其中的类,所以需要我们自己来处理。
  4.1 java 中的 ClassLoader
  BootstrapClassLoader
  负责加载 JVM 运行时的核心类,比如   JAVA_HOME/lib/rt.jar    等等。
  ExtensionClassLoader
  负责加载 JVM 的扩展类,比如   JAVA_HOME/lib/ext    下面的 jar 包。
  AppClassLoader
  负责加载 classpath 里的 jar 包和目录。
  4.2 android 中的 ClassLoader
  在这里,我们统称 dex 文件,包含 dex 的 apk 文件以及 jar 文件为 dex 文件  PathClassLoader    用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件。
  DexClassLoader    用来加载 dex 文件,可以从存储空间加载 dex 文件。
  我们在插件化中一般使用的是   DexClassLoader   。
  4.3 双亲委派机制
  每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。下面是 ClassLoader 的   loadClass    方法的具体实现。protected Class<?> loadClass(String name, boolean resolve)     throws ClassNotFoundException {         // First, check if the class has already been loaded         Class<?> c = findLoadedClass(name);         if (c == null) {             try {                 if (parent != null) {                     // 先从父类加载器中进行加载                     c = parent.loadClass(name, false);                 } else {                     c = findBootstrapClassOrNull(name);                 }             } catch (ClassNotFoundException e) {                 // ClassNotFoundException thrown if class not found                 // from the non-null parent class loader             }              if (c == null) {                 // 没有找到,再自己加载                 c = findClass(name);             }         }         return c; }
  4.4 如何加载插件中的类
  要加载插件中的类,我们首先要创建一个   DexClassLoader   ,先看下DexClassLoader    的构造函数需要哪些参数。public class DexClassLoader extends BaseDexClassLoader {     public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {         // ...     } }
  构造函数需要四个参数:
  dexPath  是需要加载的   dex / apk / jar    文件路径。
  optimizedDirectory  是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置。
  librarySearchPath  是 native 依赖的位置。
  parent  就是父类加载器,默认会先从 parent 加载对应的类。
  创建出   DexClassLaoder    实例以后,只要调用其 loadClass(className)   方法就可以加载插件中的类了。具体的实现在下面:// 从 assets 中拿出插件 apk 放到内部存储空间 private fun extractPlugin() {     var inputStream = assets.open("plugin.apk")     File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes()) }  private fun init() {     extractPlugin()     pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath     nativeLibDir = File(filesDir, "pluginlib").absolutePath     dexOutPath = File(filesDir, "dexout").absolutePath     // 生成 DexClassLoader 用来加载插件类     pluginClassLoader = DexClassLoader(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: Unable to find explicit activity class {com.zy.commontec/com.zy.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?
  这个 log 在   Instrumentation    的 checkStartActivityResult    方法中可以看到:public class Instrumentation {     public static void checkStartActivityResult(int res, Object intent) {         if (!ActivityManager.isStartResultFatalError(res)) {             return;         }          switch (res) {             case ActivityManager.START_INTENT_NOT_RESOLVED:             case ActivityManager.START_CLASS_NOT_FOUND:                 if (intent instanceof Intent && ((Intent)intent).getComponent() != null)                     throw new ActivityNotFoundException(                             "Unable to find explicit activity class "                             + ((Intent)intent).getComponent().toShortString()                             + "; have you declared this activity in your AndroidManifest.xml?");                 throw new ActivityNotFoundException(                         "No Activity found to handle " + intent);                 ...         }     } }
  Activity 的生命周期无法被调用 其实一个 Activity 主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了 Manifest 注册文件,启动 Activity 被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件 Activity 也就不能正常运行了。
  2、其实上面两个问题,最终都指向同一个难点,那就是插件中的 Activity 的生命周期如何被调用。解决问题之前我们先看一下正常系统是如何启动一个 Activity 的。
  这里对 Activity 的启动流程进行一些简单的介绍,具体的流程代码就不分析了,因为分析的话大概又能写一篇文章了,而且其实关于 Activity 的启动过程也有不少文章有分析了。这里放一张简图说明一下:
  整个调用路径如下:  Activity.startActivity -> Instrumentation.execStartActivity -> Binder -> AMS.startActivity -> ActivityStarter.startActivityMayWait -> startActivityLocked -> startActivityUnChecked -> ActivityStackSupervisor.resumeFocusedStackTopActivityLocked -> ActivityStatk.resumeTopAcitivityUncheckLocked -> resumeTopActivityInnerLocked -> ActivityStackSupervisor.startSpecificActivityLocked -> realStartActivityLocked -> Binder -> ApplictionThread.scheduleLauchActivity -> H -> ActivityThread.scheduleLauchActivity -> handleLaunchActivity -> performLaunchActivity -> Instrumentation.newActivity 创建 Activity -> callActivityOnCreate 一系列生命周期
  其实我们可以把 AMS 理解为一个公司的背后大 Boss,Activity 相当于小职员,没有权限直接和大 Boss 说话,想做什么事情都必须经过秘书向上汇报,然后秘书再把大 Boss AMS 的命令传达下来。而且大 Boss 那里有所有职员的名单,如果想要混入非法职员是不可能的。而我们想让没有在大 Boss 那里注册的编外人员执行任务,只有两种方法,一种是正式职员领取任务,再分发给编外人员,另一种就是欺骗 Boss,让 Boss 以为这个职员是已经注册的。
  对应到实际的解决方法就是:  我们手动去调用插件 Activity 的生命周期。欺骗系统,让系统以为 Activity 是注册在 Manifest 中的。
  说完生命周期的问题,再来看一下资源的问题。
  在 Activity 中,基本上都会展示界面,而展示界面基本上都要用到资源。
  在 Activity 中,有一个   mResources    变量,是 Resources 类型。这个变量可以理解为代表了整个 apk 的资源。
  在宿主中调用的 Activity,  mResources    自然代表了宿主的资源,所以需要我们对插件的资源进行特殊的处理。
  我们先看一下如何生成代表插件资源的 Resources 类。
  首先要生成一个   AssetManager    实例,然后通过其 addAssetPath    方法添加插件的路径,这样 AssetManager    中就包含了插件的资源。然后通过 Resources 构造函数生成插件资源。具体代码如下:private fun handleResources() {     try {         // 首先通过反射生成 AssetManager 实例         pluginAssetManager = AssetManager::class.java.newInstance()         // 然后调用其 addAssetPath 把插件的路径添加进去。         val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)         addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)     } catch (e: Exception) {     }     // 调用 Resources 构造函数生成实例     pluginResources = Resources(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.com/5A59/android-training/tree/master/common-tec/CommonTec/app/src/main/java/com/zy/commontec/activity/reflect  class StubReflectActivity : Activity() {     protected var activityClassLoader: ClassLoader? = null     protected var activityName = ""     private var pluginPath = ""     private var nativeLibDir: String? = null     private var dexOutPath: String? = null      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         nativeLibDir = File(filesDir, "pluginlib").absolutePath         dexOutPath = File(filesDir, "dexout").absolutePath         pluginPath = intent.getStringExtra("pluginPath")         activityName = intent.getStringExtra("activityName")         // 创建插件 ClassLoader         activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)     }      // 以 onCreate 方法为例,其他 onStart 等生命周期方法类似     fun onCreate(savedInstanceState: Bundle?) {         // 获取插件 Activity 的 onCreate 方法并调用         getMethod("onCreate", Bundle::class.java)?.invoke(activity, savedInstanceState)     }      fun getMethod(methodName: String, vararg params: Class<*>): Method? {         return activityClassLoader?.loadClass(activity)?.getMethod(methodName, *params)     } }
  6.2.2 通过接口调用 Activity 生命周期
  具体的实现见  接口调用生命周期 ,下面列出了重点代码。通过接口调用 Activity 生命周期的前提是要定义一个接口   IPluginActivity   。
  https://github.com/5A59/android-training/tree/master/common-tec/CommonTec/app/src/main/java/com/zy/commontec/activity/ainterface  interface IPluginActivity {     fun attach(proxyActivity: Activity)     fun onCreate(savedInstanceState: Bundle?)     fun onStart()     fun onResume()     fun onPause()     fun onStop()     fun onDestroy() }
  然后在插件 Activity 中实现这个接口。  open class BasePluginActivity : Activity(), IPluginActivity {     var proxyActivity: Activity? = null      override fun attach(proxyActivity: Activity) {         this.proxyActivity = proxyActivity     }      override fun onCreate(savedInstanceState: Bundle?) {         if (proxyActivity == null) {             super.onCreate(savedInstanceState)         }     }     // ... }
  在   StubActivity    通过接口调用插件 Activity 生命周期。class StubInterfaceActivity : StubBaseActivity() {     protected var activityClassLoader: ClassLoader? = null     protected var activityName = ""     private var pluginPath = ""      private var activity: IPluginActivity? = null      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         nativeLibDir = File(filesDir, "pluginlib").absolutePath         dexOutPath = File(filesDir, "dexout").absolutePath         pluginPath = intent.getStringExtra("pluginPath")         activityName = intent.getStringExtra("activityName")         // 生成插件 ClassLoader         activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)         // 加载插件 Activity 类并转化成 IPluginActivity 接口         activity = activityClassLoader?.loadClass(activityName)?.newInstance() as IPluginActivity?         activity?.attach(this)         // 通过接口直接调用对应的生命周期方法         activity?.onCreate(savedInstanceState)     } }
  6.2.3 资源处理方式
  由于手动调用生命周期的方式,会重写大量的 Activity 生命周期方法,所以我们只要重写   getResources    方法,返回插件的资源实例就可以了。下面是具体代码。open class StubBaseActivity : Activity() {      protected var activityClassLoader: ClassLoader? = null     protected var activityName = ""     private var pluginPath = ""     private var pluginAssetManager: AssetManager? = null     private var pluginResources: Resources? = null     private var pluginTheme: Resources.Theme? = null     private var nativeLibDir: String? = null     private var dexOutPath: String? = null      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         nativeLibDir = File(filesDir, "pluginlib").absolutePath         dexOutPath = File(filesDir, "dexout").absolutePath         pluginPath = intent.getStringExtra("pluginPath")         activityName = intent.getStringExtra("activityName")         activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)         handleResources()     }      override fun getResources(): Resources? {         // 这里返回插件的资源,这样插件 Activity 中使用的就是插件资源了         return pluginResources ?: super.getResources()     }      override fun getAssets(): AssetManager {         return pluginAssetManager ?: super.getAssets()     }      override fun getClassLoader(): ClassLoader {         return activityClassLoader ?: super.getClassLoader()     }      private fun handleResources() {         try {             // 生成 AssetManager             pluginAssetManager = AssetManager::class.java.newInstance()             // 添加插件 apk 路径             val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)             addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)         } catch (e: Exception) {         }         // 生成插件资源         pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)     } }
  6.3 hook 系统相关实现的方式欺骗系统,让系统调用生命周期6.3.1 hook Instrumentation
  上面讲了如何通过手动调用插件 Activity 的生命周期方法来启动插件 Activity,现在来看一下欺骗系统的方法。
  上面简单介绍了 Activity 的启动流程,我们可以看到,其实 Android 系统的运行是很巧妙的,AMS 是系统服务,应用通过 Binder 和 AMS 进行交互,其实和我们日常开发中客户端和服务端交互有些类似,只不过这里使用了 Binder 做为交互方式,关于 Binder,可以简单看看 这篇文章 。我们暂时只要知道通过 Binder 应用可以和 AMS 进行对话就行。
  https://juejin.cn/post/6844903882133356558
  这种架构的设计方式,也为我们提供了一些机会。理论上来说,我们只要在启动 Activity 的消息到达 AMS 之前把 Activity 的信息就行修改,然后再消息回来以后再把信息恢复,就可以达到欺骗系统的目的了。
  在这个流程里,有很多 hook 点可以进行,而且不同的插件化框架对于 hook 点的选择也不同,这里我们选择 hook Instrumentation 的方式进行介绍(原因是个人感觉这种方式要简单一点)。
  简化以后的流程如下:
  Instrumentation    相当于 Activity 的管理者,Activity 的创建,以及生命周期的调用都是 AMS 通知以后通过 Instrumentation    来调用的。
  我们上面说到,AMS 相当于一个公司的背后大 Boss,而   Instrumentation    相当于秘书,Activity 相当于小职员,没有权限直接和大 Boss 说话,想做什么事情都必须经过秘书向上汇报,然后 Instrumentation    再把大 Boss AMS 的命令传达下来。而且大 Boss 那里有所有职员的名单,如果想要混入非法职员是不可能的。
  不过在整个过程中,由于 java 的语言特性,大 Boss 在和秘书   Instrumentation   对话时,不会管秘书到底是谁,只会确认这个人是不是秘书(是否是Instrumentation    类型)。
  我们加载插件中的 Activity,相当于让一个不在 Boss 名单上的编外职员去申请执行任务。在正常情况下,大 Boss 会检查职员的名单,确认职员的合法性,一定是通过不了的。但是上有政策,下有对策,我们悄悄的替换了秘书,在秘书和 Boss 汇报时,把职员名字改成大 Boss 名单中的职员,在 Boss 安排工作以后,秘书再把名字换回来,让编外职员去执行任务。
  而我们 hook 的方式就是替换调   Instrumentation   ,修改 Activity 类名,达到隐瞒 AMS 的效果。
  hook 方式原理图
  接下来看看具体的代码实现。具体的实现见  hook 实现插件化 ,下面主要讲解重点代码。
  替换   Instrumentation    之前,首先我们要实现一个我们自己的Instrumentation   ,具体实现如下:
  https://github.com/5A59/android-training/tree/master/common-tec/CommonTec/app/src/main/java/com/zy/commontec/activity/hook  class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) :     Instrumentation() {     private val KEY_COMPONENT = "commontec_component"      companion object {         fun inject(activity: Activity, pluginContext: PluginContext) {             // hook 系统,替换 Instrumentation 为我们自己的 AppInstrumentation,Reflect 是从 VirtualApp 里拷贝的反射工具类,使用很流畅~             var reflect = Reflect.on(activity)             var activityThread = reflect.get("mMainThread")             var base = Reflect.on(activityThread).get("mInstrumentation")             var appInstrumentation = AppInstrumentation(activity, base, pluginContext)             Reflect.on(activityThread).set("mInstrumentation", appInstrumentation)             Reflect.on(activity).set("mInstrumentation", appInstrumentation)         }     }      override fun newActivity(cl: ClassLoader, className: String, intent: Intent): Activity? {         // 创建 Activity 的时候会调用这个方法,在这里需要返回插件 Activity 的实例         val componentName = intent.getParcelableExtra(KEY_COMPONENT)         var clazz = pluginContext.classLoader.loadClass(componentName.className)         intent.component = componentName         return clazz.newInstance() as Activity?     }      private fun injectIntent(intent: Intent?) {         var component: ComponentName? = null         var oldComponent = intent?.component         if (component == null || component.packageName == realContext.packageName) {             // 替换 intent 中的类名为占位 Activity 的类名,这样系统在 Manifest 中查找的时候就可以找到 Activity             component = ComponentName("com.zy.commontec", "com.zy.commontec.activity.hook.HookStubActivity")             intent?.component = component             intent?.putExtra(KEY_COMPONENT, oldComponent)         }     }      fun execStartActivity(         who: Context,         contextThread: IBinder,         token: IBinder,         target: Activity,         intent: Intent,         requestCode: Int     ): Instrumentation.ActivityResult? {         // 启动 activity 的时候会调用这个方法,在这个方法里替换 Intent 中的 ClassName 为已经注册的宿主 Activity         injectIntent(intent)         return Reflect.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.2 hook 系统的资源处理方式
  因为我们 hook 了   Instrumentation    的实现,还是把 Activity 生命周期的调用交给了系统,所以我们的资源处理方式和手动调用生命周期不太一样,这里我们生成 Resources 以后,直接反射替换掉 Activity 中的 mResource    变量即可。下面是具体代码。class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) : Instrumentation() {     private fun injectActivity(activity: Activity?) {         val intent = activity?.intent         val base = activity?.baseContext         try {             // 反射替换 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)             // for native activity             val componentName = intent!!.getParcelableExtra(KEY_COMPONENT)             val wrapperIntent = Intent(intent)             wrapperIntent.setClassName(componentName.packageName, componentName.className)             activity.intent = wrapperIntent         } catch (e: Exception) {         }     }      override fun callActivityOnCreate(activity: Activity?, icicle: Bundle?) {         // 在这里进行资源的替换         injectActivity(activity)         super.callActivityOnCreate(activity, icicle)     } }  public class PluginContext extends ContextWrapper {     private void generateResources() {         try {             // 反射生成 AssetManager 实例             assetManager = AssetManager.class.newInstance();             // 调用 addAssetPath 添加插件路径             Method method = assetManager.getClass().getMethod("addAssetPath", String.class);             method.invoke(assetManager, pluginPath);             // 生成 Resources 实例             resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());         } catch (Exception e) {             e.printStackTrace();         }     } }
  讲完上面两种方法,我们这里对比一下这两种方法的优缺点:
  七、Service 的插件化实现
  Service 比起 Activity 要简单不少,Service 没有太复杂的生命周期需要处理,类似的   onCreate    或者 onStartCommand    可以直接通过代理分发。可以直接在宿主 app 里添加一个占位 Service,然后在对应的生命周期里调用插件 Service 的生命周期方法即可。class StubService : Service() {     var serviceName: String? = null     var pluginService: Service? = null      companion object {         var pluginClassLoader: ClassLoader? = null         fun startService(context: Context, classLoader: ClassLoader, serviceName: String) {             pluginClassLoader = classLoader             val intent = Intent(context, StubService::class.java)             intent.putExtra("serviceName", serviceName)             context.startService(intent)         }     }      override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {         val res = super.onStartCommand(intent, flags, startId)         serviceName = intent?.getStringExtra("serviceName")         pluginService = pluginClassLoader?.loadClass(serviceName)?.newInstance() as Service         pluginService?.onCreate()         return pluginService?.onStartCommand(intent, flags, startId) ?: res     }      override fun onDestroy() {         super.onDestroy()         pluginService?.onDestroy()     }      override fun onBind(intent: Intent?): IBinder? {         return null     } }八、BroadcastReceiver 的插件化实现
  动态广播的处理也比较简单,也没有复杂的生命周期,也不需要在 Manifest 中进行注册,使用的时候直接注册即可。所以只要通过 ClassLoader 加载插件 apk 中的广播类然后直接注册就好。class BroadcastUtils {     companion object {         private val broadcastMap = HashMap()          fun registerBroadcastReceiver(context: Context, classLoader: ClassLoader, action: String, broadcastName: String) {             val receiver = classLoader.loadClass(broadcastName).newInstance() as BroadcastReceiver             val intentFilter = IntentFilter(action)             context.registerReceiver(receiver, intentFilter)             broadcastMap[action] = receiver         }          fun unregisterBroadcastReceiver(context: Context, action: String) {             val receiver = broadcastMap.remove(action)             context.unregisterReceiver(receiver)         }     } }
  静态广播稍微麻烦一点,这里可以解析 Manifest 文件找到其中静态注册的 Broadcast 并进行动态注册,这里就不对 Manifest 进行解析了,知道其原理即可。  九、ContentProvider 的插件化实现
  其实在日常开发中对于插件化中的   ContentProvider    使用还是比较少的,这里只介绍一种比较简单的 ContentProvider    插件化实现方法,就是类似 Service,在宿主 app 中注册占位 ContentProvider   ,然后转发相应的操作到插件ContentProvider    中。代码如下:class StubContentProvider : ContentProvider() {      private var pluginProvider: ContentProvider? = null     private var uriMatcher: UriMatcher? = UriMatcher(UriMatcher.NO_MATCH)      override fun insert(uri: Uri?, values: ContentValues?): Uri? {         loadPluginProvider()         return pluginProvider?.insert(uri, values)     }      override fun query(uri: Uri?, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? {         loadPluginProvider()         if (isPlugin1(uri)) {             return pluginProvider?.query(uri, projection, selection, selectionArgs, sortOrder)         }         return null     }      override fun onCreate(): Boolean {         uriMatcher?.addURI("com.zy.stubprovider", "plugin1", 0)         uriMatcher?.addURI("com.zy.stubprovider", "plugin2", 0)         return true     }      override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array?): Int {         loadPluginProvider()         return pluginProvider?.update(uri, values, selection, selectionArgs) ?: 0     }      override fun delete(uri: Uri?, selection: String?, selectionArgs: Array?): Int {         loadPluginProvider()         return pluginProvider?.delete(uri, selection, selectionArgs) ?: 0     }      override fun getType(uri: Uri?): String {         loadPluginProvider()         return pluginProvider?.getType(uri) ?: ""     }      private fun loadPluginProvider() {         if (pluginProvider == null) {             pluginProvider = PluginUtils.classLoader?.loadClass("com.zy.plugin.PluginContentProvider")?.newInstance() as ContentProvider?         }     }      private fun isPlugin1(uri: Uri?): Boolean {         if (uriMatcher?.match(uri) == 0) {             return true         }         return false     } }
  这里面需要处理的就是,如何转发对应的 Uri 到正确的插件 Provider 中呢,解决方案是在 Uri 中定义不同的插件路径,比如 plugin1 的 Uri 对应就是  content://com.zy.stubprovider/plugin1,plugin2    对应的 uri 就是content://com.zy.stubprovider/plugin2   ,然后在StubContentProvider    中根据对应的 plugin 分发不同的插件 Provider。总结
  本文介绍了插件化的相关实现,主要集中在 Activity 的实现上。重点如下:
  推荐大家在学习插件化的同时,也去学习一些四大组件以及 Binder 的系统实现~  最后
  在这里还分享一份由大佬亲自收录整理的学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料
  这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。
  当然,你也可以拿去查漏补缺,提升自身的竞争力。
  真心希望可以帮助到大家,Android路漫漫,共勉!
  如果你有需要的话,只需私信我【进阶】即可获取

用数字技术服务美好生活来源人民网本来是买空调的,最后还带了床垫枕头回家。在海尔海享睡线下门店里,一名用户说。作为海尔集团向智能睡眠场景踏出的第一步,海享睡为智能床垫推广用户体验交互用户数据反馈提供了更宽捡漏苹果手机,价格低至989元,别错过这三款物以稀为贵,苹果手机一年仅发布一个系列的机型,销量却是国产单一型号的好几倍,这些成绩得益于苹果采用了的ios系统,流畅顺滑的质感,用过的朋友都会被征服,唯一不足的是价格太高,让大多数亿WIFI芯片存在数据窃取和流量操纵风险ALPHVBlackCat今年最复杂的勒索软件安全研究团队发现了一个新的勒索软件ALPHV。研究人员认为,这可能是今年最复杂的勒索软件,它具有高度可定制的功能集,允许对各种企业环境GalaGames和CVentures推出1亿美元元宇宙游戏基金据VentureBeat报道,GalaGames和CVentures推出1亿美元元宇宙游戏基金,以加速元宇宙游戏的发展。上周末在拉斯维加斯举行的Galaverse活动上,GalaG彩色打印机能打印几种颜色呢从色彩原理上来说,彩色打印机,最少需要用3种颜色黄色青色和洋红色。这是3种基本色彩,通常叫做三基色。千万种颜色,就是这三种颜色按照一定比例混合而出。但是,三基色等量混合后的颜色,是华为WATCHD腕部心电血压记录仪体验曝光,可换不同大小气囊IT之家12月16日消息,华为腕部心电血压记录仪HUAWEIWATCHD将于12月23日正式发布,已通过药监局二类医疗器械注册。今日,数码博主看山的叔叔曝光了这款记录仪的产品配件和怼天怼地怼空气,打工还债没脾气!罗永浩重返科技圈你怎么看?怼天怼地怼空气,打工还债没脾气,这大概是对罗永浩的最好的总结了。初中毕业到企业大佬,负债累累又东山再起。他到底经历了什么?而如今他又宣布重返科技行业。不得不说,老罗还是令人钦佩的。10分钟送达日常所需暗店点亮西班牙电商新业态据西班牙阿贝赛报网站报道,新的骑手团队已经出现在城市中。这些年轻的送货员骑着摩托车或电动车,目标是在不到10分钟的时间内完成超快速送货任务。而且,与UberEats等其他送货上门应段永平再下一城,极兔速递68亿接手百世快递国内业务已完成交割百世快递今日发布公告称,其国内快递业务已交割给极兔速递。双方在今年10月29日签订了最终协议,金额约为68亿元人民币。百世快递成立于2003年,创始人是谷歌中国(GoogleChi35岁被裁高龄员工劝退,互联网公司的寒冬来了?作者柳展雄01hr这次爱奇艺大裁员,太猛了,优化比例在3040,涉及几乎所有业务线,目前仅有VR业务未有优化计划。今年稍早些,字节跳动也在砍部门,砍掉教育游戏本地生活业务,快手12发现中国成功秘诀?英媒称西方无法阻止中国崛起,美国衰落是必然自前任美国总统特朗普上台后,打压中国成为美国政府工作的重中之重。为此,特朗普对中国发起贸易战,打压华为中兴等中国公司。拜登上台后,延续了前任政府的部分政策,在继续推进印太战略拉拢盟
即将赶上苹果和三星!到底是哪家国产手机品牌这么优秀?小米集团发布2021年第一季度财报。今年第一季度,小米集团总营收净利润分别达769亿元61亿元,同比分别增长54。7163。8。61亿还是小米的单季利润历史新高。即使我们拿出202酷派COOL20新机发布!起售仅699元!硬核性能碾压同价位所有友商当代奋斗者喜欢什么样的手机?这可能是当下任何一个手机品牌都在使劲琢磨的问题。为了让科技赋能,助力奋斗者有更好的生活,酷派最新发布的酷派COOL20可谓是诚意满满,各种配置在这个69一款千元机可以有多猛?X轴线性马达和NFC居然一个都没落下红米手机可以称得上是同等价位手机里面的守门员,牢牢地把配置的底线给焊死了。而从某些角度上来说红米也不断促进着友商的进步。就比如当时。红米在充电功率上的宣传就使得10瓦的充电功率变成天玑110067W快充!想不到现在的千元机都这么拼了?作为一个米粉。看到RedmiNote10发布完毕,只想说一句不管在难,Redmi的性价始终一如往初。在RedmiNote10没发布之前,不少网友和数码博主都在猜测,现在芯片这么难,这款头部追踪科技的跨界耳机,也许是2019年难以超越的存在了HyperXCloudOrbitS夜鹰加强版游戏耳机首次亮相是在今年5月份的台北电脑展上,如果要介绍CloudOrbitS夜鹰S的话,有必要先介绍一下Audeze和WavesNx头再也不是键帽比键盘贵系列了,治愈萌软的ZOMO新猫爪猫爪虽然软萌治愈,但是单颗接近200元的售价,不是真爱粉还真舍不得掏荷包。桌面撸猫成本太高怎么办?不知道是不是ZOMO听到了玩家的心声,前一阵推出了ABS材质猫爪,将底座由铝合金材不止双主摄,还附赠显微镜?FindX3系列上手就能拍大片如今人们都喜欢用拍照的方式将生活中那些极具魅力的场景和画面保留下来,自然风光风俗民情甚至是孩子不经意间的一个笑脸都是大家眼中的好素材。那么选择一个会拍照的手机就显得格外重要,最近很妥妥的黑科技!OPPO造车专利曝光,将成为汽车市场新势力?科技企业的尽头是造车?这句话确实有道理,因为最近这几年,很多大型的科技企业都在宣布造车,就连OPPO也不例外。今年4月份有媒体报道OPPO集团正在筹备造车事项,推动者为创始人陈明永FindX3系列直出样张分享细节表现完整,色彩还原真实如今,手机的拍照功能似乎成了兵家必争之地,不少手机厂商都针对自家产品的相机部分进行了一系列的升级和优化。OPPO长时间致力于影像功能,这次在OPPOFindX3系列身上同样下足了功向光污染低头,升级体验乔思伯CR1000GT风冷散热和机箱好朋友突然把主机抱我这来,说电脑最近多次无故死机。经过多方会诊,最后判断是散热问题所导致。她的主机是最基础款机箱,CPU散热也是盒装自带,加上近2年没有清理积灰。其实这种情况一般只效果不好难量产?OPPO最新屏下摄像头技术曝光,解决屏下大难题现在手机早已进入全面屏时代,不过用户仍旧不满足,因为受到前置摄像头的限制,绝大多数全面屏手机都使用开孔屏的模式。而所正的全面屏正面应该是无任何开孔,于是各大厂商正努力攻克屏下摄像头