有些时候我们会遇到 Release 包有 Bug,Debug 包正常,或者是本地构建没问题,上传到 App Store 或者 TestFlight 版本就有 Bug。 能有个方式调试一下就好了。
这个可以有,解决的方式也很简单,我主要的灵感来自 MonkeyDev。如果你用过 MonkeyDev,会感觉使用这个工具逆向非常方便,我们可以直接在 Xcode 上像普通的 App 一样构建调试。
这个思路也可以用在我们调试一些“不能调试”的 ipa。
所以我们先来看一看,MonkeyDev 是如何将已经构建好的 ipa 直接放到 Xcode 运行起来的。
事实上这一过程很简单,基本上我们直接将原本构建的流程改成直接将从 ipa 解压的 app 拷贝过去即可。
为了了解 MonkeyDev 在这里做了什么,我创建了一个新的工程,这个工程只有三个文件 Info.plist
、*.xcodeproj
、copy_target_app.sh
。
这个工程没有需要编译的文件,也没有需要处理的资源。只有一个 Copy Target App
的脚本。
脚本执行关键步骤如下:
PRODUCT_BUNDLE_IDENTIFIER
一致EXPANDED_CODE_SIGN_IDENTITY
给动态库签名对应脚本内容也非常少:
# Xcode 生成 .app 产物的路径
BUILD_APP_PATH="${BUILT_PRODUCTS_DIR}/${TARGET_NAME}.app"
# 待拷贝的 .app 路径
TARGET_APP_PUT_PATH="${SRCROOT}/${TARGET_NAME}/TargetApp"
# 当前工程的 Info.plist 路径
TARGET_INFO_PLIST="${SRCROOT}/${INFOPLIST_FILE}"
# 拷贝 .app 中所有文件
COPY_APP_PATH=$(find "${TARGET_APP_PUT_PATH}" -type d | grep "\.app$" | head -n 1)
cp -rf "${COPY_APP_PATH}/" "${BUILD_APP_PATH}/"
# 移除不考虑支持的 PlugIns 和 Watch
rm -rf "${BUILD_APP_PATH}/PlugIns" "${BUILD_APP_PATH}/Watch" || true
# 工程和产物的 Bundle ID 应当保持一致
cp -rf "${COPY_APP_PATH}/Info.plist" "${TARGET_INFO_PLIST}"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER}" "${TARGET_INFO_PLIST}"
cp -rf "${TARGET_INFO_PLIST}" "${BUILD_APP_PATH}/Info.plist"
# 给所有的动态库签名
for library in "${BUILD_APP_PATH}/Frameworks"/*; do
/usr/bin/codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" "${library}"
done
Xcode 会帮我们完成剩下的事情,比如创建 .app 目录、给 .app 进行签名:
以上的这些工作,足以使用 Xcode 将一个已经构建好的 app 运行到手机中,并使用 lldb 进行调试。
那 ipa/app 从哪里获得?我们自己开发的 app,手里当然有未加密的 ipa(这可以从 .xcarchive 获得),完全不需要砸壳的参与,自然也用不到越狱的设备。
一般传到 App Store 的 app 我们都会使用 CI 进行构建,相关的产物也可以保留下来。只有找到需要调试那次的 ipa,直接使用上述流程处理即可。
这一步的关键是完成符号表恢复和源码路径映射。
为 App Store 构建的 app 没有调试信息,不过既然是我们自己的 app,每次构建应当留下 dSYM
文件供跟踪线上崩溃,这当然也可以用来进行本地调试。
lldb
提供了一个 add-dsym
参数,我们可以在 app 启动时(可以使用 LLDB Init File)添加 dSYM。
这里我随便挑选了一个 Demo app 进行尝试,这个工程来自 Adopting Menus and UIActions in your User Interface (opens new window)。
在 CI 上打包上传后,可以得到 ControlMenus.xcarchive
:
既有 app 又有 dSYM。
将 app 按照上一小节进行构建后,使用 (lldb) add-dsym {dsym-path}
即可添加符号信息。
如下图所示,我们可以看到调用栈和一些符号信息,同时还可以使用符号断点:
符号信息恢复完毕,为了查看源码,我们先来使用 source info
查看看断点所在的源码位置:
(lldb) source info
Lines found in module `ControlMenus
[0x00000001025f7cdc-0x00000001025f7cf4): /Users/yahaha/Downloads/AdoptingMenusAndUIActionsInYourUserInterface/ControlMenus/ViewController.swift:13:15
(Downloads 只是举个例子)这个路径是 CI 上打包的路径,我们可能很难在本地相同路径放一份代码。
不过我们可以使用 lldb 的源码路径映射解决。这个文件对应到我设备上的本地路径为:
/Users/yahaha/Desktop/AdoptingMenusAndUIActionsInYourUserInterface/ControlMenus/ViewController.swift
只是举例说明,CI 环境放到了
Downloads
目录下,本地放在了Desktop
目录下。
此时我们使用一个 lldb settings append target.source-map {old-path} {new path}
命令,即可完成目录映射,具体到这个场景命令如下:
(lldb) settings append target.source-map /Users/yahaha/Downloads /Users/yahaha/Desktop
此时在调试到指令属于某一行代码时,Xcode 将显示具体的代码信息,同时还可以查看到各个变量信息:
此外还可以从上图中看到 source info
中路径从 Downloads
变成了 Desktop
。
以上实践,我在 https://github.com/DianQK/debug-ipa (opens new window) 中提供了所有的内容,这包括一个我生成的 ControlMenus.xcarchive
以及相关源码。
甚至我们可以借助这个工程进行改造,完成一个专属的 App Store 调试套装。
需要额外补充的一点是,线上包一般会有一些反调试的手段,我们可以参考 MonkeyDev 增加动态库的注入,去掉这些反调试,也可以直接使用 MonkeyDev,本文主要是理解实现的过程。