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 变体 adb root: ✅ 默认开启 adb root: ⚠️ 需手动开启 adb root: ❌ 关闭 调试符号: 完整保留 调试符号: 部分保留 调试符号: 剥离 编译优化: -O0 编译优化: -O2 编译优化: -O2/-Os 日志: VERBOSE~ERROR 日志: VERBOSE~ERROR 日志: INFO~ERROR 用途: 日常开发调试 用途: 系统测试/性能分析 用途: 正式发布 选择建议 开发用eng → 测试用userdebug → 发布用user

实战建议:如何选择变体

说了这么多,到底怎么选?我给出一个简单粗暴的建议:

  • 写代码阶段:用eng。别跟自己过不去,调试方便最重要
  • 功能验证阶段:用userdebug。性能接近真机,又能抓日志
  • 发布前测试:用user。模拟用户环境,提前暴露问题

一个小技巧:如果你需要在user版本上临时开启调试,可以在编译时设置ENG_BUILD_TARGET=1,但记得这只是临时方案,正式发布一定要去掉。

好了,关于编译变体与调试,就聊到这儿。记住一句话:选对变体,事半功倍;选错变体,debug到崩溃


公众号:蓝海资料掘金营,微信deep3321