14、编译变体与调试:eng、userdebug、user变体区别、编译选项、调试符号、日志输出
说到Android编译,有个概念你绕不开——编译变体(Build Variant)。说白了,就是同一份源码,编译出不同用途的镜像。我刚开始接触AOSP时,觉得这玩意儿不就是几个开关嘛,后来踩了坑才明白,选错变体,调试起来能让你怀疑人生。
三种变体:eng、userdebug、user
AOSP官方定义了三种变体,它们之间的差异主要体现在权限、调试能力、性能优化三个维度。我习惯把它们比作「开发版」「测试版」「正式版」。
| 特性 | eng | userdebug | user |
|---|---|---|---|
| adb root | 默认开启 | 可通过adb root开启 | 关闭 |
| ro.debuggable | 1 | 1 | 0 |
| ro.secure | 0 | 1 | 1 |
| 编译优化级别 | -O0(无优化) | -O2 | -O2 / -Os |
| 调试符号 | 完整保留 | 部分保留 | 剥离 |
| 日志输出 | 所有等级 | 所有等级 | 仅INFO及以上 |
| 典型用途 | 日常开发调试 | 系统测试、性能分析 | 正式发布 |
eng变体:开发者的「游乐场」
eng变体,全称engineering build。它的设计目标就一个——方便开发者。我记得第一次编译eng版本时,直接adb root就能拿到root权限,当时觉得「这也太爽了吧」。
eng变体的特点:
- 关闭安全限制:ro.secure=0,意味着你不需要刷su就能直接操作系统文件
- 无编译优化:-O0级别,代码执行效率低,但调试体验极好
- 完整调试符号:所有.so、.dex都保留符号表,gdb/lldb断点随便打
- 日志全开:从VERBOSE到ERROR,所有logcat输出都能看到
重要提醒:eng变体绝对不能用于量产。我见过有人把eng镜像刷到用户手机上,结果所有app都能拿到root权限,安全漏洞直接裸奔。
userdebug变体:调试与性能的平衡点
userdebug是我个人最常用的变体。它保留了调试能力,但开启了安全保护和编译优化。说白了,就是「带着镣铐跳舞」。
为什么我推荐用userdebug?
- 可以adb root(需要先adb reboot,然后adb root),比eng多一步
- ro.debuggable=1,意味着你可以用run-as命令调试特定app
- 编译优化开到-O2,性能接近正式版
- 日志等级和eng一样,所有log都能看到
我的经验:在项目中期,我通常用userdebug做功能验证。既能抓到完整的日志,又不会因为无优化导致性能问题掩盖真正的bug。
user变体:量产的最后一道关
user变体,就是最终交付给用户的版本。它把所有调试通道都关掉了,安全策略全部拉满。
user变体的「断舍离」:
- adb root?别想了,直接返回error
- ro.debuggable=0,app无法被调试
- 调试符号全部剥离,.so文件体积能小30%左右
- 日志只输出INFO及以上,VERBOSE和DEBUG全部静默
我曾经踩过的坑:有一次在user版本上排查线上crash,发现logcat里啥都没有。后来才意识到,日志等级被限制了。从那以后,我养成了一个习惯——在user版本上保留关键日志的LOG级别为INFO。
编译选项:如何选择变体
在AOSP中,选择变体很简单。你只需要在lunch时指定即可:
# 选择eng变体
lunch aosp_arm64-eng
# 选择userdebug变体
lunch aosp_arm64-userdebug
# 选择user变体
lunch aosp_arm64-user
如果你已经lunch过了,想切换变体,直接重新lunch就行。我个人习惯在开发机上同时维护三个out目录,分别对应三种变体,避免反复编译浪费时间。
调试符号:strip与unstripped
调试符号,说白了就是符号表。它记录了函数名、变量名、行号等信息。有了它,你才能在crash时看到调用栈。
在AOSP中,编译产物分为两种:
- stripped版本:去掉了符号表,体积小,用于刷机
- unstripped版本:保留符号表,体积大,用于调试
你可以在out/target/product/你的设备/symbols/目录下找到unstripped版本。我建议你把这个目录备份一份,线上crash分析时全靠它。
关键命令:分析user版本crash时,用addr2line或llvm-symbolizer,配合unstripped的.so文件,就能还原出调用栈。
日志输出:logcat的变体差异
日志输出是调试的核心手段。三种变体对日志的处理完全不同:
- eng/userdebug:所有LOG等级都输出,包括LOGV、LOGD、LOGI、LOGW、LOGE
- user:只有LOGI、LOGW、LOGE会输出,LOGV和LOGD被编译时直接去掉
为什么会这样?因为Android的日志宏在编译时就会根据变体做条件编译。你看源码:
// 在user变体中,LOGV和LOGD被定义为空操作
#if LOG_NDEBUG
#define LOGV(...) ((void)0)
#define LOGD(...) ((void)0)
#endif
嗯,这里要注意:如果你在user版本上需要调试日志,可以用__android_log_print(ANDROID_LOG_INFO, ...)绕过,但记得上线前删掉。
知识体系总览
下面这张图,帮你把三种变体的核心差异串起来:
实战建议:如何选择变体
说了这么多,到底怎么选?我给出一个简单粗暴的建议:
- 写代码阶段:用eng。别跟自己过不去,调试方便最重要
- 功能验证阶段:用userdebug。性能接近真机,又能抓日志
- 发布前测试:用user。模拟用户环境,提前暴露问题
一个小技巧:如果你需要在user版本上临时开启调试,可以在编译时设置ENG_BUILD_TARGET=1,但记得这只是临时方案,正式发布一定要去掉。
好了,关于编译变体与调试,就聊到这儿。记住一句话:选对变体,事半功倍;选错变体,debug到崩溃。