蘑菇街Android组件与插件化

简介: 插件化的基石 -- apk动态加载 随着我街业务的蓬勃发展,产品和运营随时上新功能新活动的需求越来越强烈,经常可以听到“有个功能我想周x上,行不行”。行么?当然是不行啦,上新功能得发新版本啊,到时候费时费力打乱开发节奏不说,覆盖率也是个问题。苏格拉底曾经说过:“现在移动端的主要矛盾是产品日益增长的功能需求与平台落后的发布流程之间的矛盾”。 当然,作为一个靠谱的程序猿,我们就

插件化的基石 -- apk动态加载

随着我街业务的蓬勃发展,产品和运营随时上新功能新活动的需求越来越强烈,经常可以听到“有个功能我想周x上,行不行”。行么?当然是不行啦,上新功能得发新版本啊,到时候费时费力打乱开发节奏不说,覆盖率也是个问题。苏格拉底曾经说过:“现在移动端的主要矛盾是产品日益增长的功能需求与平台落后的发布流程之间的矛盾”。

当然,作为一个靠谱的程序猿,我们就是为了满足产品的需求而存在的(正义脸)。于是在一个阳光明媚的早晨,吃完公司的免费早餐后,我和小强、叶开,决定做一个完善的Android动态加载框架。

Android动态加载技术在蘑菇街的第一次实践,还是在14年的时候,使用的就是之前网上广(tu)为(du)流(si)传(fang)的方式,这种方式有一个重大缺陷,就是插件内部对资源的访问只能通过自己定义的方式,包括对layout文件的inflate等,使用getResouces的方式,分分钟crash给你看,而且内部实现有些复杂,容易出现莫名其妙的ResourcesNotFound错误。在一段时间的使用之后,始终无法大面积推广,原因就是对开发人员来说,写一个“正常”的模块和写一个动态加载模块,写法是不一样的。这件事一直如哏在喉,如果这个框架无法做到对开发业务的同学们透明,那么就很难推广开去。如何做到对业务开发者透明呢,最重要的是对于各类系统api的使用,尤其是Android四大组件的使用和资源访问,都要遵循系统提供的方式。

抛开上面的东西,从头开始讲述一下动态加载的原理: 
Android应用程序的.java文件在编译期会通过javac命令编译成.class文件,最后再把所有的.class文件编译成.dex文件放在.apk包里面。那么动态加载就是在运行时把插件apk直接加载到classloader里面的技术。

看完上面的原理,不知道你有没有什么疑问,反正我是有的。

  1. 如何加载插件里面的.dex文件。
  2. apk里面的资源怎么办。

上面两个问题是动态加载框架最重要的两点,无法动态安装dex或资源文件的动态加载框架都是耍流氓。我们在实现这个框架的时候同样也遇到了这两个问题。

如何动态加载插件代码:

关于代码加载,系统提供了 DexClassLoader 来加载插件代码。开发者可以对每一个插件分配一个 DexClassLoader (这是目前最常见的一种方式),也可以动态得把插件加载到当前运行环境的classloader中。蘑菇街采用的是后者,这种方式可以有效的防止各种莫名其妙的 ClassCastException ,当你在crash后台看到各种 A cast A错误而欲哭无泪的时候,我想你会喜欢上这种方式。 
事情当然不会这么简单,系统提供的DexClassloader对外api中,只有一种方式可以向类加载器指定加载路径。就是在构造函数中传入apk/zip/dex路径。这完全不符合我们“动态”的原则,难道每次加载一个插件,都必须重新实例化一个类加载器出来吗?这个时候我们想到了google提供的multidex插件,这个插件旨在帮助函数超过65536上限的应用在编译期切割class到多个dex文件中。经过观察发现,5.0以下的Android系统,在应用安装的时候只认classes.dex文件,并在安装期对这个dex文件进行opt操作,生成的odex文件放在/data/dalivk-cache里面。那么剩下classes(N).dex怎么办呢,答案就是如果在编译期使用multidex插件的话,开发者还需要让自己的Application继承 MultiDexApplication ,这样说起来,这个 MultiDexApplication 应该就有加载剩下的classes(N).dex的能力了。查看 MultiDexApplication 代码,果然找到了线索:

public class MultiDexApplication extends Application {
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
  }
}
AI 代码解读

可以看到,它在attachBaseContext函数调用了support包中 MultiDex 类的install函数来安装classes(N).dex,于是都是应用层代码,它能动态安装那表示我们也可以。有了以上的分析,剩下要做的就只是去扒一扒install这个函数了。


如何动态加载插件资源:

我们在开发的时候,当有需要用到资源的地方,可以直接调用 Context 的getResources()函数返回 Resources 的来访问打包在apk中的资源文件。在研究如何动态添加资源到系统的 Resources 对象的时候,有必要先了解一下 Resources 本身是如何访问到资源的。

查看系统的 Resources 源码,我们发现这个类主要做了两件事,首当其冲的当然是访问资源,另外一件就是管理资源配置信息。对于资源的动态加载来说,我们关心的是它如何做第一件事的。 实际上, Resources 对资源的访问,全部代理给了另一个重要的对象 AssetManager 。那么问题转化成了, AssetManager 是如何做到对资源的访问的。 Resources 类在它的构造函数里对AssetManager 做了一些重要的初始化:

public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config,
            CompatibilityInfo compatInfo, IBinder token) {
        mAssets = assets;
        mMetrics.setToDefaults();
        if (compatInfo != null) {
            mCompatibilityInfo = compatInfo;
        }
        mToken = new WeakReference<IBinder>(token);
        updateConfiguration(config, metrics);
        assets.ensureStringBlocks();
}
AI 代码解读

其中的重点就是调用了 AssetManager 对象的ensureStringBlocks()函数,这个函数的实现如下:

/*package*/ final void ensureStringBlocks() {
    if (mStringBlocks == null) {
        synchronized (this) {
            if (mStringBlocks == null) {
                makeStringBlocks(sSystem.mStringBlocks);
            }
        }
    }
}
AI 代码解读

函数先判断mStringBlocks变量是否为空,如果不为空的话,表示需要被初始化,于是调用makeStringBlocks函数初始化mStringBlocks:

/*package*/ final void makeStringBlocks(StringBlock[] seed) {
    final int seedNum = (seed != null) ? seed.length : 0;
    final int num = getStringBlockCount();
    mStringBlocks = new StringBlock[num];
    if (localLOGV) Log.v(TAG, "Making string blocks for " + this
            + ": " + num);
    for (int i=0; i<num; i++) {
        if (i < seedNum) {
            mStringBlocks[i] = seed[i];
        } else {
            mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
        }
    }
}
AI 代码解读

这里的mStringBlocks对象是一个StringBlock数组,这个类被标记为@hide,表示应用层根本不需要关心它的存在。那么它是做什么用的呢,它就是 AssetManager 能够访问资源的奥秘所在, AssetManager 所有访问资源的函数,例如getResourceTextArray(),都最终通过StringBlock再代理到native进行访问的。看到这里,依然没有任何看到能够指示为什么开发者可以访问自己应用的资源,那么我们再看得前面一点,看看传入 Resources 的构造函数之前,asset参数是不是被“做过手脚”。函数调用辗转到ResourceManager的getTopLevelResources函数:

public Resources getTopLevelResources(String resDir, String[] splitResDirs,
                String[] overlayDirs, String[] libDirs, int displayId,
                Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
    ...
    AssetManager assets = new AssetManager();
    if (resDir != null) {
        if (assets.addAssetPath(resDir) == 0) {
              return null;
        }
    }
    ...
}
AI 代码解读

函数代码有点多,截取最重要的部分,那就是系统通过调用 AssetManager 的addAssetPath函数,将需要加载的资源路径加了进去。addAssetPath函数返回一个int类型,它指示了每个被添加的资源路径在native层一个数组中的位置,这个数组保存了系统资源路径(framework-res.apk),和应用自己添加的所有的资源路径。再回过头看makeStringBlocks函数,就豁然开朗了:

  1. makeStringBlocks函数的参数也是一个StringBlock数组,它表示系统资源,首先它调用getStringBlockCount函数得到当前应用所有要加载的资源路径数量。
  2. 然后进入循环,如果属于系统资源,就直接用传入参数seed中的对象来赋值。
  3. 如果是应用自己的资源,就实例化一个新的StringBlock对象来赋值。并在StringBlock的构造函数中调用getNativeStringBlock函数来获取一个native层的对象指针,这个指针被java层StringBlock对象用来调用native函数,最终达到访问资源的目的。

有兴趣的同学可以继续深入native层的源码,可以看到不管是addAssetPath函数还是makeStringBlocks函数,使用的都是native层同一个数组,这样,这两个函数就被关联了起来。

到这里,我们已经知道了如何动态添加资源路径的“秘密”。


解决了以上两个问题,一个基本满足要求的动态加载框架就被搭了起来。 
关于如何延迟加载组件的问题,请期待下一期的 那些年蘑菇街Android组件与插件化背后的故事 

ps:查看native层Resources.cpp的代码,我们发现,Android5.0及以上版本是真正的支持动态添加资源路径到系统 Resources 对象, 直接反射调用getAsset.addAssetPath即可。5.0以下版本只是“伪动态”,需要自己重新实例化一个 Resources 对象和 AssetManager对象,添加完所有需要的资源路径后,替换运行环境的 Resources 对象才可以做到“动态”。这个跟5.0以下的Resources.cpp在初始化完成之后,无法动态扩展resTable有关。


原文链接地址http://ju.outofmemory.cn/entry/208685

目录
打赏
0
0
0
0
498
分享
相关文章
安卓应用开发中的自定义View组件
【10月更文挑战第5天】在安卓应用开发中,自定义View组件是提升用户交互体验的利器。本篇将深入探讨如何从零开始创建自定义View,包括设计理念、实现步骤以及性能优化技巧,帮助开发者打造流畅且富有创意的用户界面。
159 0
安卓应用开发中的自定义View组件
【10月更文挑战第5天】自定义View是安卓应用开发的一块基石,它为开发者提供了无限的可能。通过掌握其原理和实现方法,可以创造出既美观又实用的用户界面。本文将引导你了解自定义View的创建过程,包括绘制技巧、事件处理以及性能优化等关键步骤。
安卓应用开发中的自定义View组件实践
【9月更文挑战第10天】在安卓开发领域,自定义View是提升用户体验和实现界面个性化的重要手段。本文将通过一个实际案例,展示如何在安卓项目中创建和使用自定义View组件,包括设计思路、实现步骤以及可能遇到的问题和解决方案。文章不仅提供了代码示例,还深入探讨了自定义View的性能优化技巧,旨在帮助开发者更好地掌握这一技能。
安卓开发中的自定义View组件实践
【8月更文挑战第30天】探索Android世界,自定义View是提升应用界面的关键。本文以简洁的语言带你了解如何创建自定义View,从基础到高级技巧,一步步打造个性化的UI组件。
探索AI在医疗影像分析中的应用探索安卓开发中的自定义View组件
【7月更文挑战第31天】随着人工智能技术的飞速发展,其在医疗健康领域的应用日益广泛。本文将聚焦于AI技术在医疗影像分析中的运用,探讨其如何通过深度学习模型提高诊断的准确性和效率。我们将介绍一些关键的深度学习算法,并通过实际代码示例展示这些算法是如何应用于医学影像的处理和分析中。文章旨在为读者提供对AI在医疗领域应用的深刻理解和实用知识。
63 0
安卓开发_浅谈自定义组件
  在Android中,所有的UI界面都是由View类和ViewGroup类及其子类组合而成。其中,View类是所有UI组件的基类,而ViewGroup类是容纳这些UI组件的容器。 其本身也是View类的子类。
986 0
escrcpy:【技术党必看】Android开发,Escrcpy 让你无线投屏新体验!图形界面掌控 Android,30-120fps 超流畅!🔥
escrcpy 是一款基于 Scrcpy 的开源项目,使用 Electron 构建,提供图形化界面来显示和控制 Android 设备。它支持 USB 和 Wi-Fi 连接,帧率可达 30-120fps,延迟低至 35-70ms,启动迅速且画质清晰。escrcpy 拥有丰富的功能,包括自动化任务、多设备管理、反向网络共享、批量操作等,无需注册账号或广告干扰。适用于游戏直播、办公协作和教育演示等多种场景,是一款轻量级、高性能的 Android 控制工具。

热门文章

最新文章

  • 1
    Android历史版本与APK文件结构
    12
  • 2
    【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
    23
  • 3
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
    15
  • 4
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
    2
  • 5
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    3
  • 6
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    5
  • 7
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    2
  • 8
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件
    6
  • 9
    escrcpy:【技术党必看】Android开发,Escrcpy 让你无线投屏新体验!图形界面掌控 Android,30-120fps 超流畅!🔥
    4
  • 10
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
    1
  • 1
    android FragmentManager 删除所有Fragment 重建
    18
  • 2
    Android实战经验之Kotlin中快速实现MVI架构
    30
  • 3
    即时通讯安全篇(一):正确地理解和使用Android端加密算法
    35
  • 4
    escrcpy:【技术党必看】Android开发,Escrcpy 让你无线投屏新体验!图形界面掌控 Android,30-120fps 超流畅!🔥
    43
  • 5
    【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
    144
  • 6
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件
    46
  • 7
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    58
  • 8
    Android历史版本与APK文件结构
    160
  • 9
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    48
  • 10
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
    41