news 2026/9/26 8:20:01

Atlas 框架 bundle 加载过程全解析:从触发时机到代码初始化的三条主线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Atlas 框架 bundle 加载过程全解析:从触发时机到代码初始化的三条主线
  • 移动开发
  • 原生移动
  • 插件系统

【免费下载链接】atlas

A powerful Android Dynamic Component Framework.

项目地址:https://gitcode.com/gh_mirrors/atlas/atlas
点击查看免费下载

本文基于开源仓库 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); } } }

这段代码做了两件事:

  1. 根据 componentName 反查 bundle 名称。componentName 即com.taobao.secondbundle.SecondBundleActivity。在 Atlas 之启动过程(二) 中我们了解到,AtlasBundleInfoManager中存储了几乎所有 bundle 的信息,因此这里返回的 bundleName 就是com.taobao.secondbundle。其底层实现位于 AtlasBundleInfoManager.java:遍历BundleListing中每个 bundle 的activities、services、receivers、contentProviders四类组件表,命中即返回对应的pkgName。
  2. 根据 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"); } //... }

这里有两个判断条件:

  1. 剩余存储空间满足要求:FileUtils.getUsableSpace(Environment.getDataDirectory()) >= 5(单位 MB),空间不足时抛出LowDiskException(定义于 atlas-core);
  2. 是内置 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做了三步:

  1. 根据类名applicationClassName(来自BundleInfo.getApplicationName()),通过 bundle 自己的BundleClassLoader加载对应的 class;
  2. 反射newInstance()构造出 Application 对象;
  3. 通过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逻辑如下:

  1. 从AtlasBundleInfoManager的已加载列表中找到对应 bundle(即第二部分第 10 步Framework.bundles.put时添加的);
  2. 从对应 bundle 上的BundleClassLoader加载对应的 class。

这一"先查 bundle、再走系统 ClassLoader"的委托顺序,保证了 bundle 内的类(Activity、Application 等)一定由 bundle 自己的 ClassLoader 加载,从而让 bundle 之间、bundle 与宿主之间形成清晰、可隔离、可更新的类边界。

总结:三条主线的闭环

至此,整个 bundle 的加载、初始化过程分析完毕。将三个部分串成一条完整链路:

  1. 触发(主线程):点击 →execStartChildActivityInternal反查 bundleName →checkBundleStateAsync提交异步安装 →deliveryTask投递到 HandlerThread;
  2. 加载(HandlerThread):call校验磁盘空间与内置性 →installNewBundle构造 BundleImpl →resolveBundle创建 BundleClassLoader、注入 nativeLib 路径、触发 LOADED 回调完成资源注入 → 注册进Framework.bundles→optDexFile完成 dex 优化;
  3. 初始化(主线程):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.

项目地址:https://gitcode.com/gh_mirrors/atlas/atlas
点击查看免费下载

相关推荐

上一篇:3个技巧搞定电脑风扇噪音:FanControl免费工具让你的电脑安静如初
下一篇:5分钟掌握8球台球辅助工具:提升瞄准精度的终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 8:19:01

Claude Memory Tool API安全与持久化实战指南

1. 这不是“记住上一句”&#xff0c;而是重构Agent的记忆底层逻辑 你有没有试过让Claude帮你写一段Python脚本&#xff0c;改完变量名后让它接着优化逻辑&#xff0c;结果它一脸茫然&#xff1a;“您之前提到的是哪个变量&#xff1f;”——这不是模型“忘了”&#xff0c;是根…

作者头像 李华
网站建设 2026/9/26 8:18:58

Git分支重命名的协作风险与四层映射解析

1. 为什么改分支名不是“重命名”那么简单——一个被低估的协作风险点Git里改分支名&#xff0c;表面看就是一条git branch -m命令的事&#xff0c;但我在带三个跨地域团队做CI/CD流水线优化时&#xff0c;亲眼见过一次分支重命名引发的连锁反应&#xff1a;前端组推送了新功能…

作者头像 李华
网站建设 2026/9/26 8:18:50

AgentScope 2.0 多智能体编排实战:Java 企业级落地与 RAG 服务化

1. 从一次真实的多智能体开发翻车经历说起1.1 我当时面临的问题几个月前&#xff0c;我在做一个内部知识库问答加上工单自动分诊的小系统。最初的想法很简单&#xff1a;一个模型把所有事都干了&#xff0c;先让我提问&#xff0c;再让它从文档里找答案&#xff0c;顺带把工单归…

作者头像 李华
网站建设 2026/9/26 8:17:04

“无法启动”不是终点:PS5模拟器挑战DualSense手柄游戏的全记录

最近我又没忍住&#xff0c;把手头的PS5模拟器翻出来&#xff0c;目标很明确&#xff1a;让《宇宙机器人无线控制器使用指南》跑起来。原因有点好笑——模拟器的官方兼容库页面上&#xff0c;这个游戏那一栏明晃晃写着“无法启动”&#xff0c;四个字像挑衅一样戳在那儿。我偏想…

作者头像 李华
网站建设 2026/9/26 8:16:52

Sunshine+Moonlight自托管游戏串流搭建与调优指南

1. 为什么我要折腾自托管游戏串流 家里有台带独显的台式机&#xff0c;平时下班回来却懒得坐在书桌前&#xff0c;更想窝在沙发上用平板或者电视盒子打游戏。商业串流方案要么限制分辨率&#xff0c;要么对网络环境要求苛刻&#xff0c;延迟忽高忽低&#xff0c;玩动作游戏基本…

作者头像 李华
网站建设 2026/9/26 8:16:10

Claude账号风控升级:从行为建模看AI服务稳定性

1. 这不是“封号预警”&#xff0c;而是账号生命周期管理的信号升级 最近两周&#xff0c;不少长期用Claude的朋友明显感觉到&#xff1a;以前能稳跑三个月的账号&#xff0c;现在可能两周就弹出“验证失败”或“服务暂时不可用”的提示&#xff1b;批量注册的测试账号几乎撑不…

作者头像 李华