Android卡頓優化分析及解決方案,全面掌握!

一、卡頓介紹及優化工具選擇

1.1. 卡頓問題介紹

對于用戶來說我們的應用當中的很多性能問題比如內存占用高、流量消耗快等不容易被發現,但是卡頓卻很容易被直觀的感受到,對于開發者來說,卡頓問題又難以定位,那么它究竟難在哪里呢?

卡頓問題難點:

  • 產生原因錯綜復雜:代碼、內存、繪制、IO等都有可能導致卡頓
  • 不易復現:線上卡頓問題在線下難以復現,這和用戶當時的系統環境有很大關系(比如當時用戶磁盤空間不足導致的IO寫入性能下降從而引發了卡頓,所以我們最好能記錄在發生卡頓時用戶當時的場景)

1.2. 優化工具選擇

①、CPU Profiler

  • 圖形化的形式展示執行時間、調用棧等
  • 信息全面,包含所有線程
  • 運行時開銷嚴重,整體都會變慢

使用方式:

  • Debug.startMethodTracing("");
  • Debug.stopMethodTracing("");
  • 生成文件在sd卡:Android/data/packagename/files

②、Systrace

  • 監控和跟蹤Api調用,線程運行情況,生成Html報告
  • 要求是在API18以上使用,所以這里推薦使用TraceCompat

使用方式:

Systrace優點

  • 輕量級,開銷小
  • 直觀反映CPU利用率
  • 右側Alert一欄會給出相關建議

③、StrictMode

  • Android2.3引入的工具類——嚴苛模式,Android提供的一種運行時檢測機制,幫助開發者檢測代碼中的一些不規范的問題
  • 包含:線程策略和虛擬機策略檢測
  • 線程策略:1、自定義的耗時調用,detectCustomSlowCalls() 2、磁盤讀取操作,detectDiskReads 3、網絡操作,detectNetwork
  • 虛擬機策略:1、Activity泄露,detectActivityLeaks() 2、Sqlite對象泄露,detectLeakedSqliteObjects 3、檢測實例數量,setClassInstanceLimit()

現在到之前的Demo中來實際使用一下,找到我們的Application類,新增一個方法initStrictMode():

private void initStrictMode(){
        if (DEV_MODE) {
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectCustomSlowCalls() //API等級11,使用StrictMode.noteSlowCode
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()// or .detectAll() for all detectable problems
                    .penaltyLog() //在Logcat 中打印違規異常信息
                    .build());
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()
                    .setClassInstanceLimit(FeedBean.class, 1)
                    .detectLeakedClosableObjects() //API等級11
                    .penaltyLog()
                    .build());
        }
    }

首先在這里加了一個標記位DEV_MODE,也就是只在線下開發的時候才會走到這個方法。對于線程策略使用方式就是StrictMode.setThreadPolicy,然后就是一些配置比如磁盤的讀取、寫入、網絡監控等,如果出現了違規情況我們使用的是penaltyLog()方法在日志中打印出違規信息,這里你也可以選擇別的方式。對于虛擬機策略這里是配置需要檢測出Sqlite對象的泄露,并且這里還設置某個類的實例數量是x,如果大于x它應該會被檢測出不合規。

二、自動化卡頓檢測方案及優化

2.1. 為什么需要自動化卡頓檢測

  • 上面介紹的幾種系統工具只適合線下實際問題作針對性分析
  • 線上及測試環節需要自動化檢測方案幫助開發者定位卡頓,記錄卡頓發生時的場景

2.2. 自動化卡頓檢測方案原理

  • 消息處理機制,一個線程不管有多少Handler都只會有一個Looper對象存在,主線程中執行的任何代碼都會通過Looper.loop()方法執行,loop()函數中有一個mLogging對象
  • mLogging對象在每個message處理前后都會被調用
  • 主線程如果發生卡頓,則一定是在dispatchMessage方法中執行了耗時操作,然后我們可以通過mLogging對象對dispatchMessage執行的時間進行監控

我在這里從Looper.java的loop()方法的源碼中截取了一段代碼,大家看下:

// This must be in a local variable, in case a UI event sets the logger
if (logging != null) {
    logging.println(">>>>> Dispatching to " + msg.target + " " +
        msg.callback + ": " + msg.what);
}
 
......
此處省略一大段代碼
 
if (logging != null) {
    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}            

它在Message執行的前后都打印了一段日志并且是不同的,所以我們可以通過這個來判斷Message處理的開始和結束的時機。

具體的實現原理:

  • 使用 Looper.getMainLooper.setMessageLogging()來設置自己的logging
  • 匹配>>>>> Dispatching,閾值之后在子線程中執行任務(獲取堆棧及場景信息,比如內存大小、電量、網絡狀態等)
  • 匹配<<<<< Finished,說明在指定的閾值之內message被執行完成沒有發生卡頓,任務啟動之前取消掉

2.3. AndroidPerformanceMonitor

  • 非侵入式的性能監控組件,通知形式彈出卡頓信息
  • implementation 'com.github.markzhai:blockcanary-android:1.5.0'

下面我們在項目中實際使用一下:

首先在application中進行初始化:

//BlockCanary初始化
BlockCanary.install(this,new AppBlockCanaryContext()).start();

這里入參有一個AppBlockCanaryContext,這個是我們自定義BlockCanary配置的一些信息:

public class AppBlockCanaryContext extends BlockCanaryContext {
  
    public String provideQualifier() {
        return "unknown";
    }
 
    public String provideUid() {
        return "uid";
    }
 
    public String provideNetworkType() {
        return "unknown";
    }
 
    public int provideMonitorDuration() {
        return -1;
    }
 
    //設置卡頓閾值為500ms
    public int provideBlockThreshold() {
        return 500;
    }
 
    public int provideDumpInterval() {
        return provideBlockThreshold();
    }
 
    public String providePath() {
        return "/blockcanary/";
    }
 
    public boolean displayNotification() {
        return true;
    }
 
    public boolean zip(File[] src, File dest) {
        return false;
    }
 
    public void upload(File zippedFile) {
        throw new UnsupportedOperationException();
    }
 
    public List<String> concernPackages() {
        return null;
    }
 
    public boolean filterNonConcernStack() {
        return false;
    }
 
    public List<String> provideWhiteList() {
        LinkedList<String> whiteList = new LinkedList<>();
        whiteList.add("org.chromium");
        return whiteList;
    }
 
    public boolean deleteFilesInWhiteList() {
        return true;
    }
 
    public void onBlock(Context context, BlockInfo blockInfo) {
        Log.i("jarchie","blockInfo "+blockInfo.toString());
    }
}

然后在MainActivity中模擬一次卡頓,讓當前線程休息2s,然后來看一下這個組件會不會通知我們:

        try {
            Thread.currentThread().sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }        

當我們把程序運行之后,會發現手機桌面上出現了一個Blocks的圖標,這個玩意和之前我們使用LeakCanary的時候有點像哈,然后點進去果然發現了剛剛的Block信息,如下所示:

這里詳細的打出了當前的CPU核心數、進程名、內存情況、block的堆棧信息等等,我們就可以根據這些堆棧找到對應哪個類的哪一行代碼出現了問題,然后進行修改即可。

對于這種方案的總結如下:

  • 非侵入式方案:可以監控在主線程中執行的任何方法并且不需要我們手動埋點
  • 方便精準,定位到代碼某一行

這種方案網上有很多的使用資料,但是實際上它也是存在一定的問題的,自動檢測方案的問題:

  • 確實卡頓了,但卡頓堆棧可能不準確
  • 和OOM一樣,最后的堆棧只是表象,不是真正的問題

舉個栗子:主線程在T1 T2時間段內發生了卡頓,卡頓檢測方案獲取卡頓堆棧的信息是T2時刻,但是實際情況可能是整個這一段時間之內某個函數的耗時過長導致的卡頓,捕獲堆棧的時機此時該函數已經執行完成,所以在T2時刻捕獲的堆棧信息并不能準確的反應現場情況。

自動檢測方案優化

  • 獲取監控周期內的多個堆棧,而不僅是最后一個,這樣如果發生卡頓,由于我們有多個堆棧信息,所以可以推測出整個周期內究竟發生了什么,能夠更加清晰的還原卡頓現場

海量卡頓堆棧處理:高頻卡頓上報量太大,會導致服務端有壓力

  • 分析:一個卡頓下多個堆棧大概率有重復
  • 解決:對一個卡頓下堆棧進行hash排重,找出重復的堆棧
  • 效果:極大的減少展示量同時更高效找到卡頓堆棧

三、ANR實戰分析

3.1. 什么是 ANR?

ANR(Application Not Responding)是指應用程序未響應,Android 系統對于一些事件需要在一定時間范圍內完成,如果超過預定時間未能得到有效響應或者響應時間過長,都會造成 ANR。

ANR 的產生需要滿足三個條件:

  • 主線程:只有 應用程序的主線程 響應超時才會產生 ANR
  • 超時時間:產生 ANR 的上下文不同,超時時間也會不同,但只要在這個時間上限內沒有響應就會 ANR
  • 輸入事件/特定操作:輸入事件是指按鍵、觸屏等設備輸入事件,特定操作是指 BroadcastReceiver 和 Service 的生命周期中的各個函數,產生 ANR 的上下文不同,導致 ANR 的原因也會不同

為了降低因網絡訪問導致的 ANR,在 Android 4.0 之后強制規定訪問網絡必須在子線程處理,如果在主線程訪問網絡將會拋出 NetworkOnMainThreadException。

只要是耗時操作都可能會阻塞主線程,耗時操作要求放在子線程。

3.2. ANR 發生場景

不同的場景產生 ANR 的方式也不同,在這里詳細講講各種情況產生的場景。

ANR 事件 超時時間 相應日志描述
點擊事件(按鍵和觸摸事件) 5s 內沒被處理 Input event dispatching timed out
Service 前臺 Service 20s,后臺 Service 200s 未完成啟動 Timeout executing service
BroadcastReceiver 前臺廣播 10s,后臺廣播 60s,onReceive() 在規定時間內沒處理完 Timeout of broadcast Broadcast Record
ContentProvider publish 在 10s 內沒處理完 Timeout publishing content providers

需要注意的是,前臺廣播的 ANR 時間雖然是 10s 內 onReceive() 沒有執行完就提示,這是在沒有點擊觸摸事件導致 ANR 的前提下才是 10s,否則會先觸發點擊事件的 ANR,onReceive() 有可能執行不到 10s 就發生 ANR,所以不要在 onReceive() 處理耗時操作。

在實際項目中,大多數的 ANR 都是點擊觸摸事件超時導致,會超時的原因也主要由以下三個原因導致:

  • 數據導致的 ANR:頻繁 GC 導致線程暫停,處理事件時間被拉長
  • 線程阻塞或死鎖導致的 ANR
  • Binder 導致的 ANR:Binder 通信數據量過大

所以,我們想要得到為什么會出現 ANR,就必須對于原理了解清楚,且知道有多少情況會導致出現事件被拉長的問題。

3.3. 系統對 ANR 的捕捉原理

在網上有很多分析 ANR 的文章都都將 ANR 觸發過程理解為裝炸彈和拆炸彈的過程,但說到本質上,系統內部對于 ANR 的觸發流程其實也很簡單,ANR 也是建立在主線程 Looper 機制上的,簡單理解就是 先發送一個延時消息,然后在特定位置移除這個消息,如果消息沒有被移除則證明整個流程出現問題,執行 ANR 處理

觸發 ANR 生成日志時,在不同的系統版本會有所不同,上圖中是通過 ANRHelper 類處理 ANR 日志收集,在其他較低系統版本上是 AppErrors 類處理 ANR 日志收集。

3.4. 如何分析 ANR

3.4.1. traces.txt 信息概覽

當發生 ANR 時系統會在 /data/anr/ 目錄額外生成一份 traces.txt 日志,方便我們可以了解到發生 ANR 時的基本信息和堆棧信息。

traces.txt 日志信息如下(以主線程為例):

// main 代表的主線程
// Native 是線程狀態
// 下面的是堆棧信息
"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 obj=0x73cff4c0 self=0xafa84400
  | sysTid=5790 nice=0 cgrp=top-app sched=1073741825/1 handle=0xb2717534
  | state=S schedstat=( 3240607247 80660807 2053 ) utm=283 stm=41 core=1 HZ=100
  | stack=0xbe7c1000-0xbe7c3000 stackSize=8MB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/5790/stack)
  native: #00 pc 00048968  /system/lib/libc.so (__ioctl+8)
  native: #01 pc 0001b0cf  /system/lib/libc.so (ioctl+38)
  native: #02 pc 0003cd25  /system/lib/libbinder.so (_ZN7android14IPCThreadState14talkWithDriverEb+168)
  native: #03 pc 0003d737  /system/lib/libbinder.so (_ZN7android14IPCThreadState15waitForResponseEPNS_6ParcelEPi+238)
  native: #04 pc 0003662d  /system/lib/libbinder.so (_ZN7android8BpBinder8transactEjRKNS_6ParcelEPS1_j+36)
  native: #05 pc 000999cf  /system/lib/libandroid_runtime.so (???)
  native: #06 pc 00607b09  /system/framework/arm/boot-framework.oat (Java_android_os_BinderProxy_transactNative__ILandroid_os_Parcel_2Landroid_os_Parcel_2I+140)
  at android.os.BinderProxy.transactNative(Native method)
  at android.os.BinderProxy.transact(Binder.java:930)
  at android.view.IWindowSession$Stub$Proxy.remove(IWindowSession.java:924)
  at android.view.ViewRootImpl.dispatchDetachedFromWindow(ViewRootImpl.java:3306)
  at android.view.ViewRootImpl.doDie(ViewRootImpl.java:5961)
  - locked <0x0ed5befa> (a android.view.ViewRootImpl)
  at android.view.ViewRootImpl.die(ViewRootImpl.java:5938)
  at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:459)
  at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:397)
  - locked <0x05ba7d9d> (a java.lang.Object)
  at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
  at d.h.b.q.n.a$c.removeViewImmediate(SourceFile:1)
  at android.widget.Toast$TN.handleHide(Toast.java:496)
  at android.widget.Toast$TN$2.handleMessage(Toast.java:346)
  at android.os.Handler.dispatchMessage(Handler.java:102)
  at android.os.Looper.loop(Looper.java:154)
  at android.app.ActivityThread.main(ActivityThread.java:6193)
  at java.lang.reflect.Method.invoke!(Native method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
關鍵信息 描述
main main 標識是主線程。在 traces.txt 中如果是線程會命名為 “thread-x” 的格式,x 表示線程 id,逐步遞增
prio 線程優先級,默認是 5
tid tid 不是線程的 id,是線程唯一標識 id
group 線程組名稱
sCount 該線程被掛起的次數
dsCount 線程被調試器掛起的次數
obj 對象地址
self 該線程 native 的地址
sysTid 線程號(主線程的線程號和進程號相同)
nice 線程的調度優先級
sched 分別標志了線程的調度策略和優先級
cgrp 調度歸屬組
handle 線程處理函數的地址
state 調度狀態
schedstat 從 /proc/[pid]/task/[tid]/schedstat 讀出,三個值分別表示線程在 cpu 上執行的時間、線程的等待時間和線程執行的時間片長度,不支持這項信息的三個值都是 0
utm 線程用戶態下使用的時間值(單位是 jiffies)
stm 內核態下的調度時間值
core 最后執行這個線程的 cpu 核的序號
線程狀態 說明 描述
UNDEFINED = -1
ZOMBIE = 0 TERMINATED 線程已經終止
RUNNING = 1 RUNNABLE or running now 正常運行
TIMED_WAIT = 2 TIMED_WAITING Object.wait() 等待,一般是調用 Object.wait(2000)
MONITOR = 3 BLOCKED on a monitor synchronized
WAIT = 4 WAITING in Object.wait() 調用 Object.wait() 或 LockSupport.park() 等待
INITIALIZING = 5 allocated, not yet running 已經初始化線程,但是還沒有進行 start
STARTING = 6 started, not yet on thread list 已經 start 但是沒有 run
NATIVE = 7 off in a JNI native method native 線程出問題,有三種常見情況: 1、項目自己有 JNI 開發線程 2、有 IO 操作(IO 的本質是調用 Linux 內核的函數) 3、AQS 鎖住了
VMWAIT = 8 waiting on a VM resource 沒有時間片
SUSPENDED = 9 suspended,usually by GC or debugger 被 GC 掛起的(該情況發生的可能性不高)
Blocked 死鎖(查看 CPU usage 會發現幾乎都是 0%,這也是死鎖的體現)

其中線程狀態和堆棧信息是我們最關心的,它能夠幫助我們快速定位到具體位置(堆棧信息有我們應用的函數調用的話)。

3.4.2. 日志分析思路

日志分析思路主要可以分為四個步驟:

  • ANR 日志準備(traces.txt + mainlog)
  • 在 traces.txt 找到 ANR 信息(發生 ANR 時間節點、主線程狀態、事故點、ANR 類型)
  • 在 mainlog 日志分析發生 ANR 時的 CPU 狀態
  • 在 traces.txt 分析發生 ANR 時的 GC 情況(分析內存)

3.4.2.1. ANR 日志準備(traces.txt + mainlog)

在發生 ANR 的時候,系統會收集 ANR 相關的信息提供給開發者:

  • 發生 ANR 時會收集 trace 信息能找到各個線程的執行情況和 GC 信息,trace 文件保存在 /data/anr/traces.txt
  • 在 mainlog 日志中有 ANR 相關的信息和發生 ANR 時的 CPU 使用情況

簡單說就是我們至少需要兩份文件:/data/anr/traces.txt 和 mainlog 日志。如果有 eventlog 能更快的定位到 ANR 的類型,當然 traces.txt 和 mainlog 也能分析得到。

traces.txt 文件通過命令 adb pull /data/anr/ 獲取。

mainlog 日志需要在程序運行時就時刻記錄 adb logcat -v time -b main > mainlog.log。

3.4.2.1.1. 在 traces.txt 找到 ANR 信息(發生 ANR 時間節點、主線程狀態、事故點、ANR 類型)

當我們拿到 traces.txt 文件時,主要找四個信息:發生 ANR 時的時間節點、主線程狀態(在文件中搜索 main)、ANR 類型、事故點(堆棧信息中找到我們應用中的函數)

分析發生 ANR 時進程中各個線程的堆棧,一般有幾種情況:

  • 主線程狀態是 Runnable 或 Native,堆棧信息中有我們應用中的函數,則很有可能就是執行該函數時候發生了超時
  • 主線程狀態是 Block,非常明顯的線程被鎖,這時候可以看是被哪個線程鎖了,可以考慮優化代碼,如果是死鎖問題,就更需要及時解決了
  • 由于抓 trace 的時刻很有可能耗時操作已經執行完了(ANR -> 耗時操作執行完畢 -> 系統抓 trace),這時候的 trace 就沒有什么用了(在堆棧信息找不到我們應用的函數調用)
/data/anr/traces.txt

// 發生 ANR 時的時間節點
----- pid 5790 at 2022-07-19 13:23:32 -----

// 主線程狀態
"main" prio=5 tid=1 Waiting

// 事故點
// 不一定能找到我們應用的調用函數,因為抓 trace 的時候耗時操作可能已經執行完了,例如下面的堆棧
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(Binder.java:930)
at android.view.IWindowSession$Stub$Proxy.remove(IWindowSession.java:924)
at android.view.ViewRootImpl.dispatchDetachedFromWindow(ViewRootImpl.java:3306)
at android.view.ViewRootImpl.doDie(ViewRootImpl.java:5961)
- locked <0x0ed5befa> (a android.view.ViewRootImpl)
at android.view.ViewRootImpl.die(ViewRootImpl.java:5938)
at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:459)
at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:397)
- locked <0x05ba7d9d> (a java.lang.Object)
at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
at d.h.b.q.n.a$c.removeViewImmediate(SourceFile:1)
at android.widget.Toast$TN.handleHide(Toast.java:496)
at android.widget.Toast$TN$2.handleMessage(Toast.java:346)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6193)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

因為從上面的堆棧中其實不能分析到 ANR 類型,所以可以再借助 eventlog 或 mainlog 日志找到,可以在 mainlog 日志 搜索關鍵詞 ANR in

mainlog.log(對應的 adb logcat -v time -b main > mainlog.log)

07-19 13:23:38.029  2003  2016 E ActivityManager: ANR in com.example.demo (com.example.demo/.ui.login.LoginActivity)
07-19 13:23:38.029  2003  2016 E ActivityManager: PID: 5790
07-19 13:23:38.029  2003  2016 E ActivityManager: PSS: 42718 kB
07-19 13:23:38.029  2003  2016 E ActivityManager: Reason: Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it.  Outbound queue length: 0.  Wait queue length: 1.)
07-19 13:23:38.029  2003  2016 E ActivityManager: Load: 16.16 / 10.88 / 4.95

在 eventlog 日志 搜索關鍵詞 am_anr

07-19 13:22:29.166  2003  2016 I am_anr  : [0,3392,com.example.demo,955792964,Input dispatching timed out (Waiting because no window has focus but there is a focused application that may eventually add a window when it finishes starting up.)]
3.4.2.1.2. 在 mainlog 日志分析發生 ANR 時的 CPU 狀態

在 mainlog 日志中根據 ANR 時間節點并 搜索關鍵詞 CPU usage 查看發生 ANR 前后 CPU 的使用情況,從 CPU usage 信息中大概可以分析:

  • 如果某些進程的 CPU 占用百分比較高,幾乎占用了所有 CPU 資源,而發生 ANR 的進程(一般說的是我們的 app 進程)CPU 占用為 0% 或非常低,則認為 CPU 資源被占用,app 進程沒有被分配足夠的資源,從而發生了 ANR。這種情況多數可以認為是系統狀態的問題,并不是由應用造成的(簡單講就是其他進程 CPU 使用率非常高自己低,就是系統資源分配不足導致
  • 如果發生 ANR 的進程(一般說的是我們的 app 進程)CPU 占用較高,如到了 80% 或 90% 以上,則可以懷疑應用內一些代碼不合理消耗掉了 CPU 資源,如出現了死循環或者后臺有許多線程執行任務等等原因,這就要結合 traces.txt 和 ANR 前后的 mainlog 日志進一步分析(簡單理解就是 IO 非常頻繁,要么死循環了,要么上鎖了
  • 如果 CPU 總用量不高,該進程和其他進程的占用過高,這有一定概率是由于某些主線程的操作就是耗時過長,或者是由于主進程被鎖造成的
mainlog.log

07-19 13:23:38.029  2003  2016 E ActivityManager: CPU usage from 2068ms to -8489ms ago (2022-07-19 13:23:27.434 to 2022-07-19 13:23:37.991):
07-19 13:23:38.029  2003  2016 E ActivityManager:   30% 2003/system_server: 16% user + 14% kernel / faults: 7835 minor 541 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   13% 5790/com.example.demo: 9.2% user + 3.9% kernel / faults: 11775 minor 141 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   5.4% 247/mmcqd/0: 0% user + 5.4% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   5.4% 2591/com.xtc.i3launcher: 4.3% user + 1% kernel / faults: 4186 minor 276 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   3.3% 36/kworker/u8:3: 0% user + 3.3% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   2.9% 410/audioserver: 1.7% user + 1.2% kernel / faults: 79 minor 3 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   2.8% 5754/com.netease.xtc.cloudmusic: 2.8% user + 0% kernel / faults: 1954 minor 315 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   2.3% 2457/com.android.phone: 1.6% user + 0.7% kernel / faults: 2226 minor 513 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   2.2% 35/kworker/u8:2: 0% user + 2.2% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   2.2% 356/surfaceflinger: 0.9% user + 1.3% kernel / faults: 464 minor
07-19 13:23:38.029  2003  2016 E ActivityManager:   2.1% 110/kswapd0: 0% user + 2.1% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   2% 448/kworker/u8:8: 0% user + 2% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   1.9% 444/kworker/u8:7: 0% user + 1.9% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   1.9% 4683/kworker/u8:9: 0% user + 1.9% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   1.4% 4782/com.android.commands.monkey: 0.5% user + 0.8% kernel / faults: 1598 minor 3 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   1.1% 299/logd: 0.5% user + 0.5% kernel / faults: 200 minor 115 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   1.1% 3499/super_log: 0.2% user + 0.8% kernel / faults: 69 minor 1 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.9% 7/rcu_preempt: 0% user + 0.9% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.8% 2795/com.android.dialer: 0.7% user + 0% kernel / faults: 1270 minor 221 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.4% 4696/mdss_fb0: 0% user + 0.4% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.3% 12/ksoftirqd/1: 0% user + 0.3% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.3% 20/ksoftirqd/3: 0% user + 0.3% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.3% 258/core_ctl/0: 0% user + 0.3% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.3% 2178/VosRXThread: 0% user + 0.3% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.3% 2183/sdcard: 0% user + 0.3% kernel / faults: 42 minor 3 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.2% 3/ksoftirqd/0: 0% user + 0.2% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.2% 290/jbd2/mmcblk0p43: 0% user + 0.2% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.1% 15/migration/2: 0% user + 0.1% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.1% 16/ksoftirqd/2: 0% user + 0.1% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.1% 19/migration/3: 0% user + 0.1% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.1% 269/kworker/0:4: 0% user + 0.1% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0.1% 355/servicemanager: 0% user + 0% kernel / faults: 68 minor
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 1//init: 0% user + 0% kernel / faults: 6 minor 4 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 5/kworker/0:0H: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 8/rcu_sched: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 10/migration/0: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 11/migration/1: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 14/kworker/1:0H: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 135/mdp3_ppp: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 223/irq/114-5-0024: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 271/kworker/0:1H: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 276/kworker/3:3: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 309/debuggerd: 0% user + 0% kernel / faults: 237 minor 27 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 415/media.extractor: 0% user + 0% kernel / faults: 117 minor 66 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 419/netd: 0% user + 0% kernel / faults: 134 minor 2 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 2177/VosTXThread: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 2713/com.xtc.i3launcher:push: 0% user + 0% kernel / faults: 955 minor 58 major
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 2767/perffeedbackd: 0% user + 0% kernel / faults: 77 minor
07-19 13:23:38.029  2003  2016 E ActivityManager:   0% 4546/kworker/1:3: 0% user + 0% kernel
07-19 13:23:38.029  2003  2016 E ActivityManager: 84% TOTAL: 14% user + 17% kernel + 51% iowait + 0.6% softirq

發生死鎖時的 CPU 狀態如下:

3.4.2.1.3. 在 traces.txt 分析發生 ANR 時的 GC 情況(分析內存)

當上面分析了 CPU 狀態后發現是非 CPU 問題時,就需要從內存 GC 分析,因為 GC 會觸發 STW(Stop The World)導致線程執行時間被拉長。

// 這里只截取了一部分 GC 信息
// 如果還有其他信息,還需要分析如 GC 回收率等,下面的 GC 信息是正常的,這里僅用于展示
Total time waiting for GC to complete: 64.298ms
Total GC count: 30
Total GC time: 4.961s
Total blocking GC count: 1
Total blocking GC time: 149.286ms
1234567

其實 ANR 問題主要就是兩類問題:

  • CPU 問題
  • GC 問題

所以定位 ANR 總的來說就是

  • 判定是否為 CPU 問題:常見的是在主線程事件發生
  • 如果非 CPU 問題,再去定位 GC 問題
  • GC 問題直接去看 traces.txt 上面的 GC 信息
    • 常規 GC 導致的問題,就是有頻繁的對象創建
    • 常規少量數據不會出現有這個問題,但是數據量異常將會引發連鎖反應,ANR 是結果的體現,具體體現是卡頓和內存泄漏

3.5. ANR 解決方案

  • 將所有耗時操作如訪問網絡、socket 通信、查詢大量 SQL 語句、復雜邏輯計算等都放在子線程中,然后通過 handler.sendMessage、runOnUIThread 等方式更新 UI。無論如何都要確保用戶界面的流暢度,如果耗時操作需要讓用戶等待,可以在界面上顯示進度條
  • 將 IO 操作放在異步線程。在一些同步的操作主線程有可能被鎖,需要等待其他線程釋放響應鎖才能繼續執行,這樣會有一定的 ANR 風險,對于這種情況有時也可以用異步線程來執行相應的邏輯,另外,要避免死鎖的發生
  • 使用 Thread 或 HandlerThread 時,調用 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)設置優先級,否則仍然會降低程序響應,因為默認 Thread 優先級和主線程相同
  • 使用 Handler 處理工作線程結果,而不是使用 Thread.wait() 或 Thread.sleep() 來阻塞主線程
  • Activity 的 onCreate() 和 onResume() 回調中避免耗時代碼
  • BroadcastReceiver 中 onReceive() 代碼也要盡量減少耗時,建議使用 IntentService 處理
  • 各個組件的生命周期函數都不應該有太耗時的操作,即使對于后臺 Service 或 ContentProvider 來講,雖然應用在后臺運行時生命周期函數不會有用戶輸入引起無響應的 ANR,但其執行時間過長也會引起 Service 或 ContentProvider 的 ANR

以上的常規解決方案實際上只有一個核心,就是降低線程阻塞時間,將耗時操作放到子線程處理

3.6. ANR 監控方案

3.6.1. FileObserver

FileObserver 可以做到指定文件目錄的監控功能,我們可以使用它實現監控 /data/anr 目錄,當該目錄有變動時即說明發生了 ANR。

public class ANRFileObserver extends FileObserver {
    private static final String TAG = "ANRFileObserver";

    public ANRFileObserver(String path) {
        super(path);
    }

    @Override
    public void onEvent(int event, @Nullable String path) {
        switch (event) {
            case FileObserver.ACCESS: // 文件被訪問
                Log.i(TAG, "ACCESS = " + path);
                break;
            case FileObserver.ATTRIB: // 文件屬性被修改,如 chmod、chown、touch 等
                Log.i(TAG, "ATTRIB = " + path);
                break;
            case FileObserver.CLOSE_NOWRITE: // 不可寫文件被 close
                Log.i(TAG, "CLOSE_NOWRITE = " + path);
                break;
            case FileObserver.CREATE: // 創建新文件
                Log.i(TAG, "CREATE = " + path);
                break;
            case FileObserver.DELETE: // 文件被刪除
                Log.i(TAG, "DELETE = " + path);
                break;
            case FileObserver.DELETE_SELF: // 自刪除,即一個可執行文件在執行時刪除自己
                Log.i(TAG, "DELETE_SELF = " + path);
                break;
            case FileObserver.MODIFY: // 文件被修改
                Log.i(TAG, "MODIFY = " + path);
                break;
            case FileObserver.MOVE_SELF: // 自移動,即一個可執行文件在執行時移動自己
                Log.i(TAG, "MOVE_SELF = " + path);
                break;
            case FileObserver.MOVED_FROM: // 文件被移走
                Log.i(TAG, "MOVED_FROM = " + path);
                break;
            case FileObserver.MOVED_TO: // 文件被移動過來
                Log.i(TAG, "MOVED_TO = " + path);
                break;
            case FileObserver.OPEN: // 文件被打開
                Log.i(TAG, "OPEN = " + path);
                break;
            default:
                Log.i(TAG, "event = " + event + ", path = " + path);
                break;
        }
    }
}

ANRFileObserver fileObserver = new ANRFileObserver("/data/anr");
fileObserver.startWatching();

但是該監控方案也有弊端導致適配性不是很好:

  • /data/anr/ 目錄可能會因為廠商定制化沒有權限無法訪問
  • 應用被殺死時可能無法及時的監控

3.6.2. ANR WatchDog

Android 基于 Looper 的方案寫了一個 WatchDog 監控,同樣的也是通過 Handler post 消息檢測時間,相關流程如下:


我們也可以參考 WatchDog 的源碼和原理自定義一個監控 ANR 的 WatchDog,事件 ANR 是 5s 無響應,那就設定每 5s 從 Looper 插一條消息,如果 5s 后還沒執行完成就說明出現了 ANR。具體代碼如下:

public class ANRWatchDog extends Thread {
    private static final String TAG = "ANRWatchDog";
    private static final int TIMEOUT = 5000;

    private static ANRWatchDog sWatchDog;

    private final Handler mMainHandler = new Handler(Looper.getMainLooper());

    private final ANRChecker mAnrChecker = new ANRChecker();

    private ANRListener mAnrListener;

    private ANRWatchDog() {
        super("ANR-WatchDog-Thread");
    }

    public static ANRWatchDog getInstance() {
        if (sWatchDog == null) {
            sWatchDog = new ANRWatchDog();
        }
        return sWatchDog;
    }

    @Override
    public void run() {
        // 設置為后臺線程
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        while (true) {
            synchronized (this) {
                // 開始計時,往主線程 Looper 插一條消息
                mAnrChecker.schedule();
                // 每 5s 循環一次
                long waitTime = TIMEOUT;
                long start = SystemClock.uptimeMillis();
                while (waitTime > 0) { // 避免提前喚醒,保證 5s 檢測一次
                    try {
                        wait(waitTime);
                    } catch (InterruptedException e) {
                        Log.w(TAG, e.toString());
                    }
                    waitTime = TIMEOUT - (SystemClock.uptimeMillis() - start);
                }
                // 沒有超時 5s,繼續循環
                if (!mAnrChecker.isBlocked()) {
                    continue;
                }
            }

            // 響應超過 5s 認為已經發生了 ANR,將堆棧信息打印出來
            String stackTrace = getStackTraceInfo();
            if (mAnrListener != null) {
                mAnrListener.onAnrHappened(stackTrace);
            }
            mAnrListener = null;
            break;
        }
    }

    private String getStackTraceInfo() {
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement element : Looper.getMainLooper().getThread().getStackTrace()) {
            sb.append(element.toString()).append("\r\n");
        }
        return sb.toString();
    }

    private class ANRChecker implements Runnable {
        private boolean completed;
        private long startTime;
        private long executeTime = SystemClock.uptimeMillis();

        @Override
        public void run() {
            synchronized (ANRWatchDog.this) {
                completed = true; // 執行完修改標志位
                executeTime = SystemClock.uptimeMillis();
            }
        }

        void schedule() {
            completed = false;
            startTime = SystemClock.uptimeMillis();
            mMainHandler.postAtFrontOfQueue(this); // 往主線程 Looper 插入一條消息
        }

        // 如果標志位是 false 或響應時間超過 5s
        boolean isBlocked() {
            return !completed || executeTime - startTime >= TIMEOUT;
        }
    }

    public ANRWatchDog setANRListener(ANRListener listener) {
        mAnrListener = listener;
        return this;
    }

    public interface ANRListener {
        void onAnrHappened(String stackTrack);
    }
}

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ANRWatchDog.getInstance().setANRListener(new ANRWatchDog.ANRListener() {
            @Override
            public void onAnrHappened(String stackTrack) {
                Log.i("ANRWatchDog", "stack = " + stackTrack);
            }
        }).start();

        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    // 模擬 anr
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

ANR 定位分析總結如下:

  • 在 traces.txt 找到發生 ANR 時間節點、主線程的狀態、ANR 類型和事故點
  • 在 mainlog 日志查看 CPU 狀態
  • 根據以上步驟收集的信息大致判斷問題原因
    • 是 CPU 問題還是 非 CPU 問題
    • 如果是非 CPU 問題,那么看 GC 處理信息
  • 在 traces.txt 分析 CG 信息
  • 結合項目代碼和以上步驟分析到的原因,定位到問題修復 ANR

其實 ANR 發生的原因本質上只有三個:

  • 線程掛起
  • CPU 不給資源
  • GC 觸發 STW 導致線程執行時間被拉長

四、應用界面秒開

應用界面秒開的實現方案:

  • SysTrace查看CPU運行程度,以及啟動優化部分的優雅異步+優雅延遲初始化
  • 界面異步Inflate、X2C、繪制優化
  • 提前獲取頁面數據

4.1. 界面秒開率統計

  • onCreate到onWindowFocusChanged
  • 實現特定接口,在具體方法中統計耗時

這里來介紹一個開源方案:Lancet,它是一個輕量級的Android AOP框架:

  • 編譯速度快,支持增量編譯
  • API簡單,沒有任何多余代碼插入apk(包體積優化)
  • @Proxy通常用于對系統API調用的Hook
  • @Insert通常用于操作APP與Library的類

下面我們來具體使用一下這個庫,我們使用這個庫來統計頁面的onCreate()方法到onWindowsFocusChanged()方法之間的加載耗時情況:

①、添加依賴

這里大家可以參考github上的使用方式進行依賴的添加,主要是兩個部分:工程的build.gradle和app module的build.gradle:

classpath 'me.ele:lancet-plugin:1.0.6' //工程的build.gradle
 
apply plugin: 'me.ele.lancet' //module的build.gradle
//lancet
compileOnly 'me.ele:lancet-base:1.0.6'

②、編寫一個實體類,定義用于上述兩個方法時間統計的成員變量:

public class ActivityLive {
 
    public long mOnCreateTime;
    public long mOnWindowsFocusChangedTime;
 
}

③、創建統計方法的工具類,在類中分別編寫onCreate()和onWindowFocusChanged()方法,關于具體的注解的使用含義詳見代碼注釋:

public class ActivityHooker {
 
    public static ActivityLive mLive;
 
    static {
        mLive = new ActivityLive();
    }
 
    //@Insert:使用自己程序中自己的一些類需要添加,值這里就指定onCreate()方法,
    //可配置項mayCreateSuper是當目標函數不存在的時候可以通過它來創建目標方法
    //@TargetClass:框架知道要找的類是哪個,可配置項Scope.ALL:匹配value所指定的所有類的子類
    @Insert(value = "onCreate",mayCreateSuper = true)
    @TargetClass(value = "androidx.appcompat.app.AppCompatActivity", scope = Scope.ALL)
    protected void onCreate(Bundle savedInstanceState) {
        mLive.mOnCreateTime = System.currentTimeMillis();
        Origin.callVoid(); //無返回值的調用
    }
 
 
    //注解含義同上面onCreate()
    @Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
    @TargetClass(value = "androidx.appcompat.app.AppCompatActivity", scope = Scope.ALL)
    public void onWindowFocusChanged(boolean hasFocus) {
        mLive.mOnWindowsFocusChangedTime = System.currentTimeMillis();
        Log.i("onWindowFocusChanged","---"+(mLive.mOnWindowsFocusChangedTime - mLive.mOnCreateTime));
        Origin.callVoid();
    }
 
}

下面運行程序來看下結果:

界面秒開監控緯度

  • 總體耗時:onCreate()--->onWindowsFocusChanged(),更精確的時間可以通過自定義接口來實現
  • 生命周期耗時:onCreate()、onStart()、onResume()等等
  • 生命周期間隔耗時:各個生命周期耗時時間差

五、優雅監控耗時盲區

5.1. 為什么會出現耗時盲區

對于一般的監控方案,它的監控指標只是一個大的范圍,只是一個數據,比如:

  • 生命周期的間隔
  • onResume到Feed展示的間隔
  • 舉個栗子:比如在Activity的生命周期當中postMessage,很有可能在Feed展示之前執行,如果msg耗時1s,那么Feed展示時間也就相對應的延遲1s,如果是200ms,那么自動化卡頓監測方案實際上就監測不到它,但是你的列表展示就相對應的延時200ms

如下代碼所示,我首先在Activity的onCreate()方法中發送了一個msg,并且打印了一條日志

然后在列表展示的第一條同樣打印一條日志:

最后輸出的結果如下:

從執行結果來看,這個MSG是跑在Feed展示之前的,這個msg模擬的耗時是1s,此時用戶看到界面的時間也就被往后延遲了1s。其實這個場景還是很常見的,因為我們可能由于某些業務需求在某個生命周期或者某個階段及某些第三方的SDK中會做一些handler post的操作,這個操作很有可能會在列表展示之前被執行到,所以出現這種耗時的盲區,既普遍又不好排查。

耗時盲區監控難點

  • 通過細化監控的方式知道盲區時間,但是不清楚在盲區中具體在做什么
  • 線上盲區無從排查

5.2. 耗時盲區監控線下方案

這種場景非常適合之前說過的一個工具,你能想到是什么嗎?————答案是TraceView:

  • 特別適合一段時間內的盲區監控
  • 線程具體時間做了什么,一目了然

5.3. 耗時盲區監控線上方案

  • 方法一:主線程的所有方法都是通過msg來執行的,那么我們是否可以通過mLogging來做盲區監測呢?mLogging確實知道主線程發送了msg執行了一段代碼,但是它并不清楚msg具體的調用棧信息,它所能獲取到的調用棧信息都是系統回調它的,它并不清楚msg是被誰拋出的,這個只能說可以,但是不太好。
  • 方法二:是否可以通過AOP的方式來切Handler的sendMessage()等方法呢?使用這種方式我們可以知道發送msg的堆棧信息,但是這種方案并不清楚具體的執行時間,你只知道這個msg是在哪里被發送的,你并不知道它會在什么時候執行。

可行性方案:

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

推薦閱讀更多精彩內容