iOS 的崩潰捕獲-堆棧符號化-崩潰分析

一、獲取 Crash、dSYM 文件

獲取到的 .ips 改后綴為 .crash 即可

  • 真機(jī) Crash 文件目錄:var/mobile/Library/Logs/CrashReporter

    通過 iTunes 同步后在 macOS 目錄:~/Library/Logs/CrashReporter/MobileDevice/

  • 在 iOS 設(shè)備上直接查看:設(shè)置 -> 隱私 -> 分析 -> 分析數(shù)據(jù)(不同系統(tǒng)版本不一樣)

  • macOS Archives 目錄(.dSYM 和 .app):~/Library/Developer/Xcode/Archives

  • 通過 iTunes Connect :Manage Your Applications -> View Details -> Crash Reports

    需要用戶在設(shè)置->隱私里同意共享診斷數(shù)據(jù)

    使用 bitcode 編譯成中間碼上傳的,本地不會留下 dSYM,而需要從 iTunes Connect 或者 Xcode 下載 dSYM(編譯成機(jī)器碼才能生成)

  • 通過 Xcode:Xcode -> Window -> Devices and Simulators -> 選中設(shè)備 -> View Device Logs

  • 通過 Xcode 直接查看:Xcode -> Window -> Organizer -> Crashes

    上傳到App Store的時(shí)候,同時(shí)上傳dsym文件,那么從Xcode中的 Crash 會自動符號化。

  • 通過 iTools -> 工具箱 -> 崩潰日志 -> 在以下路徑查看

  • # Mac
    ~/Library/Logs/CrashReporter/MobileDevice/<DEVICE_NAME>
    # Windows
    C://Users/<USER_NAME>/AppDataRoamingApple/ComputerLogsCrashReporterMobileDevice/<DEVICE_NAME>/
    
  • 第三方 Crash 統(tǒng)計(jì)庫:KSCrashplcrashReporterCrashKit

  • 第三方 Crash 統(tǒng)計(jì)服務(wù):Crashlytics、Hockeyapp、友盟、Bugly

    注意:

    • 最好只集成一個(gè) Crash 統(tǒng)計(jì)服務(wù),當(dāng)各家的服務(wù)都以保證自己的Crash統(tǒng)計(jì)正確完整為目的時(shí),難免出現(xiàn)時(shí)序手腳,強(qiáng)行覆蓋等等的惡意競爭。
    • 應(yīng)用層參與收集 Crash日志的服務(wù)方越多,越有可能影響iOS系統(tǒng)自帶的 Crash Reporter。

二、Crash 符號化(Symbolicating crash logs)

symbols 和 Symbolicate

symbols 就是函數(shù)名或變量名。符號化的過程就是把 crash log 中的內(nèi)存地址轉(zhuǎn)化為相應(yīng)的函數(shù)調(diào)用關(guān)系。

一般來說,debug 模式構(gòu)建的 app 會把符號表存儲在編譯好的 binary 信息中,而 release 模式構(gòu)建的app會把符號表存儲在 dSYM 文件中以節(jié)省體積。

系統(tǒng)庫符號化文件

每當(dāng) Xcode 連接一臺從未在當(dāng)前電腦調(diào)試過的 iOS 版本的設(shè)備時(shí),都會花一段時(shí)間把手機(jī)的系統(tǒng)庫符號化文件自動導(dǎo)入到 ~/Library/Developer/Xcode/iOS DeviceSupport,這個(gè)過程叫 Processing symbol files。每個(gè)系統(tǒng)版本的 symbols 文件約占 2GB,所以這個(gè)文件夾會占用不少磁盤空間。但是,最好將這些內(nèi)容備份到外置硬盤,需要符號化的時(shí)候再重新拷貝回來,而不是使用清理工具清理掉。因?yàn)椋到y(tǒng)符號化文件的獲取沒有那么容易。

系統(tǒng)庫符號文件不是通用的,而是對應(yīng)crash所在設(shè)備的系統(tǒng)版本和CPU型號的。獲取系統(tǒng)符號化文件的兩大方式就是通過真機(jī),或者通過各版本 Xcode 附帶,蘋果官方?jīng)]有提供任何下載方式。有技術(shù)員總結(jié)了搜集方式,并給出了 github 下載方式,可查看附錄。

通過 Xcode 符號化

需要3個(gè)文件,放在同一目錄下

  • crash報(bào)告(.crash文件)
  • Debug Symbol 符號文件 (.dsym文件)
  • 解壓 ipa 包后的 .app 文件

操作過程:Xcode -> Devices and Simulators -> 選中設(shè)備 -> View Device Logs

然后把 .crash文件 拖到 Device Logs 或者選擇下面的import導(dǎo)入.crash文件。這樣你就可以看到crash的詳細(xì)log了。

通過命令行工具 symbolicatecrash 符號化
  • 將 .app、.dSYM、.crash 文件放到同一個(gè)目錄下。
# 找到 symbolicatecrash 工具并拷貝出來
find /Applications/Xcode.app -name symbolicatecrash -type f
# 會返回幾個(gè)路徑,拷貝其中一個(gè)
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

# 引入環(huán)境變量
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
# 符號解析
./symbolicatecrash appName.crash .dSYM文件路徑 > appName.log
./symbolicatecrash appName.crash appName.app > appName.log
# 將符號化的 crash log 保存在 appName.log 中
./symbolicatecrash appName.crash appName.app > appName.log
通過命令行工具 atos 符號化

有多個(gè) .app、.dSYM、.crash 的時(shí)候很好用。用于符號化單個(gè)地址(可使用腳本批量化)。

每一個(gè)可執(zhí)行程序都有一個(gè)build UUID來唯一標(biāo)識(每次 build 都不同)。Crash日志包含發(fā)生crash的這個(gè)應(yīng)用(app)的 build UUID以及crash發(fā)生的時(shí)候,應(yīng)用加載的所有庫文件的[build UUID]。

# 獲取 crash 文件的 UUID
grep "appName armv" *crash
# 或者
grep --after-context=2 "Binary Images:" *crash

# 獲取 app 的 UUID
xcrun dwarfdump --uuid appName.app/appName
# 獲取 dSYM 的 UUID
xcrun dwarfdump --uuid appName.dSYM

# 對比 app 和 crash 的 UUID 進(jìn)行匹配

# 用 atos 命令來符號化某個(gè)特定模塊加載地址 (3種方式都可以)
# 0x4000 是模塊的加載地址(必須是DWARF文件地址,而不是dSYM地址,dSYM只是一個(gè)bundle)
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l 0x4000 -arch armv7
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -arch armv7
xcrun atos -o appName.app/appName -arch armv7

# 另外,應(yīng)用內(nèi) 獲取 UUID 的方法

#import <mach-o/ldsyms.h>
NSString *executableUUID() {
    const uint8_t *command = (const uint8_t *)(&_mh_execute_header + 1);
    for (uint32_t idx = 0; idx < _mh_execute_header.ncmds; ++idx) {
        if (((const struct load_command *)command)->cmd == LC_UUID) {
            command += sizeof(struct load_command);
            return [NSString stringWithFormat:@"%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
                    command[0], command[1], command[2], command[3],
                    command[4], command[5],
                    command[6], command[7],
                    command[8], command[9],
                    command[10], command[11], command[12], command[13], command[14], command[15]];
        } else {
            command += ((const struct load_command *)command)->cmdsize;
        }
    }
    return nil;
}

# 通過 iTunes Connect 網(wǎng)站來下載 dSYM 的話,對下載下來的每個(gè) dSYM 文件都執(zhí)行一次
xcrun dsymutil -symbol-map ~/Library/Developer/Xcode/Archives/[...]/BCSymbolMaps [UUID].dSYM

示例:

# 有兩行未符號化的 crash log
* 3 appName 0x000f462a 0x4000 + 984618 
* 4 appName **0x00352aee** 0x4000 + 3468014

# 1. 執(zhí)行
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l 0x4000 -arch armv7
# 2. 然后輸入 0x00352aee
# 3. 符號化結(jié)果:
-[UIScrollView(UITouch) touchesEnded:withEvent:] (in appName) (UIScrollView+UITouch.h:26)
注意:
  • 使用 symbolicatecrash,先拷貝出 symbolicatecrash 文件比較方便。
  • 無論是 symbolicatecrash 還是 atos,都只需要.crash.dSYM ,或 .crash.app,就可以符號化了。
  • 系統(tǒng)方法的堆棧符號化需要系統(tǒng)符號化文件,如果本地 macOS 沒有該文件,也沒有該版本 iOS 設(shè)備可拷貝,可通過 Github iOS-System-Symbols(iOS 各版本系統(tǒng)符號庫) 下載,

三、Crash 文件結(jié)構(gòu)

1. Process Information(進(jìn)程信息)
Incident Idnetifier 崩潰報(bào)告的唯一標(biāo)識符,不同的Crash
CrashReporter Key 設(shè)備的 id(不是 uuid)。通常同一個(gè)設(shè)備上同一版本的 app 發(fā)生Crash時(shí),該值都是一樣的。
Hardware Model 設(shè)備類型
Process 進(jìn)程名稱[進(jìn)程 id],進(jìn)程通常是 app 名字
Path 可執(zhí)行程序的位置
Identifier com.companyName.appName
Version app 版本號
Code Type CPU 架構(gòu)
Parent Process 父進(jìn)程,iOS中App通常都是單進(jìn)程的,一般父進(jìn)程都是 launchd
2. Basic Information(基本信息)
Date/Time Crash發(fā)生的時(shí)間,可讀的字符串
OS Version 系統(tǒng)版本(build 號)
Report Version Crash日志的格式,目前基本上都是104,不同的version里面包含的字段可能有不同
3. Exception(異常)
Exception Type 異常類型
Exception Subtype: 異常子類型
Crashed Thread 發(fā)生異常的線程號
Exception Information 額外診斷信息

從macOS Sierra, iOS 10, watchOS 3, 和 tvOS 10開始,額外診斷信息,包括:

  1. 應(yīng)用的具體信息:在進(jìn)程被終止前捕捉到的框架錯(cuò)誤信息

  2. 內(nèi)核信息:關(guān)于代碼簽名問題的細(xì)節(jié)

  3. Dyld (動態(tài)鏈接庫)錯(cuò)誤信息:被動態(tài)鏈接器提交的錯(cuò)誤信息

# 一段因?yàn)檎也坏芥溄訋於鴮?dǎo)致進(jìn)程被終止的Crash Report的摘錄
Dyld Error Message:

Dyld Message: Library not loaded: @rpath/MyCustomFramework.framework/MyCustomFramework

 Referenced from: /private/var/containers/Bundle/Application/CD9DB546-A449-41A4-A08B-87E57EE11354/TheElements.app/TheElements  
 Reason: no suitable image found.
 
 # 一段因?yàn)闆]能快速加載初始view controller而導(dǎo)致進(jìn)程被終止的Crash Report的摘錄
Application Specific Information:
com.example.apple-samplecode.TheElements failed to scene-create after 19.81s (launch took 0.19s of total time limit 20.00s)
Elapsed total CPU time (seconds): 7.690 (user 7.690, system 0.000), 19% CPU
Elapsed application CPU time (seconds): 0.697, 2% CPU
4. Thread Backtrace(線程回溯)

Crash 發(fā)生時(shí)的線程的調(diào)用棧,沒有符號化前是內(nèi)存地址。

5. Thread State(線程狀態(tài))

Crash 發(fā)生時(shí)的寄存器狀態(tài)。在你讀一個(gè) Crash Report 的時(shí)候,了解線程狀態(tài)并非必須,但是如果你想更好地了解crash的細(xì)節(jié),這會起一些幫助,這需要一些處理器硬件只是和匯編知識的儲備。

LLDB與匯編調(diào)試-提高你的調(diào)試效率

6. Binary Images(二進(jìn)制映像)

Crash 發(fā)生時(shí) app 可執(zhí)行文件、加載的所有系統(tǒng)庫和第三方庫。

 # app 可執(zhí)行文件 Elephant
 0x104e80000 -        0x107b2bfff +Elephant arm64  <38c058044caa34818a83d88981986fad> /var/containers/Bundle/Application/5694FC83-018E-46E7-B060-A008867D3C9D/Elephant.app/Elephant
 
 # WCDB 可執(zhí)行文件。b512f6d343e73a0db1bcb499d2597c8a 是 WCDB 的 UUID
 # 符號化時(shí) dsym 的 UUID 需要與之匹配才能符號化
 0x10b724000 -        0x10b86ffff  WCDB arm64  <b512f6d343e73a0db1bcb499d2597c8a> /private/var/containers/Bundle/Application/5694FC83-018E-46E7-B060-A008867D3C9D/Elephant.app/Frameworks/WCDB.framework/WCDB

四、Crash 的類型

4.1 兩類主要的 Crash

引發(fā)崩潰的代碼本質(zhì)上就兩類,

一類是 c/c++ 語言層面的錯(cuò)誤,比如野指針,除零,內(nèi)存訪問異常等等(相對復(fù)雜)

對于前者,無論是 iOS 還是 Android 系統(tǒng),其底層都是 unix 或者是類 unix 系統(tǒng),都可以通過信號機(jī)制來獲取 signal 或者是 sigaction (但是只能捕捉有限的幾種類型),設(shè)置一個(gè)回調(diào)函數(shù)。

  • Watchdog 超時(shí)、用戶強(qiáng)制退出、低內(nèi)存終止等,系統(tǒng)拋出Unix信號,沒有任何的錯(cuò)誤堆棧信息

另一類是未捕獲異常 Uncaught Exception(相對簡單)

iOS 下面最常見的就是 Objective-C 的NSException(@throw 拋出),可以使用NSUncaughtExceptionHandler catch 住防止崩潰。

  • 如數(shù)組越界,給對象發(fā)送了無法識別的消息(selector方法沒有實(shí)現(xiàn),對象調(diào)用方法出錯(cuò))等,系統(tǒng)拋出一個(gè)NSException對象,對象中有出錯(cuò)的堆棧,描述了出錯(cuò)的代碼位置、類名和方法名
4.1.1 Bus Error
  • Non-existent address(訪問不存在的內(nèi)存地址)
  • Unaligned access(訪問未對齊的內(nèi)存地址)
  • Paging errors(分頁錯(cuò)誤)

在檢測順序上,先檢測 SIGBUS,再檢測 SIGSEGV。

SIGBUS 地址被放到地址總線之后,檢測出地址不對齊,發(fā)出異常信號,

SIGSEGV 地址已經(jīng)放到地址總線上后,在后續(xù)流程中檢測出內(nèi)存違法訪問,發(fā)出異常信號。

  • SIGBUS (Bus error)訪問非法地址

    指針?biāo)鶎?yīng)的地址是有效地址,但總線不能正常使用該指針,通常是未對齊的數(shù)據(jù)訪問所致。

    一些處理器架構(gòu)上要求對齊訪問數(shù)據(jù),比如只能從4字節(jié)邊界上讀取一個(gè)4字節(jié)的數(shù)據(jù)類型(對于長度4個(gè)字節(jié)的對象,其存放地址起碼要被4整除才可以)。否則向當(dāng)前進(jìn)程分發(fā)SIGBUS信號。

  • SIGSEGV (Segmentation fault、segfault)合法地址的非法訪問

    在 ARC 后很少遇到,意味著指針?biāo)鶎?yīng)的地址是無效地址,沒有物理內(nèi)存對應(yīng)該地址。

    訪問不屬于本進(jìn)程的內(nèi)存地址

    往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù)

    訪問已被釋放的內(nèi)存

  • SEGV(Segmentation Violation)

    代表無效內(nèi)存地址,比如空指針,未初始化指針,棧溢出等。

4.1.2 其他異常類型
  • EXC_CRASH(SIGABRT)

    情形:Abnormal Exit 異常退出

    未捕獲的 Objective-C 異常(NSException),導(dǎo)致系統(tǒng)發(fā)送了 Abort 信號退出,導(dǎo)致這類異常崩潰的原因是捕獲到 Objective-C/C++ 異常,并且調(diào)用了 abort() 函數(shù),會在斷言/app內(nèi)部/操作系統(tǒng)用終止方法拋出。

    1. 通常發(fā)生在異步執(zhí)行系統(tǒng)方法的時(shí)候,如CoreData/NSUserDefaults等,還有一些其他的系統(tǒng)多線程操作。這并不一定意味著是系統(tǒng)代碼存在bug,代碼僅僅是成了無效狀態(tài),或者異常狀態(tài)。
    2. 通常Foundation庫中的容器為了保護(hù)狀態(tài)正常會做一些檢測,例如插入nil到數(shù)組中等會遇到此類錯(cuò)誤。
    3. App Extensions,例如輸入法,如果花了太多時(shí)間做初始化的話就會以這種異常退出(看門狗機(jī)制)。如果擴(kuò)展程序由于在啟動時(shí)掛起進(jìn)而被kill掉,那 Report 中的Exception Subtype字段會寫 LAUNCH_HANG。因?yàn)閿U(kuò)展App沒有main函數(shù),所以任何情況下的在static constructors和+load方法里的初始化時(shí)間都會體現(xiàn)在你的擴(kuò)展或者依賴庫中。因此你應(yīng)當(dāng)盡可能的推遲這些邏輯。
    example 1: unrecognized selector sent to instance
    example 1: attempt to insert nil object from objects
    

    對于可能在別處被釋放的對象,要自己持有一份(alloc 或 copy)。

  • EXC_BREAKPOINT(SIGTRAP)

    情形:Trace Trap 追蹤捕獲

    和進(jìn)程異常退出類似,這種異常是由于在特殊的節(jié)點(diǎn)加入debugger調(diào)試節(jié)點(diǎn),如果當(dāng)前沒有調(diào)試器(debugger)依附,那么則會導(dǎo)致進(jìn)程被殺掉。可以通過 __builtin_trap() 在代碼里手動出發(fā)這種異常。

    1. 這種 Crash 在 iOS 底層的框架中經(jīng)常出現(xiàn),最常見的是GCD。底層庫(例如libdispatch)會在遇到fatal錯(cuò)誤的時(shí)候陷入這個(gè)困局。

    2. Swift代碼會在運(yùn)行時(shí)的時(shí)候遇到下述問題時(shí)拋出這種異常:

      一個(gè)non-optional的類型被賦予一個(gè)nil值

      一個(gè)失敗的強(qiáng)制轉(zhuǎn)換

    遇到這種錯(cuò)誤,查下堆棧信息并想清楚是在哪里遇到了未知情況(unexpected condition)。額外信息也可能會在設(shè)備的控制臺的日志里出現(xiàn)。你應(yīng)當(dāng)盡量修改你的代碼,去優(yōu)雅的處理這種運(yùn)行時(shí)錯(cuò)誤。例如,處理一個(gè)optional的值,通過可選綁定(Optional binding)而不是強(qiáng)制解包來獲得其值。

  • EXC_BAD_INSTRUCTION(SIGILL)

    情形:Illegal Instruction 非法指令

    當(dāng)嘗試去執(zhí)行一個(gè)非法或者未定義的指令時(shí)會觸發(fā)該異常。有可能是因?yàn)榫€程在一個(gè)配置錯(cuò)誤的函數(shù)指針的誤導(dǎo)下嘗試jump到一個(gè)無效地址。
    在Intel處理器上,ud2操作碼會導(dǎo)致一個(gè)EXC_BAD_INSTRUCTION異常,但是這個(gè)通常用來做調(diào)試用途。

    在Intel處理器上,Swift會在運(yùn)行時(shí)碰到未知情況時(shí)被停止。 詳情參考Trace Trap。

  • SIGKILL

    情形:Killed

    進(jìn)程收到系統(tǒng)指令被干掉。請自行查看Termination Reason(會包含一個(gè)命名空間和代碼)來定位線程被干掉的原因。

  • SIGQUIT

    情形:Quit 退出

    這個(gè)異常是由于其它進(jìn)程擁有高優(yōu)先級且可以管理本進(jìn)程(因此被高優(yōu)先級進(jìn)程Kill掉)所導(dǎo)致。SIGQUIT不代表進(jìn)程發(fā)生Crash了,但是它確實(shí)反映了某種不合理的行為。

    iOS中,如果占用了太長時(shí)間,鍵盤擴(kuò)展程序會隨著宿主app被干掉。因此,這種情況的異常下不太可能會在Crash Report中出現(xiàn)合理可讀的異常代碼。大概率是因?yàn)橐恍┢渌a在啟動時(shí)占用了太長時(shí)間但是在總時(shí)間限制前(看門狗的時(shí)間限制,見上文中的表格)成功結(jié)束了,但是執(zhí)行邏輯在extension退出的時(shí)候被錯(cuò)誤的執(zhí)行了。你應(yīng)該運(yùn)行Profile,仔細(xì)分析一下extension的各部分消耗時(shí)間,把耗時(shí)較多的邏輯放到background或者推遲(推遲到extension加載完畢)。

  • EXC_ARITHMETIC

    除零錯(cuò)誤會拋出此類異常

    arithmetic [?'r?θm?t?k] 算術(shù),算法

  • SIGPIPE 管道破裂

    這個(gè)信號通常在進(jìn)程間通信產(chǎn)生,比如采用FIFO(管道)通信的兩個(gè)進(jìn)程,讀管道沒打開或者意外終止就往管道寫,寫進(jìn)程會收到SIGPIPE信號。

    此外用Socket通信的兩個(gè)進(jìn)程,寫進(jìn)程在寫Socket的時(shí)候,讀進(jìn)程已經(jīng)終止。

    對一個(gè)端已經(jīng)關(guān)閉的socket調(diào)用兩次寫入操作,第二次寫入將會產(chǎn)生SIGPIPE信號,該信號默認(rèn)結(jié)束進(jìn)程。

    // 預(yù)防方式,寫在 pch 文件
    // 僅在 iOS 系統(tǒng)上支持 SO_NOSIGPIPE
    #if defined(SO_NOSIGPIPE) && !defined(MSG_NOSIGNAL)
        // We do not want SIGPIPE if writing to socket.
        const int value = 1;
        setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(int));
    #endif
    
    另外一些異常類型

    為了防止一個(gè)應(yīng)用占用過多的系統(tǒng)資源,設(shè)計(jì)了 watchdog 的機(jī)制, watchdog 會監(jiān)測應(yīng)用的性能。如果超出了該場景所規(guī)定的運(yùn)行時(shí)間,watchdog 強(qiáng)制終結(jié)這個(gè)應(yīng)用的進(jìn)程。

    Exception Code 說明
    0xbaaaaaad 并非一個(gè)真正的Crash,由用戶同時(shí)按Home鍵和音量鍵觸發(fā)。
    0xbad22222 當(dāng)VoIP程序在后臺太過頻繁的激活時(shí),系統(tǒng)可能會終止此類程序。
    0x8badf00d(badfood) launch/resume/suspend/quit/background 響應(yīng)超過規(guī)定時(shí)間會被 Watchdog 終止(詳見下表), 并產(chǎn)生一個(gè)崩潰日志。在連接Xcode調(diào)試時(shí)為了便于調(diào)試,系統(tǒng)會暫時(shí)禁用掉Watchdog,所以此類問題的發(fā)現(xiàn)需要使用正常的啟動模式。
    0xc00010ff 程序執(zhí)行大量耗費(fèi)CPU和GPU的運(yùn)算,導(dǎo)致設(shè)備過熱,觸發(fā)系統(tǒng)過熱保護(hù)被系統(tǒng)終止。這個(gè)也許是和發(fā)生crash的特定設(shè)備有關(guān),或者是和它所在的環(huán)境有關(guān)。
    0xdead10cc(deadlock) 程序退到后臺時(shí)還占用系統(tǒng)資源(如通訊錄)被系統(tǒng)終止。或者程序掛起時(shí)拿到了文件鎖或者sqlite數(shù)據(jù)庫所長期不釋放直到被凍結(jié)。如果你的app在掛起時(shí)拿到了文件鎖或者sqlite數(shù)據(jù)庫鎖,它必須請求額外的后臺執(zhí)行時(shí)間(request additional background execution time )并在被掛起前完成解鎖操作。
    0xdeadfa11(deadfall) 程序無響應(yīng)用戶強(qiáng)制退出。當(dāng)用戶長按電源鍵,直到屏幕出現(xiàn)關(guān)機(jī)確認(rèn)畫面后再長按Home鍵,將強(qiáng)制退出應(yīng)用。(不是雙擊 home 的強(qiáng)退)Exception Note 會有 SIMULATED 字段
    0x2bad45ec app因?yàn)榘踩`規(guī)操作被iOS系統(tǒng)終止。終止描述會寫:“進(jìn)程被查到在安全模式進(jìn)行非安全操作”,暗示app嘗試在禁止屏幕繪制的時(shí)候繪制屏幕,例如當(dāng)屏幕鎖定時(shí)。用戶可能會忽略這種異常,尤其當(dāng)屏幕是關(guān)閉的或者當(dāng)這種終止發(fā)生時(shí)正好鎖屏。

    說明:通過App Switcher(就是雙擊home鍵出現(xiàn)的那個(gè)界面)并不會生成Crash Report。一旦app進(jìn)入掛起狀態(tài),被iOS在任何時(shí)間終止掉都是合理的,因此這時(shí)候不會生成Crash Report。

    以下異常代碼只針對 watchOS

    Exception Code 說明
    0xc51bad01 在后臺任務(wù)占用了過多的cpu時(shí)間而導(dǎo)致watch app被干掉。想要解決這個(gè)問題,優(yōu)化后臺任務(wù),提高CPU執(zhí)行效率,或者減少后臺的任務(wù)運(yùn)行數(shù)量。
    0xc51bad02 在后臺的規(guī)定時(shí)間內(nèi)沒有完成指定的后臺任務(wù)而導(dǎo)致watch app被干掉。想要解決這個(gè)問題,需要當(dāng)app在后臺運(yùn)行時(shí)減少app的處理任務(wù)。
    0xc51bad03 沒有在規(guī)定時(shí)間內(nèi)完成后臺任務(wù),且系統(tǒng)一直非常忙以至于app無法獲取足夠的CPU時(shí)間來完成后臺任務(wù)。雖然一個(gè)app可以通過減少自身在后臺的運(yùn)行任務(wù)來避免這個(gè)問題,但是0xc51bad03這個(gè)錯(cuò)誤把矛頭指向了過高的系統(tǒng)負(fù)載,而非app本身有什么問題。
  • 附:Watchdog 超時(shí)時(shí)間

    場景 超時(shí)時(shí)間
    launch(啟動) 20s
    resume(恢復(fù)) 10s
    suspend(掛起) 10s
    quit(退出) 6s
    background(后臺) 10min

    簡單說,就是以下代理必須在規(guī)定時(shí)間內(nèi)執(zhí)行完畢,讓程序響應(yīng)起來。

    - (void)applicationDidFinishLaunching:(UIApplication *)application;
    - (void)applicationDidBecomeActive:(UIApplication *)application;
    - (void)applicationWillResignActive:(UIApplication *)application;
    - (void)applicationDidEnterBackground:(UIApplication *)application;
    - (void)applicationWillEnterForeground:(UIApplication *)application;
    - (void)applicationWillTerminate:(UIApplication *)application;
    

崩潰(準(zhǔn)確的說是程序異常終止)是程序接收到未處理信號的結(jié)果。

未處理信號有三個(gè)來源:內(nèi)核、其他進(jìn)程和應(yīng)用本身。導(dǎo)致崩潰最常見的兩個(gè)信號如下:

  • EXC_BAD_ACCESS 是一種由內(nèi)核發(fā)出的Mach異常,通常是因?yàn)閼?yīng)用試圖訪問不存在的內(nèi)存空間導(dǎo)致的。如果未能在Mach內(nèi)核級別進(jìn)行處理,它將被轉(zhuǎn)化為SIGBUS或者SIGSEGV BSD信號。
  • SIGABRT是當(dāng)產(chǎn)生未捕獲的NSException或者obj_exception_throw時(shí),應(yīng)用發(fā)給自身的BSD信號。

在 Objective-C 異常中,導(dǎo)致異常拋出最常見的原因是應(yīng)用向?qū)ο蟀l(fā)送了未實(shí)現(xiàn)的方法選擇器(比如拼寫錯(cuò)誤,對象混淆或者向已經(jīng)釋放的對象發(fā)送消息)。

4.2 Low Memory Report 低內(nèi)存報(bào)告

Low Memory Termination

跟一般的Crash結(jié)構(gòu)不太一樣,通常有Free pages,Wired Pages,Purgeable pages,largest process 組成,同時(shí)會列出當(dāng)前時(shí)刻系統(tǒng)運(yùn)行所有進(jìn)程的信息。

Low Memory Report 與其它 Crash Report 不同,它沒有堆棧信息,所以不需要符號化。一個(gè)低內(nèi)存 Report的Header會和 Crash Report 的header有些類似。緊接著Header的時(shí)各個(gè)字段的系統(tǒng)級別的內(nèi)存統(tǒng)計(jì)信息。記錄下頁大小(Page Size)字段。每一個(gè)進(jìn)程的內(nèi)存占用大小是根據(jù)內(nèi)存的頁的數(shù)量來 Report的。一個(gè)低內(nèi)存 Report最重要的部分是進(jìn)程表格。這個(gè)表格列出了所有的運(yùn)行進(jìn)程,包括系統(tǒng)在生成低內(nèi)存 Report時(shí)的守護(hù)進(jìn)程。如果一個(gè)進(jìn)程被”遺棄”了,會在[原因]一列附上具體的原因。一個(gè)進(jìn)程可能被遺棄的原因有:

  • [per-process-limit]

    進(jìn)程占用超過了它的最大內(nèi)存值。每一個(gè)進(jìn)程在常駐內(nèi)存上的限制是早已經(jīng)由系統(tǒng)為每個(gè)應(yīng)用分配好了的。超過這個(gè)限制會導(dǎo)致進(jìn)程被系統(tǒng)干掉。

    注意:擴(kuò)展程序(nimo: Extension app, 例如輸入法等)的最大內(nèi)存值更少。一些技術(shù),例如地圖視圖和SpriteKit,占用非常多的基礎(chǔ)內(nèi)存,因此不適合用在擴(kuò)展程序里。

  • [vm-pageshortage]/[vm-thrashing]/[vm]

    由于系統(tǒng)內(nèi)存壓力被干掉。

  • [vnode-limit]

    打開太多文件了。

    注意:系統(tǒng)會盡量避免在vnodes已經(jīng)枯竭的時(shí)候干掉高頻app。因此你的應(yīng)用如果在后臺,即便并沒有占用什么vnode,而有可能被殺掉。

  • [highwater]

    一個(gè)系統(tǒng)守護(hù)進(jìn)程超過過了它的內(nèi)存占用高水位(就是已經(jīng)很危險(xiǎn)了)。

  • [jettisoned]

    進(jìn)程因?yàn)槠渌豢擅枋龅脑虮粴⒌簟?/p>

當(dāng)你發(fā)現(xiàn)一個(gè)低內(nèi)存crash,與其去擔(dān)心哪一部分的代碼出現(xiàn)問題,還不如仔細(xì)審視一下自己的內(nèi)存使用習(xí)慣和針對低內(nèi)存告警(low-memory warning)的處理措施。Locating Memory Issues in Your App 列出了如何使用Leaks Instrument工具來檢查內(nèi)存泄漏,和如何使用Allocations Instrument的Mark Heap 功能來避免內(nèi)存浪費(fèi)。 Memory Usage Performance Guidelines 討論了如何處理接受到低內(nèi)存告警的問題,以及如何高效使用內(nèi)存。當(dāng)然,也推薦你去看下2010年的WWDC中的 Advanced Memory Analysis with Instruments 那一章節(jié)。

重要:Leaks和Allocation工具不能檢測所有的內(nèi)存使用情況。你需要和VM Tracker工具一起運(yùn)行(包含在Allocation工具里)來查看你的內(nèi)存運(yùn)行。默認(rèn)VM Tracker是不可用的。如果想通過VM Tracker來profile你的應(yīng)用,點(diǎn)擊instrument工具,選中”Automatic Snapshotting”標(biāo)簽或者手動點(diǎn)擊”Snapshot Now”按鈕。

五、Crash 的捕獲

5.0 Last Exception Backtrace

若程序因 NSException 而 Crash,系統(tǒng)日志中的 Last Exception Backtrace 信息是完整準(zhǔn)確的,不會受應(yīng)用層的 Crash 統(tǒng)計(jì)服務(wù)影響,可作為排查問題的參考線索。如果 Last Exception Backtrace,只包含16進(jìn)制信息的日志,必須進(jìn)行符號化來獲取有價(jià)值的堆棧信息

# 未符號化的異常堆棧
Last Exception Backtrace:

(0x18eee41c0 0x18d91c55c 0x18eee3e88 0x18f8ea1a0 0x195013fe4 0x1951acf20 0x18ee03dc4 0x1951ab8f4 0x195458128 0x19545fa20 0x19545fc7c 0x19545ff70 0x194de4594 0x194e94e8c 0x194f47d8c 0x194f39b40 0x194ca92ac 0x18ee917dc 0x18ee8f40c 0x18ee8f89c 0x18edbe048 0x19083f198 0x194d21bd0 0x194d1c908 0x1000ad45c 0x18dda05b8)

5.1 處理未捕獲異常(uncaught exceptions)

Demo :https://github.com/xcysuccess/iOSCrashUncaught

有兩種方式可以捕獲那些會導(dǎo)致崩潰的未捕獲狀態(tài)。

  • 使用 NSUncaughtExceptionHandler 函數(shù)來安裝未捕獲 Objective-C 異常的處理器。
  • 使用 signal 函數(shù)來安裝 BSD 信號處理器。

注意:signal 要在沒有附加 debugger 的環(huán)境下獲取,否則會被 debugger 優(yōu)先攔截。UncaughtExceptionHandler可以在調(diào)試狀態(tài)下捕獲

抓取 NSException
// 安裝 Objective-C 異常處理器和信號處理的代碼如下:
void InstallUncaughtExceptionHandler() {
    NSSetUncaughtExceptionHandler(&MyUncaughtExceptionHandler);
    signal(SIGABRT, SignalHandler);
    signal(SIGILL, SignalHandler);
    signal(SIGSEGV, SignalHandler);
    signal(SIGFPE, SignalHandler);
    signal(SIGBUS, SignalHandler);
    signal(SIGPIPE, SignalHandler);
}
// 對于異常和信號的響應(yīng)會在 MyUncaughtExceptionHandler 和 SignalHandler 中實(shí)現(xiàn)。在樣例程序中,以上二者的處理方式相同。

void MyUncaughtExceptionHandler(NSException *exception) {
    NSString *ret = [NSString stringWithFormat:@"異常名稱:\n%@\n\n異常原因:\n%@\n\n出錯(cuò)堆棧內(nèi)容:\n%@\n",exception.name, exception.reason, exception.callStackSymbols];
    // 將捕獲到的 exception 細(xì)節(jié)上傳到后臺
}
抓取 Signal

signal信號是Unix系統(tǒng)中的,是一種異步通知機(jī)制.信號傳遞給進(jìn)程后,在沒有處理函數(shù)的情況下,程序可以指定三種行為:

  1. 忽略該信號,但是對于信號SIGKILLSIGSTOP不可忽略
  2. 使用默認(rèn)的處理函數(shù)SIG_DFL(即 signal(sig, SIG_DFL);),大多數(shù)信號的默認(rèn)動作是終止進(jìn)程
  3. 捕獲信號,執(zhí)行用戶定義的函數(shù)

有兩個(gè)特殊的常量:

  • SIG_IGN,向內(nèi)核表示忽略此信號.對于不能忽略的兩個(gè)信號SIGKILLSIGSTOP,調(diào)用時(shí)會報(bào)錯(cuò)
  • SIG_DFL,執(zhí)行該信號的系統(tǒng)默認(rèn)動作.

還有兩個(gè)常用的函數(shù)

  • int kill(pid_t pid, int signo);,發(fā)送信號到指定的進(jìn)程
  • int raise(int signo);,發(fā)送信號給自己.
// UNIX系統(tǒng)中常用的信號有以下幾種:
SIGABRT--程序中止命令中止信號 
SIGBUS--程序內(nèi)存字節(jié)未對齊中止信號
SIGFPE--程序浮點(diǎn)異常信號
SIGILL--程序非法指令信號
SIGSEGV--程序無效內(nèi)存中止信號
SIGTERM--程序kill中止信號
SIGKILL--程序結(jié)束接收中止信號 
    
SIGALRM--程序超時(shí)信號 
SIGHUP--程序終端中止信號
SIGINT--程序鍵盤中斷信號 
SIGSTOP--程序鍵盤中止信號  
SIGPIPE--程序Socket發(fā)送失敗中止信號

// 抓取的是以下幾種
static int Beacon_errorSignals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGSEGV,
    SIGTRAP,
    SIGTERM,
    SIGKILL,
};
for (int i = 0; i < Beacon_errorSignalsNum; i++) {
    signal(Beacon_errorSignals[i], &mysighandler);
}
// 抓取信號的處理函數(shù)
void mysighandler(int sig) {
    void* callstack[128];
    NSString* name ;
    int i, frames = backtrace(callstack, 128);
    for (i = 0; i < Beacon_errorSignalsNum; i++) {
        if (Beacon_errorSignals[i] == sig ) {
            name = [Beacon_errorSignalNames[i] copy];
            break;
        }
    }
    char** strs = backtrace_symbols(callstack, frames);
    NSMutableString* exceptionStr = [[NSMutableString alloc]initWithFormat:@"異常名稱:\n%@\n\n出錯(cuò)堆棧內(nèi)容:\n",name];
    for (i =0; i <frames; i++) {
        [exceptionStr appendFormat:@"%s\n",strs[i]];
    }
    free(strs);
}

// 在應(yīng)用崩潰后,保持運(yùn)行狀態(tài)而不退出,讓響應(yīng)更加友好
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

while (!dismissed) {
    for (NSString *mode in (__bridge NSArray *)allModes) {
        CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.001, false);
    }
}

CFRelease(allModes);

這里只處理最常見的信號,但是,你可以為自己的程序添加所需的所有異常信號。

注意,有兩種異常是不能捕獲的:SIGKILL和SIGSTOP。它們會終止或者暫停應(yīng)用。(SIGKILL是命令行函數(shù)kill -9發(fā)出的,SIGSTOP是鍵入Control-Z發(fā)出的)。

如果你發(fā)現(xiàn)本應(yīng)該被捕捉的異常并沒有被捕捉到,請確定您沒有在building應(yīng)用或者library時(shí)添加了-no_compact_unwind標(biāo)簽。

64位 iOS 用了zero-cost的異常實(shí)現(xiàn)機(jī)制。在zero-cost系統(tǒng)里,每一個(gè)函數(shù)都有一個(gè)額外的數(shù)據(jù),它會描述如果一個(gè)異常在跨函數(shù)范圍內(nèi)實(shí)現(xiàn),該如何展開相應(yīng)的堆棧信息。如果一個(gè)異常發(fā)生在多個(gè)堆棧但是沒有可展開的數(shù)據(jù),那么異常處理函數(shù)自然無法跟蹤并記錄。也許在堆棧很上層的地方有異常處理函數(shù),但是如果那里沒有一個(gè)片段的可展開信息,沒辦法從發(fā)生異常的地方到那里。指定了-no_compact_unwind標(biāo)簽表明你那些代碼沒有可展開信息,所以你不能跨越函數(shù)拋出異常(也就是說無法通過別的函數(shù)捕捉當(dāng)前函數(shù)的異常)。

5.2 Xcode 提供的調(diào)試工具

都在 Edit Scheme -> Diagnostics(診斷) 依次可以找到

Runtime Sanitization
  • Address Sanitizer(地址消毒劑)

    AddressSanitizer的原理是當(dāng)程序創(chuàng)建變量分配一段內(nèi)存時(shí),將此內(nèi)存后面的一段內(nèi)存也凍結(jié)住,標(biāo)識為中毒內(nèi)存。當(dāng)程序訪問到中毒內(nèi)存時(shí)(越界訪問),就會拋出異常,并打印出相應(yīng)log信息。調(diào)試者可以根據(jù)中斷位置和的log信息,識別bug。如果變量釋放了,變量所占的內(nèi)存也會標(biāo)識為中毒內(nèi)存,這時(shí)候訪問這段內(nèi)存同樣會拋出異常(訪問已經(jīng)釋放的對象)。

  • Thread Sanitizer

    用于解決多線程問題:如何用Xcode8解決多線程問題

    • Use of uninitialized mutexes(使用未初始化的互斥器)
    • Thread leaks (missing pthread_join) 線程泄漏(缺少pthread_join)
    • Unsafe calls in signal handlers (ex:malloc) 信號處理程序中的不安全調(diào)用(例如:malloc)
    • Unlock from wrong thread 從錯(cuò)誤的線程解鎖
    • Data races 數(shù)據(jù)競爭(只要涉及到多線程編程,遇到的概率非常之高,寫多線程代碼時(shí)最容易遇到的問題,一旦踩坑,現(xiàn)象往往是偶現(xiàn)的,難以調(diào)試)

    大致原理是記錄每個(gè)線程訪問變量的信息來做分析,值得一提的是,現(xiàn)階段的Thread Sanitizer最多只同時(shí)記錄4個(gè)線程的訪問信息,在復(fù)雜的場景下,可能出現(xiàn)偶爾檢測不出data race的場景,所以需要長時(shí)間經(jīng)常性的運(yùn)行來盡可能多的發(fā)現(xiàn)data race,這也是為什么蘋果建議默認(rèn)開啟Thread Sanitizer,而且Thread Sanitizer 造成的額外性能損耗非常之小。

    Thread Sanitizer 現(xiàn)階段只能在模擬器環(huán)境下執(zhí)行,真機(jī)還不支持,有同學(xué)測試發(fā)現(xiàn),只支持64位系統(tǒng),也就是說iPhone 5及其更早的模擬器也不支持,iPhone 5s 之后才是64位系統(tǒng)。

Memory Management
  • Malloc Scribble

    申請內(nèi)存后在申請的內(nèi)存上填 0xAA,內(nèi)存釋放后在釋放的內(nèi)存上填 0x55;再就是說如果內(nèi)存未被初始化就被訪問,或者釋放后被訪問,就會引發(fā)異常,這樣就可以使問題盡快暴漏出來。

    Scribble 其實(shí)是 malloc 庫 libsystem_malloc.dylib 自身提供的調(diào)試方案

  • Malloc Guard Edges

    申請大片內(nèi)存的時(shí)候在前后page上加保護(hù),詳見保護(hù)模式

  • Guard Malloc

    使用 libgmalloc 捕獲常見的內(nèi)存問題,比如越界、釋放之后繼續(xù)使用。

    由于 libgmalloc 在真機(jī)上不存在,因此這個(gè)功能只能在模擬器上使用.

  • Zombie Objects(僵尸對象)

    Instrument 也有一個(gè) Zombie 工具,使用起來差不多。

    Zombie 的原理是用生成僵尸對象來替換 dealloc 的實(shí)現(xiàn),當(dāng)對象引用計(jì)數(shù)為 0 的時(shí)候,將需要 dealloc 的對象轉(zhuǎn)化為僵尸對象。如果之后再給這個(gè)僵尸對象發(fā)消息,則拋出異常,并打印出相應(yīng)的信息,調(diào)試者可以很輕松的找到異常發(fā)生位置。

    如果 objc_msgSend 或者 objc_release出現(xiàn)在crash的線程的附近,則進(jìn)程有可能嘗試去給一個(gè)被釋放的對象發(fā)送消息,那么可使用 Zombie 調(diào)試

    # 控制臺會多一些調(diào)試信息
    message sent to deallocated instance 0x60800000c380
    
Analyze(靜態(tài)代碼分析)

不是那么準(zhǔn)確,但是會發(fā)現(xiàn)一些問題

可以發(fā)現(xiàn)編譯中的 warning,內(nèi)存泄漏隱患,甚至還可以檢查出邏輯上的問題;所以在自測階段一定要解決Analyze發(fā)現(xiàn)的問題,可以避免出現(xiàn)嚴(yán)重的bug。

主要分析以下四種問題:

  • 邏輯錯(cuò)誤:訪問空指針或未初始化的變量等;
  • 內(nèi)存管理錯(cuò)誤:如內(nèi)存泄漏等;
  • 聲明錯(cuò)誤:從未使用過的變量;
  • 調(diào)用錯(cuò)誤:未包含使用的庫和框架。
# 內(nèi)存泄漏隱患
Potential(潛在) Leak of an object allocated on line ……
# 數(shù)據(jù)賦值隱患
The left operand of …… is a garbage value;
# 對象引用隱患
Reference-Counted object is used after it is released;
Profile(就是運(yùn)行 Instrument)

真正運(yùn)行程序,對程序進(jìn)行內(nèi)存分析(查看內(nèi)存分配情況、內(nèi)存泄露)

優(yōu)點(diǎn):分析非常準(zhǔn)確,如果發(fā)現(xiàn)有提示內(nèi)存泄露,基本可以斷定代碼問題

缺點(diǎn):分析效率低(真正運(yùn)行了一段代碼,才能對該代碼進(jìn)行內(nèi)存分析)

六、附錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內(nèi)容