1. LiveContainer 与 JIT-Less 模式到底解决什么问题
1.1 从一个常见的装机困境说起
LiveContainer 这个名字,第一次看到的人大多会以为它是某种虚拟机,其实它干的事更朴素:把应用装进一个"壳"里跑,而不是直接装进系统。我在一台没有越狱、系统版本又比较新的设备上折腾调试包的时候,最常撞到的墙就是——包能签名、能安装,但一启动就闪退,日志里反复出现可执行内存申请失败的记录。这类问题的根子往往不在应用本身,而在 iOS 对可执行内存的管控策略上。JIT-Less 模式,就是在这个约束下把应用跑起来的一套配置方案。它的价值在于:让你在拿不到 JIT 权限的环境里,依然能把一份自己构建的应用包跑起来做功能验证、UI 走查、多环境对比测试。适合谁?适合手上只有一台日常主力机、没有额外测试设备,又需要频繁验证自己构建产物的开发者;也适合想搞明白 iOS 运行时到底怎么加载一个 Mach-O 的技术爱好者。不适合谁?不适合期望"装完就万事大吉、完全不用管兼容性"的用户,因为 JIT-Less 天然要牺牲一部分性能,也会让某些强依赖动态代码生成的框架直接罢工。
1.2 JIT、AOT 与解释执行,三条路到底差在哪
要理解 JIT-Less,得先把三条执行路径摆清楚。AOT(Ahead-of-Time)是编译期就把机器码生成好,运行时直接映射进内存执行,iOS 上绝大多数应用走的就是这条路,所以它们对系统来说是"静态代码",加载即执行,不需要额外权限。JIT(Just-in-Time)是运行时按需生成机器码再执行,典型场景是 JS 引擎的热点代码编译、某些脚本语言的加速层、部分模拟器的动态重编译。问题在于,iOS 从设计上就限制了"可写内存同时可执行"这件事(业界常说的 W^X 保护),一个进程想要拿到可执行内存,必须持有系统专门授予的权限声明,而这个声明通常只在带调试授权(get-task-allow)的签名里才会被认可。解释执行则是第三条路:不生成机器码,逐条读取中间表示再分派执行,速度慢但完全不需要可执行内存权限。JIT-Less 模式的本质,就是让原本指望 JIT 的那部分逻辑,退回到 AOT 或解释执行的路径上,代价是性能,收益是"能跑起来"。
我试过用一个类比来解释这件事:AOT 像是提前印好的纸质书,翻开就能读;JIT 像是现场翻译,需要一块可以随时涂改的黑板;而系统的限制就是"这块黑板不许和书本放在同一个房间"。JIT-Less 就是干脆不现场翻译了,改用一本提前写好的双语对照版——慢一点,但房间里不需要黑板。这个类比基本能覆盖大部分人的疑惑,也解释了为什么 JIT-Less 下某些应用会明显变卡:原本靠 JIT 加速的热点循环,现在只能走解释器。
1.3 JIT-Less 的适用边界:什么能跑,什么别硬跑
不是所有应用都适合塞进 JIT-Less 容器里跑,这一点必须提前说清楚,否则你会在一堆闪退里浪费一整晚。判断标准其实很简单,看这个应用在启动和运行过程中,是否主动申请了可执行内存。纯原生 UI 的应用、以静态库为主的工具类应用、不依赖脚本引擎加速的应用,通常在 JIT-Less 下表现良好;而内置脚本引擎且默认开启加速的、带模拟器内核的、依赖动态代码生成做序列化的应用,基本一启动就会撞墙。我总结了一个粗略的三档判断:
- 绿灯:纯 Swift / Objective-C 构建、无脚本引擎、无模拟器内核的应用,JIT-Less 下基本无感。
- 黄灯:内置脚本引擎但支持关闭加速开关的应用,关掉加速后能跑,性能下降明显但不影响功能。
- 红灯:内核级模拟器、依赖运行时代码生成的框架,JIT-Less 下大概率起不来,不建议硬试。
提示:判断一个包属于哪一档,最快的办法不是猜,而是看它链接了哪些动态库。用开发工具链里的
otool -L扫一遍主二进制,出现脚本引擎相关动态库的基本就是黄灯起步。
这里还有个容易被忽略的点:JIT-Less 不仅是"能不能跑"的问题,还涉及"跑起来之后稳不稳"。有些应用启动阶段不申请可执行内存,但运行到某个功能页面才申请,表现就是"前几分钟正常,点进某个模块直接闪退"。所以验证时不能只看启动成功,得把主要功能路径都走一遍。
2. 配置前的环境盘点与材料准备
2.1 设备与系统版本自查清单
动手之前先做一轮盘点,能把后面 80% 的玄学问题挡在门外。我踩过最亏的一次坑,是用一台剩余存储只剩 2GB 出头的设备去导入一个解包后接近 1.8GB 的包,导入过程反复失败,报错信息还特别含糊,最后清出空间才解决——白白耗了四十分钟。所以第一件事是看空间:可用存储建议不低于目标包解压体积的 3 倍,因为导入、解压、临时拷贝、容器内复制这几个阶段会同时占用空间。
第二件事是看系统版本。容器类工具对系统机制依赖很深,系统大版本之间的行为差异会直接体现在启动成功率上。我的习惯是把关键信息记成一张小表,出问题时对照排查,比凭记忆靠谱得多:
| 自查项 | 建议状态 | 不满足时的典型症状 |
|---|---|---|
| 可用存储空间 | ≥ 目标包解压体积的 3 倍 | 导入中断、包不完整、启动白屏 |
| 系统版本 | 与所用容器版本匹配的区间内 | 启动即闪退、签名校验失败 |
| 是否开启开发者模式 | 已开启 | 首次启动被系统拦截,无提示 |
| 设备是否已配对信任 | 已信任 | 安装阶段卡在"正在安装" |
| 后台应用刷新 | 建议关闭,减少内存争抢 | 切后台回来应用被杀 |
第三件事是关掉那些"看起来无关、实际吃内存"的开关。JIT-Less 模式下应用的内存峰值本来就高,因为原本由 JIT 优化掉的开销现在全落在解释执行上,堆内存占用会明显上升。后台再挂几个大应用,系统内存回收一触发,你的目标应用就是第一个被清理的。
2.2 签名方式的取舍逻辑
LiveContainer 的主容器本身也是要装进系统的,所以绕不开签名。这一步的选择直接决定了你的配置能用多久、会不会反复失效。常见有几条路径,各有各的代价:
- 个人免费开发者签名:门槛最低,但有效期通常是 7 天,过期后主容器直接打不开,容器内已导入的应用和数据一般还在,重签主容器即可恢复。
- 付费开发者签名:有效期长得多,适合长期使用,但需要正规的开发者账号,且证书有设备数量限制。
- 持久化签名方案:某些工具利用系统机制让签名长期有效,省心,但可用性高度依赖系统版本,跨版本升级后经常失效。
- 企业级分发签名:不建议个人折腾,稳定性不可控,且存在合规风险。
我的选择逻辑很直白:如果只是短期验证,用免费签名,7 天内把该测的测完;如果要长期挂着一个测试环境,就上付费开发者签名。别为了省事去碰来路不明的证书,这类东西失效是小事,把设备搞出一堆配置残留才麻烦。
注意:无论走哪条签名路径,请只对自己构建、或明确获得授权的应用包做容器化配置。容器工具本身是中性的,用法必须落在合法合规的范围内。
还有一个细节:主容器的签名和容器内应用的签名是两套体系。主容器被系统校验,容器内的应用包则由容器自己做加载与映射。这意味着主容器签名失效时,容器内的应用不会立刻失效,但你打不开容器。理解这一点,排查"到底是哪一层出了问题"时能省很多时间。
2.3 需要提前备好的文件与工具
材料清单不长,但少一样就卡住:
| 材料 | 说明 | 常见坑 |
|---|---|---|
| 主容器安装包 | 与系统版本匹配的版本 | 版本对不上,装了打不开 |
| 待测应用包 | 自己构建的产物,结构完整 | 包被二次压缩,解包后目录层级不对 |
| 签名工具链 | 用于给主容器签名 | 环境缺依赖,签名中途报错 |
| 文件管理工具 | 用于把包塞进设备 | 传输过程被系统截断,文件大小异常 |
| 日志查看工具 | 用于看崩溃记录 | 只看得懂界面报错,拿不到底层原因 |
关于应用包,有个特别容易翻车的点:目录层级。一个标准的应用包,根目录下应该直接是Payload文件夹,Payload里面才是那个以.app结尾的目录。很多人从各种渠道拿到包之后,中间多套了一层文件夹,导入时容器解析不到主二进制,表现就是"导入成功但列表里是空的"或者"点启动毫无反应"。养成导入前先看一眼目录结构的习惯,能省掉大量无效排查。
另外,日志工具的准备别偷懒。JIT-Less 模式下绝大多数失败都以"闪退"的形式呈现,界面层什么都不告诉你。能不能拿到崩溃记录,直接决定了你是"五分钟解决"还是"五小时瞎猜"。
3. 五分钟主流程:从零到第一次成功启动
3.1 主容器安装与首次启动的注意事项
主容器的安装流程和普通应用装到设备上没有本质区别,但有几个动作顺序不能乱。先签名,再安装,最后才打开。我见过有人装完直接点开,结果被系统拦下来,然后再回头补签名,发现装上去的那份是未签名的,得先卸载。顺序错了就是白折腾。
安装完成后第一次打开,通常会有几秒钟的初始化,界面上看起来像卡住了。这时候别急着杀进程——初始化阶段容器在准备自己的沙盒目录结构、建立配置文件、扫描可用空间。杀掉的后果是目录结构建立到一半,后续导入会报各种路径不存在的错误。我的做法是给足 30 秒,如果确实没动静,再去翻崩溃记录。
首次启动还会遇到一个典型场景:系统弹出一个"不受信任的开发者"之类的拦截提示。这不是容器的问题,是签名信任链没建立。去系统的设备管理页面里把对应的描述文件信任一下即可。这一步做完,后续所有操作都不会再被拦。
还有一个环境层面的注意点:开发者模式。较新的系统版本里,如果没有开启这个模式,某些调试相关的机制不会生效,表征就是应用装上去了但一启动就退出,且没有任何有效日志。开启方式在系统的隐私与安全相关设置里,开启后设备会要求重启一次,重启完再继续,别在重启前就急着导入包。
3.2 应用包导入与 bundle 目录结构还原
导入这个动作看起来只是"把文件拷进去",实际上容器在做的事要复杂得多:它需要把.app目录整体搬进自己的沙盒,重建 bundle 结构,修正内部的可执行权限位,然后建立一份映射记录,让启动时能准确找到主二进制、Info.plist、资源目录。
实操中我建议按这个顺序走:
- 用文件管理工具把应用包传到设备的"文件"应用里,放在一个路径简单、没有中文和空格的目录下。
- 在 LiveContainer 里选择导入,定位到那个文件。
- 等待解析完成,确认列表里出现条目,且条目上显示的名称、版本号与包内
Info.plist一致。 - 点进去看一眼详情,确认主二进制被正确识别。
如果第 3 步列表是空的,八成是Payload层级错了;如果列表有但点进去详情是空的,八成是Info.plist读不到或者格式有问题。这两种情况都不用重装容器,把包的结构修好重新导入即可。
细节上,包内可执行文件的权限位这一项经常被忽略。容器在还原 bundle 时会尝试修正,但如果文件在传输过程中权限被抹掉了(比如经过某些压缩格式往返),容器可能修不回来,表现就是"列表里有条目,点启动瞬间退出"。这种时候把包重新打一遍,确保打包工具保留了权限位,问题通常就消失了。
3.3 关掉 JIT 开关的正确姿势
这是整篇内容的核心动作。LiveContainer 这类容器默认会尝试走 JIT 路径,因为它性能更好;但在没有 JIT 权限的环境里,这个尝试必然失败。所以配置的关键是:显式地把 JIT 相关选项关掉,让容器走 AOT / 解释路径。
具体位置在不同版本里略有差异,但逻辑是一致的,通常在应用条目的设置页里,会有一个与 JIT 相关的开关,或者一个"启动模式"的下拉选择。你要做的是:
- 找到该条目对应的配置项。
- 把 JIT 开关置为关闭状态,或者在模式选择里选到不带 JIT 的那一项。
- 保存配置,退出容器再重新进入,让配置生效。
第三步很多人会漏。配置写完直接点启动,容器用的还是内存里的旧配置,表现就是"我明明关了 JIT 怎么还闪退"。退出重进这一步不能省。
如果容器版本提供了更细的选项,我的建议是:一次性把所有依赖可执行内存的开关都关掉,而不是只关主开关。因为某些子功能会独立申请可执行内存,只关主开关的话,启动能过,跑到特定界面还是会崩。全关掉之后再逐项往回开,是定位兼容性问题最快的方法——这个方法我后面在排查章节还会展开。
配置生效后,容器在启动应用时会跳过 JIT 初始化阶段,直接进入 Mach-O 加载。启动时间会稍微长一点,但不会再出现"申请可执行内存失败"这类记录。
3.4 首次启动自检:三个必须验证的点
启动成功不等于配置成功。我给自己定了一套三点的自检流程,建议你也照着走一遍:
第一点,看首屏是否完整渲染。有些应用在 JIT-Less 下能过启动阶段,但渲染层依赖的动态能力不可用,表现是界面出来了但一片空白,或者只有导航栏没有内容。这种情况不能算成功。
第二点,走一遍数据写入路径。随便改一个设置项,退出应用,再重新进入,看设置是否保留。这一项验证的是沙盒路径重定向是否正常。JIT-Less 配置出错时,数据被写到一个临时目录然后被系统回收,是很常见的问题。
第三点,跑一遍最重的功能模块。通常就是那个占内存最多、逻辑最复杂的页面。JIT-Less 下这部分最容易暴露出性能瓶颈或兼容性问题。如果它能正常跑完一次,基本可以认为配置是可用的。
三点全过,五分钟的配置流程算是真正走完了。任何一点没过,都别急着开始用,先按后面的排查章节定位。
4. JIT-Less 模式背后的关键机制
4.1 entitlements、签名与可执行内存的三角关系
想真正搞懂 JIT-Less,得把这三个概念的关系捋清楚。entitlements是随签名一起下发的权限声明,本质是一段键值对,系统在加载可执行文件时会读它,决定给这个进程开放哪些能力。签名则是把这些声明和二进制绑定在一起的完整性校验机制,改一个字节签名就失效。可执行内存是系统提供的一项底层能力,进程通过内存映射接口申请一块可读可写可执行的内存区域。
三者的关系是:进程想申请可执行内存,系统会检查它的 entitlements 里有没有对应声明;有声明还不够,签名必须能证明这份声明是合法下发的。而能够合法下发这类声明的签名类型非常有限,通常只在调试场景下由官方工具链生成。这就解释了为什么"自己签的包"经常拿不到 JIT——签名是合法的,但声明里的那一条不被认可。
JIT-Less 模式绕开的正是这个死循环:既然申请不到可执行内存,那就不申请。容器在加载应用时会拦截相关的内存映射请求,把可执行权限降级为只读或读写,让运行时退回到不需要动态代码生成的路径。这个降级动作是容器做的,不是系统做的,所以你在系统日志里看不到"权限被拒",只能看到应用层抛出的异常。
理解这一点之后,很多现象就说得通了。比如为什么同一个包,用调试签名直接装能跑,塞进容器就崩——不是容器坏了,是签名语境变了。再比如为什么关掉 JIT 后某些应用反而更稳——它们原本就是在没权限的环境里反复重试申请,关掉之后不再重试,路径反而干净。
4.2 页对齐与内存映射:那些报错数字的由来
排查这类问题时,日志里经常出现一串十六进制数字,看着像天书。其实它们大多和页对齐有关。系统做内存映射时,偏移量必须是内存页大小的整数倍,在 64 位 ARM 架构上这个值是16384 字节(16KB)。如果你的映射请求偏移量不是 16KB 的整数倍,系统会直接拒绝,返回一个错误码。
我举个实际算例。假设某个段在文件里的偏移是0x3A000,换算成十进制是 237568 字节。除以 16384,得到 14.5——不是整数。这时候正确做法是向下对齐到最近的整数倍:14 × 16384 = 229376,也就是0x38000。对齐后需要额外读入的头部长度是 237568 − 229376 = 8192 字节,也就是0x2000。容器在做段映射时,通常会自动做这个对齐处理,但如果它拿到的二进制结构本身有问题(比如被打包工具改过、或者文件被截断),对齐计算就会出错,于是你看到的就是"加载失败"。
这个知识点在排查时非常有价值。当你看到报错里带着0x开头的数字,先把它换算一下,看看能不能被 16384 整除。能整除说明问题在别处;不能整除,基本可以锁定是段映射环节出的问题,方向立刻收窄一大半。
提示:不要试图手动改二进制文件来"修正"对齐。段结构和签名是绑定的,改了签名就废,容器会拒绝加载。正确的做法是回到打包环节,用干净的构建产物重新导入。
还有一类数字是内存上限。系统对每个进程有内存占用上限,超过就强制终止(业内叫 jetsam)。这个上限跟设备物理内存有关,量级上:2GB 内存的设备前台大概能用到 650MB 左右,3GB 到 1.3GB 左右,4GB 到 1.8GB 左右,6GB 及以上能到 3GB 上下。JIT-Less 模式下,因为少了 JIT 优化,同一份逻辑的内存占用会明显高于 AOT 模式,所以更容易撞上限。这也是为什么我在准备章节强调要关后台刷新——那点内存省下来,有时候就是启动成功和闪退的分界线。
4.3 沙盒路径重定向与数据落盘
容器跑应用时,应用以为自己还是装在系统标准路径下,实际上所有读写都被重定向到了容器自己的沙盒目录里。这套重定向机制是 LiveContainer 能"多开"和"隔离"的基础,但也是最容易出问题的地方。
重定向大致分三类。第一类是 bundle 路径,应用读自己的Info.plist、资源文件时,路径被拦截并指向容器内的副本。第二类是数据容器路径,应用的文档目录、缓存目录、偏好设置文件,被指向容器内为它单独准备的目录。第三类是临时目录,指向一个容器可控的临时区。
出问题的典型表现是"设置不保存"。你改了个选项,退出重进又变回默认值,说明偏好设置文件的读写被导向了不同的位置——写的时候在 A,读的时候在 B。这种情况多半是重定向规则里某个路径没覆盖到,或者应用使用了相对路径 + 当前工作目录的方式定位文件,而容器没有正确设置工作目录。
另一类表现是"数据在,但读不出来"。文件确实被写进了容器目录,但应用启动后读不到,原因是权限位不对——容器重建目录时给的权限太严,应用自己没有读权限。这种问题在手动往容器目录里塞文件时特别容易发生。
我的一般处理顺序是:先确认应用的读写路径是否都落在容器目录内(用文件管理工具直接看目录里有没有对应文件生成),再确认文件权限是否合理,最后才怀疑重定向规则本身。这个顺序能过滤掉绝大多数误判——很多时候不是规则错了,是文件根本没写进去。
5. 常见问题排查实录
5.1 启动即闪退的六种可能
闪退是最常见的症状,也是最难直接定位的。我把它拆成六种可能,按出现频率排序,逐个排除比盲目试错快得多:
| 可能原因 | 判断依据 | 处理方式 |
|---|---|---|
| JIT 配置未生效 | 崩溃记录里出现可执行内存相关字样 | 关掉全部 JIT 相关开关,退出容器重进 |
| 包结构不完整 | 列表有条目但详情为空 | 检查Payload层级,重新导入 |
| 可执行权限位丢失 | 加载阶段直接失败,无应用层日志 | 重新打包,确保权限位保留 |
| 签名环境不匹配 | 换成调试签名直装正常,容器内崩 | 确认容器版本与签名类型兼容 |
| 内存超限 | 崩溃前有明显卡顿,记录里是内存压力 | 关后台、降负载、减少同时运行条目 |
| 系统版本不兼容 | 同类包换设备正常 | 换匹配版本的容器 |
排查顺序上,我的建议是从配置查起,最后才怀疑包本身。因为配置是最容易改的,包是要重新打包的,改配置的成本远低于重打一个包。而且实测下来,闪退里超过一半是配置问题——尤其是那种"关了一半 JIT 开关"的情况。
还有一种容易误判的情况:首次启动闪退,第二次正常。这种多半是容器在首次启动时做了额外初始化(建立目录、拷贝资源),初始化过程中某个非关键步骤失败导致进程退出,第二次启动时初始化已完成,就正常了。遇到这种,别急着下结论,连启动三次看规律。如果三次都崩,才是真问题。
5.2 白屏、卡启动图与无限加载
白屏和闪退是两回事。闪退是进程没了,白屏是进程活着但界面没内容。这类问题的排查思路完全不同,核心是判断"卡在哪一层"。
卡在启动图,说明应用的主入口已经执行,但界面构建没完成。常见原因是渲染层依赖的某个动态能力在 JIT-Less 下不可用,或者某个初始化任务超时。可以试着把应用的后台初始化逻辑延迟一点——有些容器提供了启动参数配置,加一个短暂延迟能让初始化任务有机会完成。
白屏但有导航栏,说明界面框架起来了,内容层没渲染。这种情况经常和资源加载有关:容器重定向了 bundle 路径,但应用内部用了硬编码的绝对路径去读资源,结果读不到。表现就是框架在、内容空。
无限加载转圈,通常是网络请求或者本地数据库初始化卡住。前者和容器无关,检查网络即可;后者多半是数据目录权限或者路径重定向的问题,参考上一节的排查顺序。
我的经验是:白屏类问题先看容器日志,再看系统日志。容器日志能告诉你应用启动到哪一步,系统日志能告诉你有没有被系统层面拦截。两边对着看,定位速度会快很多。
5.3 数据读写与权限相关的异常
这类问题最隐蔽,因为应用能跑起来,功能看着也正常,就是某些数据莫名其妙丢了或者读不到。典型症状有三个:设置项不持久、文件保存后找不到、相册/文件选择器返回空。
设置项不持久,前面说过,是偏好设置文件读写路径不一致。处理方式是去容器目录里看偏好设置文件到底生成在哪,然后对比应用读取时用的路径。如果两个路径不一致,说明重定向规则需要调整,或者应用用了非标准方式存储。
文件保存后找不到,多半是保存路径落在了容器的临时目录,而临时目录在应用退出后被清理。这种问题在 JIT-Less 下更常见,因为容器为了节省空间,对临时目录的清理策略会更激进。解决办法是把应用的保存路径改到文档目录,或者调整容器的临时目录保留策略。
文件选择器返回空,涉及系统级权限。容器内的应用申请文件访问权限时,权限是挂在容器这个宿主上的,不是挂在应用自己身上。如果容器本身没拿到对应权限,应用里怎么点都是空。这种问题不是配置能解决的,得从容器层面把权限给到位。
6. 实操心得:怎么把 JIT-Less 跑得更稳
6.1 用日志定位问题的最小闭环
我见过太多人在这一步卡住:应用闪退,界面没提示,然后就不知道下一步该干嘛了。其实只要建立一个最小闭环,大部分问题都能自己定位。闭环由三部分组成:崩溃记录的采集、关键日志的过滤、以及一次对照实验。
崩溃记录方面,系统自带的日志入口能看到应用崩溃的概要信息,包括异常类型和出问题的线程。这一步能区分"是进程被系统杀了"还是"是应用自己崩了",方向立刻清晰。
关键日志过滤方面,别一股脑看全量日志,信息量太大反而看不出东西。我的习惯是按关键词过滤:可执行内存相关的、签名相关的、路径相关的,这三类关键词能覆盖八成问题。容器本身一般也会输出启动阶段的日志,重点看它在加载哪个阶段停下。
对照实验是最有价值的一步。同一份包,换一个配置(比如把所有 JIT 开关全关 vs 全开),看崩溃行为是否变化。如果行为变了,说明问题就在这个配置项上;如果完全没变,说明问题在更底层(包结构、签名、系统兼容性)。这个二分法能把排查范围快速砍半,我用它定位过好几次看着毫无头绪的问题。
6.2 降低内存峰值与启动耗时的几个习惯
JIT-Less 模式下性能天生吃亏,但通过一些习惯能把体验拉回可接受的范围。
第一个习惯:一次只跑一个条目。容器支持同时装多个应用,但别同时跑。每多跑一个,就多一份内存占用,撞上系统内存上限的概率直线上升。
第二个习惯:导入后先清一次容器缓存。导入过程会产生大量临时文件,不清掉的话,后续启动时可用空间被挤压,系统会更早触发内存回收。
第三个习惯:把最重的功能放到冷启动后第一时间验证。冷启动时内存最干净,这时候跑重负载模块成功率最高。等到跑了一堆其他功能再回来测,失败了你都分不清是兼容性问题还是内存问题。
第四个习惯:记录每次配置变更。这个听起来很笨,但真的有用。我给自己弄了个简单的记录表,写清楚改了哪个开关、改完什么现象。出现过好几次"改了三个地方好了,但不知道是哪个起的作用",有了记录就能回溯。
最后分享一个判断小技巧:如果一个应用在 JIT-Less 下启动要超过 15 秒还没出首屏,基本可以判定它不适合这个模式,别再花时间优化了。正常应用在 JIT-Less 下的启动延迟,相对 AOT 模式通常是百分之几十的量级,不是数倍。超过这个量级,说明它在启动阶段做了大量依赖动态代码生成的工作,硬跑下去体验也好不了。这种时候换条路更划算——比如改成在真机调试环境下验证,或者把功能拆小、分批验证。