iOS 崩溃日志在线符号化实践
1. 什么是符號化?
在日常開發(fā)中,應用難免會發(fā)生崩潰。通常,我們直接從用戶導出來的崩潰日志都是未符號化或者部分符號化的,都是一堆十六進制內存地址的集合,可讀性較差。未符號化或者部分符號化的崩潰日志對閃退問題的解決幾乎毫無幫助,如下所示:
Last Exception Backtrace: 0 CoreFoundation 0x1ca4cd27c 0x1ca3b5000 + 1147516 1 libobjc.A.dylib 0x1c96a79f8 0x1c96a2000 + 23032 2 CoreFoundation 0x1ca3ded94 0x1ca3b5000 + 171412 3 TestBacktrace 0x102a47464 0x102a40000 + 29796 4 UIKitCore 0x1f6c86e30 0x1f63d3000 + 9125424只有符號化后的崩潰日志才能顯示各個線程的函數調用,而不僅僅是毫無意義的虛擬內存地址。符號化后的崩潰日志如下所示, 此時,我們就能夠直接從堆棧信息中知道應用 TestBacktrace 發(fā)生崩潰時的函數為 [AppDelegate Application:didFinishLaunchingWithOptions:],崩潰時函數所在文件為 AppDelegate.m,行號為23:
Last Exception Backtrace: 0 CoreFoundation 0x1ca4cd27c __exceptionPreprocess + 228 1 libobjc.A.dylib 0x1c96a79f8 objc_exception_throw + 55 2 CoreFoundation 0x1ca3ded94 -[__NSSingleObjectArrayI objectAtIndex:] + 127 3 TestBacktrace 0x102a47464 -[AppDelegate Application:didFinishLaunchingWithOptions:] + 29796 (AppDelegate.m:23) 4 UIKitCore 0x1f6c86e30 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 4112. 符號化原理
丨2.1 什么是dSYM文件?
iOS 平臺中, dSYM 文件是指具有調試信息的目標文件,文件名通常為:xxx.app.dSYM,其中 xxx 通常表示應用程序的二進制包名,如下圖所示:
通常我們可以在 Xcode 打包出來的文件 xcarchive 里面看到 dSYM 文件以及目錄架構:
dSYM 中存儲著文件名、方法名、行號等信息,是和可執(zhí)行文件的16進制函數地址一一對應的,通過分析崩潰的崩潰文件可以準確知道具體的崩潰信息。DWARF(Debuging With Arbitrary Record Format) 是 ELF 和 Mach-O 等文件格式中用來存儲和處理調試信息的標準格式。DWARF 中的數據是高度壓縮的,可以通過 dwarfdump、otool 等命令提取其中的可讀信息。比如提取關鍵的調試信息 debug_info 、debug_line,可使用命令
dwarfdump --debug-line /Users/xxxx/Desktop/resource/TestBacktrace.app.dSYM > debug_line.txt導出debug_line 的信息到文件 debug_line.txt 中,debug_info 也可以使用類似命令導出。
ELF、Mach-O 分別是 Linux 和 Mac OS 平臺用于存儲二進制文件、可執(zhí)行文件、目標代碼和共享庫的文件名稱。
丨2.2 如何生成dSYM文件
在編譯工程時, Debug 模式會默認選中不生成 dSYM 文件, 該配置可在 Build Setting|Build Option 中更改,Release 模式下 dSYM 是默認生成的。另外,如果開啟了 bitcode 優(yōu)化的話,蘋果會做二次編譯優(yōu)化,所以最終的 dSYM 就需要在 Apple Connect 手動下載了。每次編譯生成的 dSYM 都會有所差別,通常 dSYM 中會有一個唯一標識,稱作 UUID ,用以區(qū)分不同的 dSYM 文件。
丨2.3 如何通過崩潰日志中應用的 UUID 找到匹配的 dSYM ?
還原崩潰堆棧時,需要 dSYM 的 UUID 與崩潰時的應用 UUID 一致。通常,每一個 dSYM 文件都有一個 UUID,和 App 文件中的 UUID 對應,代表著是一個應用。而每一條崩潰信息都會記錄著應用的 UUID,用來和 dSYM 的 UUID 進行校對匹配。
丨2.4 符號化流程
將崩潰日志中的 APP 二進制地址轉化為函數流程如下所示:
獲取到崩潰日志 App 關鍵行信息
從上圖中可以看到 APP 的關建行為是:
3 TestBacktrace 0x102a47464 0x102a40000 + 29796其中 TestBacktrace 為我們的二進制包名名稱,其余行都是系統(tǒng)堆棧。
獲取到偏移量、運行時堆棧地址、運行時APP起始地址
由關鍵行信息獲取到 TestBacktrace 相對于起始地址的偏移量為 29796,運行時堆棧地址為 0x102a47464,運行時APP起始地址為 0x102a40000。
獲取 dSYM 起始地址
dSYM 文件中保存中符號表 TEXT 段的起始地址,起始地址可通過以下命令獲得:
由上圖中可得到 dSYM 中代碼段起始地址為 0x10000000。
計算崩潰地址對應 dSYM 符號表中的地址
因為 iOS 加載 Mach-O 文件時為了安全使用了 ASLR(Address Space Layout Randomization) 機制,導致二進制 Mach-O 文件每次加載到內存的首地址都會不一樣,但是偏移量,加載地址,起始地址的計算規(guī)則是一樣的;從上面我們可以得到 0x102a47464 (運行時地址) = 0x102a40000 (起始地址) + 29796(偏移量)這個公式。因此通過 dSYM 的起始地址和偏移量就可以計算出 0x102a47464 對應在 dSYM 中的地址為 0x100007464 = 0x0000000100000000 + 29296。
獲取到具體的函數/行數/文件
獲取到運行堆棧地址在 dSYM 文件的對應地址 0x100007464 之后,在 dSYM 文件的 debug-info 中就可以查找到包含該地址的 DIE(Debug Information Entry) 單元,Mac OS 下可使用命令
獲取相應信息,如圖所示:
- DW_TAG_Subprogram 表示這個DIE單元表示的是函數方法。
- DW_AT_low_pc 表示這個方法起始地址為 0x1000073b4 。
- DW_AT_high_pc 表示這個方法結束地址為 0x1000074c4 。這就表示崩潰日志中 0x102a47464 轉化后的偏移地址0x100007464 正好位于這
- DW_AT_low_p 和 DW_AT_high_pc 之間。
- DW_AT_name 表示我們的函數名為[AppDelegateApplication:didFinishLaunchingWithOptions:]
- DW_AT_decl_file表示函數所在文件路徑為 AppDelegate.m
- DW_AT_decl_line 表示函數開始行數為 19。
組裝并格式化
最終經過格式優(yōu)化,崩潰日志中 0x102a47464 符號化出來對應的方法為:
3. 本地符號化
丨3.1 符號化方法
Xcode 符號化
將崩潰日志、 dSYM 文件和可執(zhí)行文件放在同一目錄下,然后將 崩潰日志拖拽至 Devicelog中,右鍵 symbolicate Log 或者 Re-symbolicate Log 就能符號化。
使用 symbolicatecrash 命令行符號化
- 定位 symbolicatecrash 腳本
通常 symbolicatecrash 的路徑為 /Applications/Xcode.App/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
- 前置運行命令
運行 symbolicatecrash 前一般需要先運行:
export DEVELOPER_DIR="/Applications/XCode.App/Contents/Developer"- 運行symbolicatecrash命令
首先將崩潰日志、 dSYM 以及 symbolicatecrash 復制出來放到同一個文件夾,然后 cd 到當前文件夾,運行如下命令符號化
./symbolicatecrash TestBacktrace-2021-07-30-135514.ips TestBacktrace.app.dSYM > symbol.log丨3.2 系統(tǒng)日志符號化
值得注意的是,有些時候,崩潰日志里并不會有 App 的調用,而可能全都是系統(tǒng)庫的調用,如下:
Thread 32 Crashed: 0 libobjc.A.dylib 0x19aaf6c10 0x19aad3000 + 146448 1 CFNetwork 0x187545a28 0x18737d000 + 1870376 2 Foundation 0x18808db4c 0x187f6c000 + 1186636 3 Foundation 0x187f8a908 0x187f6c000 + 125192 4 Foundation 0x18808fde8 0x187f6c000 + 1195496 5 Foundation 0x187f8a5c4 0x187f6c000 + 124356 6 Foundation 0x1880907e0 0x187f6c000 + 1198048 7 Foundation 0x1880902ac 0x187f6c000 + 1196716 8 libdispatch.dylib 0x1869863e4 0x186976000 + 66532 9 libdispatch.dylib 0x1869d7298 0x186976000 + 397976 10 libdispatch.dylib 0x18697c028 0x186976000 + 24616 11 libdispatch.dylib 0x18697b828 0x186976000 + 22568 12 libdispatch.dylib 0x186988bb8 0x186976000 + 76728 13 libdispatch.dylib 0x186989378 0x186976000 + 78712 14 libsystem_pthread.dylib 0x1cf2c5580 0x1cf2ba000 + 46464符號化后的日志為:
Thread 32 Crashed: 0 libobjc.A.dylib _objc_release (in libobjc.A.dylib) 16 1 CFNetwork __CFNetworkHTTPConnectionCacheSetLimit (in CFNetwork) 154728 2 Foundation ___NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ (in Foundation) 16 3 Foundation -[NSBlockOperation main] (in Foundation) 100 4 Foundation ___NSOPERATION_IS_INVOKING_MAIN__ (in Foundation) 20 5 Foundation -[NSOperation start] (in Foundation) 784 6 Foundation ___NSOPERATIONQUEUE_IS_STARTING_AN_OPERATION__ (in Foundation) 20 7 Foundation ___NSOQSchedule_f (in Foundation) 180 8 libdispatch.dylib __dispatch_block_async_invoke2 (in libdispatch.dylib) 104 9 libdispatch.dylib __dispatch_client_callout (in libdispatch.dylib) 16 10 libdispatch.dylib __dispatch_continuation_pop$VARIANT$mp (in libdispatch.dylib) 412 11 libdispatch.dylib __dispatch_async_redirect_invoke (in libdispatch.dylib) 784 12 libdispatch.dylib __dispatch_root_queue_drain (in libdispatch.dylib) 376 13 libdispatch.dylib __dispatch_worker_thread2 (in libdispatch.dylib) 120 14 libsystem_pthread.dylib __pthread_wqthread (in libsystem_pthread.dylib) 212可以看出是 CFNetwork 網絡請求時發(fā)生野指針導致的問題,那么我們就可以針對網絡相關的請求做進一步排查。
至此可以得出結論:符號化系統(tǒng)庫是很有必要的,特別是對一些 App 堆棧信息完全沒有的崩潰日志。
如何符號化系統(tǒng)庫符號
符號化自己 App 的方法名,需要編譯生成的 dSYM 文件。而要將系統(tǒng)庫的符號化為完整的方法名,也需要 iOS 各系統(tǒng)庫的符號文件。
- 匹配對應的符號文件版本
用戶的崩潰日志來自各種系統(tǒng)版本,需要對應版本的系統(tǒng)符號文件才能符號化。系統(tǒng)庫符號文件不是通用的,而是對應崩潰所在設備的系統(tǒng)版本和 CPU 型號的。
崩潰日志中有這樣幾個信息:
Code Type: ARM-64 OS Version: iOS 10.2 (14C82) ......... Binary Images: 0x102a40000 - 0x102a6bfff TestBacktrace arm64 <6be881754f573769926b838490e39857> /var/containers/Bundle/Application/B44844E6-AFF4-491E-8168-F4ED93D644C2/TestBacktrace.App/TestBacktraceCode Type 表示此設備的 CPU 架構為 armv7、armv7s、arm64 還是 arm64e。
OS Version 表示此設備的系統(tǒng)版本號,括號中的字符串代表了此系統(tǒng)的 build 號。
Binary Images 中的<9c893b6aa3b13d9596326ef6952e7195> 里面的字符表示對應的系統(tǒng)庫 dyld 的 UUID,只有 build + UUID 匹配的系統(tǒng)庫符號文件才能符號化系統(tǒng)符號。
- 把符號文件放到指定位置
把獲取到的對應版本的符號文件放到 Mac OS 的 ~/Library/Developer/Xcode/iOS DeviceSupport 目錄下,就可以使用Xcode自帶的符號化工具 symbolicatecrash 進行符號化了。這個工具會自動根據崩潰日志中系統(tǒng)庫的 UUID 搜索本機系統(tǒng)庫的符號文件。
丨3.3 獲取系統(tǒng)符號文件的2個方法
從真機上獲取
大部分系統(tǒng)庫符號文件只能從真機上獲取,蘋果也沒有提供直接的下載地址。但是當你用 Xcode 第一次連接某臺設備進行真機調試時,會看到 Xcode 顯示 Processing symbol files ,這時候就是在拷貝真機上的符號文件到 Mac OS 系統(tǒng)的 /Users/xxx/Library/Developer/Xcode/iOS DeviceSupport 目錄下。
目錄下的 14.7.1 (18G82) 這樣的文件夾就是對應的符號文件,通常都有 1-5GB 的大小。
從固件中提取符號文件
從固件 (iPSW) 中可以通過一些方式提取到系統(tǒng)庫符號文件。固件解密分為 下載并提取系統(tǒng)符號 和 系統(tǒng)庫符號 提取兩步。
1. 下載并提取系統(tǒng)符號
- iOS9 以及 iOS9 之前
a. 下載對應版本的 iPSW 固件,直接解壓,解壓后里面有幾個 dmg 格式的鏡像文件,最大的 dmg 文件就是系統(tǒng)鏡像。
b. 從 Firmware_Keys (見文末參考鏈接)找到對應固件的解密 key (頁面上 Root Filesystem 字段的 key )
c. 用 dmg 工具進行解密。cd 到解壓后的 iPSW 文件夾,執(zhí)行 ./dmg extract xxx-xxxx-xxx.dmg dec.dmg -k 。extract 后面跟兩個參數,分別是系統(tǒng)鏡像 dmg 的名字和解密后的文件名,-k 后面填寫第2步獲取到的 key 。如果 key 不對,解密會失敗。解密成功后會生成一個 dec.dmg 文件,雙擊打開即可加載系統(tǒng)鏡像。
- iOS10 以及 iOS10 之后
下載對應版本的 iPSW 固件,直接解壓,解壓后里面有幾個 dmg 格式的鏡像文件,最大的dmg 文件就是系統(tǒng)鏡像。
2. 系統(tǒng)庫符號提取
從 iPhone OS 3.1 開始,所有的系統(tǒng)庫都打包成一個文件:dyld_shared_cache_xxx ,其中 xxx 表示具體的架構,此文件位于:/System/Library/Caches/com.Apple.dyld 目錄。dyld_shared_cache_xxx 文件的解壓可以使用 dyld 中的 dsc_extractor.cpp 代碼,但做一定的改動。
a. 首先在 Apple 開源網站下載源碼dyld庫的源碼,注意,這里需要下載 dyld-7 的源碼。
b. 下載之后,將文件 dsc_extractor.cpp,main 函數前后的代碼改為如下代碼:
#if 1 // test program#include <stdio.h>#include <stddef.h>#include <dlfcn.h> typedef int (*extractor_proc)(const char* shared_cache_file_path,const char* extraction_root_path,void (^progress)(unsigned current,unsigned total)); int main(int argc, const char* argv[]){if ( argc != 4 ) {fprintf(stderr,"usage: dsc_extractor <dsc_extractor.bundle path> <path-to-cache-file> <path-to-device-dir>\n");return 1;}void* handle = dlopen(argv[1],RTLD_LAZY);if ( handle == NULL ) {fprintf(stderr,"dsc_extractor.bundle could not be loaded\n");return 1;}extractor_proc proc = (extractor_proc)dlsym(handle,"dyld_shared_cache_extract_dylibs_progress");if ( proc == NULL ) {fprintf(stderr,"dsc_extractor.bundle did not have dyld_shared_cache_extract_dylibs_progress symbol\n");return 1;}int result = (*proc)(argv[2],argv[3],^(unsigned c, unsigned total) { printf("%d/%d\n", c, total); } );fprintf(stderr, "dyld_shared_cache_extract_dylibs_progress() => %d\n",r esult);return 0; } #endifc. 在終端上 cd 到 dyld 源碼目錄 launch-cache 下,在終端命令行編譯并生成 dsc_extractor 工具。
clang++ -o dsc_extractor dsc_extractor.cpp dsc_iterator.cppd. 從Xcode的包中 /Applications/Xcode.App/Contents/Developer/Platforms/iPhoneOS.platform/usr/lib 中提取出dsc_extractor.bundle 文件。dsc_extractor.bundle 和要提取的 iOS 系統(tǒng)強關聯(lián),比如 iOS14 的系統(tǒng)符號需要導出 Xcode12 里的 dsc_extractor.bundle,而 iOS15 的需要 Xcode 13 Beta 里的。如果不匹配的話,有可能不能提取出系統(tǒng)符號。
e. 調用如下命令提取出系統(tǒng)符號;如下,最終提取的系統(tǒng)庫在目錄 17C81 下,我們解析系統(tǒng)符號需要的文件基本為 dylib 和 framework。
dsc_extractor dsc_extractor.bundle /System/Library/Caches/com.Apple.dyld/dyld_shared_cache_arm64 17C814. 在線符號化
丨4.1 為什么要實現在線符號化
- 打包時候符號文件是由持續(xù)集成打包機產生,本地獲取有成本。
- 方便研發(fā)人員快速符號化崩潰日志。很多時候,崩潰都是在非研發(fā)人員(產品,QA等)使用應用的時候發(fā)生的;同步到研發(fā)人員之后,因為本地環(huán)境的差異,在沒有打包環(huán)境的情況下,研發(fā)人員也需要能迅速符號化崩潰堆棧
- 線上用戶上傳的崩潰日志規(guī)模大。大多數崩潰都是發(fā)版之后用戶使用過程中發(fā)生的,如果大量線上日志未經符號化就同步到研發(fā)人員,就會增加研發(fā)人員的負擔,降低問題解決的效率。
- 用戶系統(tǒng)多,收集難度大。用戶的系統(tǒng)從 iOS9 到 iOS14 都有,千奇百怪,靠研發(fā)人員本地想要解析所有的系統(tǒng)符號純屬臆想。
丨4.2 在線 App /動態(tài)庫符號化
通常情況下,我們只需要符號化極少部分崩潰日志,這種情況下我們在本地就可以符號化了。但當我們的應用上線發(fā)版后,崩潰日志日均收集量級可能超百萬以上,此時就不適合在 Mac OS 上使用腳本/工具符號化了( 在 Mac OS 上使用 symbolicatecrash 命令符號化單個日志時,耗時基本 1 秒以上)。此時,就需要更通用,快速的符號化方式了。
為了能夠在 Linux 服務器上極速符號化 iOS 崩潰日志,我們深入調研了 iOS 本地符號化的原理,在和平臺方多次就技術方案進行了調研磋商之后,最終采取了如下方案:
生成mapping文件
將 dSYM 文件通過腳本提取生成一個 mapping 文件,格式如下:
提取操作會涉及到 DWARF 中 debug_line 段數據的符號化,相關提取算法可以參考 DWARF 官方的資料。debug_line 段包含有詳細的代碼偏移量地址和文件名稱,按照 DWARF 的算法就可以解析出來,然后與 Symbol Table 的函數符號一一匹配,就能生成代碼地址偏移量與函數、文件、行數的映射關系。需要注意的是,蘋果的 Mach-O 現在大部分格式都是使用 DWARF2 和 DWARF4 版本,提取的時候需要重點關注這兩種格式的兼容和算法不同。最終,可以看到 Symbol table 每一行對應一個符號的偏移量??梢园l(fā)現 7464 剛好處于 7454 - 7478 之間,匹配出來的符號剛好是 -[AppDelegate Application:didFinishLaunchingWithOptions:] (in TestBacktrace) (AppDelegate.m:23) ,與 Mac OS 上使用 symbolicatecrash 腳本符號化的結果一致。
根據 mapping 文件符號化
借助于腳本工具提取的符號 mapping 文件,服務端就能夠脫離平臺限制,根據崩潰日志中的 UUID 去匹配映射文件,在 Linux 上極速符號化崩潰日志,提供高效實時的符號化服務。
丨4.3 在線符號化 iOS 系統(tǒng)庫符號
在 Mac OS 平臺上,我們可以直接使用系統(tǒng)庫的符號直接使用腳本去符號化符號,但是一旦要符號化所有用戶上傳的崩潰日志,這一套機制就難免被速度和平臺限制。并且iOS 系統(tǒng)從 2.0 開始,一直到現在 iOS 14 ,發(fā)出的版本幾百個,要手動提取出系統(tǒng)庫符號幾乎是不可能的事情。為了解決這個問題,在借鑒了 dSYM 跨平臺符號化方案之后,我們做了一套系統(tǒng)符號自動化符號化的方案,最終實現了在 Linux 平臺上高效實時的符號化系統(tǒng)堆棧。
定時從 theiphonewiki 網站上導出各個系統(tǒng)以及最新發(fā)布系統(tǒng)的 iPSW 文件下載地址。
解壓 iPSW 并加載系統(tǒng)鏡像 dmg 文件,找到 dyld_shared_cache_xxx 文件。
使用工具 dsc_extractor 將系統(tǒng)庫符號文件導出,導出文件基本為后綴為 dylib 和 framework 的 Mach-O 類型文件。
將所有的 dylib 和 framework 使用工具提取生成如下格式的 mapping 文件。這一步與 dSYM 提取操作會有一定差別,通常來說,系統(tǒng)庫只有符號表段,不需要對 debug_line 段做提取,相對比較簡單。
5. 效果
Thread 0 name: Dispatch queue: com.Apple.main-thread
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x00000001ca06a0dc __pthread_kill + 8
1 libsystem_pthread.dylib 0x00000001ca0e3094 pthread_killVARIANTVARIANTVARIANTmp + 380
2 libsystem_c.dylib 0x00000001c9fc3ea8 abort + 140
3 libc++abi.dylib 0x00000001c9690788 __cxa_bad_cast + 0
``
2. 部分來自研發(fā)和測試的崩潰日志,平臺提供了在線符號化的入口,只需要手動上傳崩潰日志到平臺,立刻就能把符號化后的崩潰日志下載給相應人員。
6. 收益
線上問題定位速度獲得極大提升,從線上發(fā)生新增卡頓/崩潰問題到具體研發(fā)響應時間大大縮減,從發(fā)生崩潰到定位問題,基本都在 10 分鐘以內。
目前,性能平臺日均在線符號化崩潰/卡頓日志超百萬次,廠內接入產品線超 30+,符號化功能做到了上傳即解析,整個過程無需研發(fā)人員干預。真正做到了自動化、在線、實時的符號化崩潰、卡頓日志,并實時根據符號化的問題代碼定位到具體開發(fā)人員,高效的響應并解決線上問題。
參考資料
[1] iOS Crash 分析必備:符號化系統(tǒng)庫方法https://zuikyo.github.io/2016/12/18/iOS%20Crash%E6%97%A5%E5%BF%97%E5%88%86%E6%9E%90%E5%BF%85%E5%A4%87%EF%BC%9A%E7%AC%A6%E5%8F%B7%E5%8C%96%E7%B3%BB%E7%BB%9F%E5%BA%93%E6%96%B9%E6%B3%95/
[2] 聊聊從 iOS 固件提取系統(tǒng)庫符號 http://crash.163.com/#news/!newsId=31
[3] Xcode 中和 symbols 有關的幾個設置 https://www.jianshu.com/p/11710e7ab661
[4] iOS_SDK https://en.wikipedia.org/wiki/IOS_SDK
[5] IOS_version_history https://en.wikipedia.org/wiki/IOS_version_history#iOS_14
[6] dyld 源碼下載地址 https://opensource.apple.com/tarballs/dyld/
[7] The DWARF Debugging Standard http://www.dwarfstd.org/
[8] iOS9 之前的Firmware_Keys https://www.theiphonewiki.com/wiki/Firmware_Keys
[9] dmg 工具下載地址 https://github.com/Zuikyo/iOS-System-Symbols/blob/master/tools/dmg
[10] 系統(tǒng)符號下載地址索引 wiki https://www.theiphonewiki.com/wiki/Firmware
點擊進入了解更多技術資訊~~
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的iOS 崩溃日志在线符号化实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 做百度AI工程师,还要会“相牛”?
- 下一篇: 吊打一切现有开源OCR项目:效果再升7%