- 移动开发
- 原生移动
- 插件系统
【免费下载链接】atlas
A powerful Android Dynamic Component Framework.
本文基于开源仓库 atlas 源码体系,围绕动态组件框架中"bundle 如何被加载并运行"这一核心链路展开。文章以 MainActivity 点击跳转到 secondbundle 的真实场景为线索,完整剖析 bundle 加载的**触发时机(步骤 1-7)、加载过程(步骤 8-15)、代码初始化(步骤 16-19)**三个阶段,并结合 atlas-core 与 atlas-demo 中的源码实现,讲清主线程与 HandlerThread 的协作方式、BundleClassLoader 的创建、资源的注入以及 bundle Application 的启动原理。读完本文,你将掌握 Atlas 容器"按需异步加载 bundle"的完整调用链,能够据此定位和分析自己项目中的 bundle 加载问题。
概述:把加载过程拆成三部分
Atlas 中 bundle 的加载并非一次性完成,而是贯穿"触发 → 安装解析 → 业务初始化"三个环节。整个过程可用两张时序图直观描述:
- 主流程时序图:bundle_load_img.svg
- 第二部分(加载过程)时序图:bundle_load_part_2_img.svg
阅读时序图时的关键:注意图中的箭头。
- 实线箭头表示该步骤运行在主线程(UI 线程);
- 虚线箭头表示该步骤运行在HandlerThread中。
将整个流程分为三大部分:
| 部分 | 说明 | 对应步骤 |
|---|---|---|
| 第一部分 | 加载时机的触发逻辑 | 1-7 |
| 第二部分 | bundle 加载过程 | 8-15 |
| 第三部分 | bundle 代码初始化 | 16-19 |
其中,第二部分整体运行在 HandlerThread 中,第三部分整体运行在 UI 线程中——这种"后台解析安装、前台回调启动"的设计,正是 Atlas 保证 bundle 首次加载不卡顿 UI 的核心手段。
第一部分 触发时机:从一次点击到安装任务入队
入口:MainActivity 中的 switchToActivity
以一个真实场景作为起点:用户在 MainActivity 中点击导航栏,希望跳转到 secondbundle 内的SecondBundleActivity。
case R.id.navigation_dashboard: switchToActivity("second","com.taobao.secondbundle.SecondBundleActivity")switchToActivity辗转调用后,会执行到第 2 步execStartChildActivityInternal方法。
execStartChildActivityInternal:反查 bundle 名并判断是否已加载
execStartChildActivityInternal是 ActivityGroup 兼容层(ActivityGroupDelegate.java)提供的入口方法,其核心逻辑如下:
public void execStartChildActivityInternal(ViewGroup container,String key, Intent intent){ String bundleName = AtlasBundleInfoManager.instance().getBundleForComponet(componentName); if(!TextUtils.isEmpty(bundleName)){ BundleImpl impl = (BundleImpl) Atlas.getInstance().getBundle(bundleName); if(impl!=null&&impl.checkValidate()) { // bundle 已加载且校验通过,直接启动 } else { // bundle 未加载,走异步加载 asyncStartActivity(container,key,bundleName,intent); } } }这段代码做了两件事:
- 根据 componentName 反查 bundle 名称。componentName 即
com.taobao.secondbundle.SecondBundleActivity。在 Atlas 之启动过程(二) 中我们了解到,AtlasBundleInfoManager中存储了几乎所有 bundle 的信息,因此这里返回的 bundleName 就是com.taobao.secondbundle。其底层实现位于 AtlasBundleInfoManager.java:遍历BundleListing中每个 bundle 的activities、services、receivers、contentProviders四类组件表,命中即返回对应的pkgName。 - 根据 bundleName 查询已加载的 BundleImpl 结构体。由于 secondbundle 此前并未加载过,
Atlas.getInstance().getBundle(bundleName)返回的impl为 null 或未通过checkValidate()校验,于是执行第 3 步asyncStartActivity方法。
在仓库的完整实现中,asyncStartActivity还会弹出一个"bundle 处理中"的等待对话框(RuntimeVariables.alertDialogUntilBundleProcessed),并构造 success/failed 两个CancelableTask,保证 bundle 加载完成后能安全回调到performLaunchChildActivity真正启动目标 Activity。
checkBundleStateAsync:异步触发安装链路
asyncStartActivity最终调用了BundleUtil的方法,直接看第 4 步checkBundleStateAsync:
public static boolean checkBundleArrayStateAsync(final String[] bundlesName, final Runnable bundleActivated, final Runnable bundleDisabled){ BundleInstaller installer = BundleInstallerFetcher.obtainInstaller(); installer.installTransitivelyAsync(bundlesName, new BundleInstaller.InstallListener() { @Override public void onFinished() { boolean success = true; BundleImpl tmp; for(String bundleName : bundlesName){ if((tmp=((BundleImpl) Atlas.getInstance().getBundle(bundleName)))==null || !tmp.checkValidate()){ success = false; }else{ tmp.startBundle(); } } } }); return true; }在这一步中,开始异步加载 bundle;加载成功后,会在主线程中回调onFinished(后面会提及),并调用BundleImpl对象的startBundle方法,开启第三部分的初始化过程。
值得注意的是,installTransitivelyAsync中的 "Transitively" 意味着该安装是带依赖传递的:一个 bundle 所依赖的其他 bundle 也会被一并检查与安装,这与第二部分resolveBundle中解析dependencies的逻辑前后呼应。
deliveryTask:把安装任务投递到 HandlerThread
第 5、6 步主要是各种逻辑判断(bundle 是否已在安装中、是否重复请求等),之后辗转调用到第 7 步deliveryTask:
private void deliveryTask(boolean sync){ Runnable installTask = new Runnable() { @Override public void run() { synchronized (BundleInstaller.this) { try{ call(); } catch (Throwable e) { e.printStackTrace(); } finally { if (mListener != null) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { mListener.onFinished(); } }); } } } } }; sBundleHandler.post(installTask); }函数首先创建了一个异步任务installTask,之后将任务提交给sBundleHandler。而sBundleHandler实际上关联的是一个HandlerThread,所以installTask运行在单独的线程中。
在installTask中做了两件事:
- 调用了
call函数(真正的安装逻辑,见第二部分); - 向UI 线程提交一个任务,用于回调
onFinished——这保证了后续的 bundle 启动逻辑一定发生在主线程,符合 Android 组件对主线程的要求。
至此,第一部分(触发时机)分析完毕,进入第二部分加载过程。
第二部分 加载过程:HandlerThread 中的安装与解析
需要注意,第二部分整体是运行在 HandlerThread 中的,因此所有涉及文件 IO、dexOpt 的耗时操作都不会阻塞 UI。
call:磁盘空间与内置 bundle 的双重校验
call(){ //... if (FileUtils.getUsableSpace(Environment.getDataDirectory()) >= 5) { //has enough space if(AtlasBundleInfoManager.instance().isInternalBundle(bundleName)) { bundle = installBundleFromApk(bundleName); if (bundle != null) { ((BundleImpl) bundle).optDexFile(); } } } else { throw new LowDiskException("no enough space"); } //... }这里有两个判断条件:
- 剩余存储空间满足要求:
FileUtils.getUsableSpace(Environment.getDataDirectory()) >= 5(单位 MB),空间不足时抛出LowDiskException(定义于 atlas-core); - 是内置 bundle:
isInternalBundle的判断实现在 AtlasBundleInfoManager.java,即该 bundle 随宿主 APK 一起打包在storage目录中(而非动态下载的外部 bundle)。
当两个条件都满足时,执行 bundle 的安装和optDexFile操作。installBundleFromApk又调用了installNewBundle方法,直接看第 9 步installNewBundle的实现。
installNewBundle:构造 BundleImpl
static BundleImpl installNewBundle(final String location, final InputStream in) throws BundleException { //... BundleListing.BundleInfo info = AtlasBundleInfoManager.instance().getBundleInfo(location); BundleImpl bundle = new BundleImpl(bundleDir, location, new BundleContext(), null, file, version, true, -1); return bundle; }函数很简单:获取 bundle 的信息(getBundleInfo从BundleListing中按名称取出BundleInfo,见 AtlasBundleInfoManager.java),之后构造一个BundleImpl对象。
有两个参数需要注意:
| 参数 | 说明 |
|---|---|
| bundleDir | /data/data/com.taobao.demo/files/storage/com.taobao.secondbundle |
| in | 指向lib/armeabi/libcom_taobao_secondbundle.so |
其中bundleDir的根目录对应 Framework.java 中的STORAGE_LOCATION = BASEDIR + "/storage/",即宿主应用的files/storage目录,每个 bundle 在该目录下拥有独立的子目录,目录名即 bundle 的包名。
BundleImpl:构造函数的两件套
BundleImpl(final File bundleDir, final String location, final BundleContext context, final InputStream stream, ...) throws BundleException, IOException{ if (stream != null) { this.archive = new BundleArchive(location, bundleDir, stream, version, dexPatchVersion); } if (autoload) { resolveBundle(); Framework.bundles.put(location, this); } }构造函数首先创建了一个BundleArchive对象。BundleArchive持有bundleDir和InputStream的引用,用于后续的 dex 优化(dexOpt)与版本管理——一个 bundle 在 storage 目录中按版本组织,getCurrentRevision().getRevisionDir()指向当前生效的版本目录。
随后,若autoload为 true(内置 bundle 场景即为 true),依次执行两件事:调用resolveBundle(),并将自身注册到Framework.bundles这个全局 map 中(对应 Framework.java 的ConcurrentHashMap<String, Bundle>)。
resolveBundle:创建 BundleClassLoader 并注入资源
private synchronized void resolveBundle() throws BundleException { //... if (this.classloader == null){ // create the bundle classloader List<String> dependencies = AtlasBundleInfoManager.instance().getDependencyForBundle(location); String nativeLibDir = getArchive().getCurrentRevision().getRevisionDir().getAbsolutePath()+"/lib"+":" + RuntimeVariables.androidApplication.getApplicationInfo().nativeLibraryDir+":" + System.getProperty("java.library.path"); if(dependencies!=null) { for (String str : dependencies) { BundleImpl impl = (BundleImpl) Atlas.getInstance().getBundle(str); if (impl != null) { nativeLibDir += ":"; File dependencyLibDir = new File(impl.getArchive().getCurrentRevision().getRevisionDir(), "lib"); nativeLibDir += dependencyLibDir; } } } this.classloader = new BundleClassLoader(this, dependencies, nativeLibDir); } // notify the listeners Framework.notifyBundleListeners(0 /*LOADED*/, this); }resolveBundle为 bundle 创建一个独立的BundleClassLoader,在创建 classloader 的过程中:
- 指定 bundle 自身依赖 so 的路径:
revisionDir/lib+ 系统nativeLibraryDir+java.library.path; - 指定依赖 bundle 所依赖 so 的路径:遍历
getDependencyForBundle返回的依赖列表(实现在 AtlasBundleInfoManager.java,从BundleInfo.getDependency()读取),把每个已加载依赖 bundle 的revisionDir/lib追加到nativeLibDir中。
例如在 demo 中,nativeLibDir的值为:
/data/user/0/com.taobao.demo/files/storage/com.taobao.secondbundle/version.1/lib:/data/app/com.taobao.demo-1/lib/x86:/vendor/lib:/system/lib创建完BundleClassLoader后,最后一行Framework.notifyBundleListeners(0 /*LOADED*/, this)进行状态回调,这里的回调实现类是BundleLifecycleHandler:
public void bundleChanged(final BundleEvent event){ switch (event.getType()) { case 0:/* LOADED */ loaded(event.getBundle()); } }这一步对应 Runtime_principle.md 中定义的 bundle 生命周期:Installed(安装到 storage 目录)→Resolved(classloader 被创建、assetpatch 注入 DelegateResources)→Active(校验通过、dexOpt 完成、资源注入成功)→Started(application 的 onCreate 被调用)。resolveBundle正是生命周期中Resolved阶段的实现。
loaded:把 bundle 资源注入全局 Resources
private void loaded(Bundle bundle) { long time = System.currentTimeMillis(); BundleImpl b = (BundleImpl) bundle; DelegateResources.addBundleResources( b.getArchive().getArchiveFile().getAbsolutePath() ); }loaded的核心动作是把该 bundle 的 APK 文件路径交给DelegateResources.addBundleResources。这正是 Atlas 资源加载机制的关键:宿主LoadedApk中的 Resources 已被替换为 Atlas 内部的DelegateResources(见 Runtime_principle.md 的资源加载机制一节),每个 bundle 安装时,其 assets 路径会被更新到 DelegateResources 的 AssetManager 中,从而让宿主在查找资源时能命中 bundle 内的资源。这就是 bundle 中资源"看起来像宿主资源"的根本原因。
回到第 10 步,BundleImpl构造函数中:
BundleImpl(final File bundleDir, final String location...) { //... resolveBundle(); Framework.bundles.put(location, this); }在resolveBundle函数执行周期内,主要完成了两件事:
- 为 bundle 创建了独立的ClassLoader;
- 将 bundle 中的资源添加到全局 Resources上。
随后第二行Framework.bundles.put(location, this)将 bundle 的信息加入到已加载的列表中——这一步正是第三部分DelegateClassLoader能从已加载列表中反查 bundle 的前提。
完成之后,回到第 8 步,执行optDexFile方法:对 bundle 中的 dex 文件进行 dexOpt(dalvik)/ dex2oat(ART)优化,生成优化后的 odex/oat 文件,为后续类加载做好准备,并完成生命周期中的Active状态校验。
第三部分 初始化过程:UI 线程上的 bundle 启动
整个第三部分运行在 UI 线程中。
bundle 加载完成后,第 7 步的deliveryTask在 UI 线程中回调了完成接口(onFinished),调用startBundle方法,又经过辗转调用,到达BundleLifecycleHandler的started方法中。
started:构造 bundle 的 Application 并执行 onCreate
private void started(Bundle bundle){ BundleImpl b = (BundleImpl) bundle; BundleListing.BundleInfo info = AtlasBundleInfoManager.instance().getBundleInfo(b.getLocation()); //... String appClassName = info.getApplicationName(); Application app = newApplication(appClassName, b.getClassLoader()); app.onCreate(); }第 6 行构造出 bundle 中注册的 application 对象,之后执行 application 的onCreate方法。对于 application 来说,似乎还差一个关键的attachBaseContext入口函数调用,我们接着看构造过程。
newApplication:加载类、实例化并反射 attach
protected static Application newApplication(String applicationClassName, ClassLoader cl) throws ApplicationInitException { final Class<?> applicationClass = cl.loadClass(applicationClassName); Application app = (Application) applicationClass.newInstance(); AtlasHacks.Application_attach.invoke(app, RuntimeVariables.androidApplication); return app; }newApplication做了三步:
- 根据类名
applicationClassName(来自BundleInfo.getApplicationName()),通过 bundle 自己的BundleClassLoader加载对应的 class; - 反射
newInstance()构造出 Application 对象; - 通过
AtlasHacks.Application_attach.invoke(app, RuntimeVariables.androidApplication)反射执行 application 的 attach 方法(Hack 工具层位于 atlas-core,负责系统层面的注入与校验)。
在 Atlas 之启动过程(二) 中解释过:application 的 attach 方法最终会调用到attachBaseContext方法——这正是 bundle Application 获得宿主 Context、完成生命周期初始化的关键一步。这也与宿主自身的启动流程(AtlasBridgeApplication中先 attach 再 onCreate)保持了一致的语义,确保 bundle 内 Application 的attachBaseContext→onCreate顺序与普通 Android Application 完全一致。
DelegateClassLoader:bundle 类的路由查找
最后,看一下DelegateClassLoader加载 bundle 中 class 的过程。Atlas 中存在两种 ClassLoader(详见 Runtime_principle.md 的类加载机制一节):
- DelegateClassLoader:作为类查找的"路由器",本身不真正加载类,启动时被注入
LoadedApk替换原有的 PathClassLoader; - BundleClassLoader:每个 bundle resolve 时分配一个,负责该 bundle 的类加载,查找顺序为 findOwn(自身 dex)→ findDependency(依赖 bundle)→ findPath(主 APK)。
protected Class<?> findClass(String className) throws ClassNotFoundException { Class<?> clazz = loadFromInstalledBundles(className, false); //... return clazz; } static Class<?> loadFromInstalledBundles(String className, boolean safe) throws ClassNotFoundException { String bundleName = AtlasBundleInfoManager.instance().getBundleForComponet(className); BundleImpl bundle = (BundleImpl) Atlas.getInstance().getBundle(bundleName); //... ClassLoader classloader = bundle.getClassLoader(); Class<?> clazz = classloader.loadClass(className); return clazz; }可以看到,DelegateClassLoader.findClass会优先从 bundle 上去找 class。而loadFromInstalledBundles逻辑如下:
- 从
AtlasBundleInfoManager的已加载列表中找到对应 bundle(即第二部分第 10 步Framework.bundles.put时添加的); - 从对应 bundle 上的
BundleClassLoader加载对应的 class。
这一"先查 bundle、再走系统 ClassLoader"的委托顺序,保证了 bundle 内的类(Activity、Application 等)一定由 bundle 自己的 ClassLoader 加载,从而让 bundle 之间、bundle 与宿主之间形成清晰、可隔离、可更新的类边界。
总结:三条主线的闭环
至此,整个 bundle 的加载、初始化过程分析完毕。将三个部分串成一条完整链路:
- 触发(主线程):点击 →
execStartChildActivityInternal反查 bundleName →checkBundleStateAsync提交异步安装 →deliveryTask投递到 HandlerThread; - 加载(HandlerThread):
call校验磁盘空间与内置性 →installNewBundle构造 BundleImpl →resolveBundle创建 BundleClassLoader、注入 nativeLib 路径、触发 LOADED 回调完成资源注入 → 注册进Framework.bundles→optDexFile完成 dex 优化; - 初始化(主线程):
onFinished回调 →startBundle→started用 bundle 自己的 ClassLoader 加载 Application → 反射 attach(触发attachBaseContext)→onCreate;此后宿主通过DelegateClassLoader将 bundle 内类的加载请求路由回各自的 BundleClassLoader。
理解这条链路的关键在于时刻记住两条线程边界:耗时解析与文件操作全部下沉到 HandlerThread,而任何涉及 Android 组件生命周期的操作都回到 UI 线程。这种设计让 bundle 的动态加载对用户无感,也构成了 Atlas 作为 Android 动态组件框架的核心运行机制。
- 移动开发
- 原生移动
- 插件系统
【免费下载链接】atlas
A powerful Android Dynamic Component Framework.
相关推荐
UITableView高级应用:打造IoT-Firstep iOS版设备管理界面终极指南
UITableView高级应用:打造IoT Firstep iOS版设备管理界面终极指南 在物联网 IoT 开发中,设备管理界面是连接用户与智能设备的关键桥梁。
Layui 复选框初始化事件触发机制解析
Layui 复选框初始化事件触发机制解析 在 Layui 框架开发中,表单组件的初始化事件触发是一个常见需求。本文将以复选框组件为例,深入探讨如何在页面加载时自
前端UI组件Autoenv源码解析:从初始化到环境加载的完整流程
Autoenv源码解析:从初始化到环境加载的完整流程 Autoenv是一个强大的目录环境管理工具,它能自动执行特定目录下的环境配置文件。当您 cd 进入包含 .
开发工具CLI工作流自动化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考