iOS 無痕埋點解決方案—— AOP 篇(1)

簡單介紹一下 AOP

無痕埋點最重要的技術是將埋點代碼從業(yè)務代碼中剝離,放到獨立的模塊中的技術。寫業(yè)務的同學只需按照正常的設計思路編寫業(yè)務代碼,寫埋點的同學通過 AOP 技術往業(yè)務代碼中插入埋點代碼。

AOP 全稱叫 Aspect-Oriented Programming,中文名叫面向切面編程。網(wǎng)絡上有關于面向切面編程的名詞解釋。通俗一點,即可使用下圖來說明。

如果希望某個函數(shù)的實現(xiàn)邏輯如下:

希望邏輯

代碼塊 1代碼塊 2 之間的邏輯毫無干系,是兩塊獨立的邏輯,那么同時寫在一個文件的一個函數(shù)中會造成代碼臃腫,不易維護。

如果能將兩個代碼分離,編寫在不同文件中,即可簡化邏輯,增加代碼的可閱讀性:

編碼時

上圖為編碼時的代碼邏輯圖,兩份代碼塊寫在兩個不同的方法中。當使用 AOP 技術后,軟件在運行時就會將兩份邏輯合并到一個方法中,變成當初期望的邏輯的樣子。

運行時

在 iOS 平臺中,AOP 一般使用基于 ObjC 動態(tài)性質以及消息轉發(fā)的功能實現(xiàn)的。ObjC 的動態(tài)性就是所謂的 Runtime。利用 Runtime 可以對一個類的某個方法進行修改,從而實現(xiàn)運行時的邏輯變化。而消息轉發(fā)則是對面向對象的方法調用的攔截,增加自己特定的處理。Runtime 配合消息轉發(fā)就能實現(xiàn) iOS 平臺上基本的 AOP 需求。

目前,能夠完成 iOS 的 AOP 的庫有:ReactiveCocoa、RxSwift、Aspects 等等。RAC 和 Rx 系列則是為了響應式編程而出現(xiàn)的,他們功能強大,但也很重。如果僅僅只是為了無痕埋點,引入響應式變成則需要很大的成本,也是大材小用。Aspects 則是專門為 AOP 設計的,底層學習蘋果 KVO 實現(xiàn)機制,可以以對象為顆粒進行 AOP。曾經(jīng)我們的項目引入了這個庫,但如果業(yè)務已經(jīng)實現(xiàn)了 - (void)forwardInvocation:(NSInvocation *)invocation 方法,又被 Aspects 進行 AOP 后,業(yè)務的消息轉發(fā)機制就會被覆蓋,造成不可預計的后果*。

*關于 Aspects 的 bug:

在去年使用時,發(fā)現(xiàn) Aspects 與 JSPatch(都使用了 - forwardInvocation: 方法)不兼容。主要表現(xiàn)是當 JSPatch 先生效,Aspects 后生效的情況下,JSPatch 的消息轉發(fā)機制斷裂,無法完成補丁修復,甚至 Crash。

而我們在業(yè)務上也用到了 - forwardInvocation: 方法,即使沒有引入 JSPatch,也依舊會出現(xiàn)同樣的問題。

在 Issue 里查找了一番,發(fā)現(xiàn)是 class_replaceMethod 的 bug (Merge),但至今都沒有修復這個問題(已并入 master,但是在 tag 1.4.2 之后并入的,而 CocoaPods 上最新發(fā)布的版本是 1.4.1)。

所以在不引入臃腫的三方庫外,如何既安全又準確地使用 AOP 進行無痕埋點,就是本文即將討論的內容。

大部分的無痕埋點實現(xiàn)方案的弊端

在丟棄 Aspects 后,就尋找無痕埋點的解決方案。百度搜索 iOS 無痕埋點 關鍵字,得到的結果幾乎一樣。貼上搜索結果第一篇的地址:簡書 - iOS無痕埋點方案分享探究 by: SandyLoo

在此先非常感謝作者的分享,此方案給我提供了幾個版本的穩(wěn)定的無痕埋點。

但是在某次埋點 SDK 重構的過程中,發(fā)現(xiàn)了此方案的多處弊端,由于網(wǎng)絡上大部分的無痕埋點方案都與此大同小異,所以就用此例子來分析這系列的方案存在的隱患和缺陷。

方案基本介紹

無痕埋點主要是記錄用戶的操作,而用戶的操作無非就是按鈕的點擊和列表的選擇,所以無痕埋點的需求即是對用戶點擊的響應方法進行 AOP 處理,插入對應埋點方法。

UIKit 提供的用戶交互接口里,主要可以分為兩大類:

  1. Delegate 類(UITableViewUICollectionView 的點擊事件,特點是方法名定死,使用 weak 屬性持有響應對象)
  2. Target-Action 類(UIControlUIGestureRecognizer 的回調事件,特點是方法名可自定義,方法參數(shù)可有可無,使用 weak 屬性持有響應對象,支持多個響應者)

此方案也對這幾種接口提供了不同的 AOP 代碼。

1. UITableView 與 UICollectionView

這兩種對象歸結到第一類中(下文主要講解 UITableViewUICollectionView 同理就不解釋),業(yè)務通過實現(xiàn) - tableView:didSelectRowAtIndex: 方法來捕獲用戶點擊事件。此方法的方法簽名(由返回值類型和參數(shù)類型編碼而成)因 UITableViewDelegate 的定義而被定死,所以可以很好的完成 AOP 代碼。

  1. 使用 runtime 對 -[UITableView setDelegate:] 進行方法交換,插入 delegate 的捕獲代碼。
  2. 當捕獲到 delegate 對象時(一般為 ViewController),獲取該對象的類。
  3. 構建臨時方法名:aop_tableView:didSelectRowAtIndex:,判斷 2 中的類是否有這個方法。
  4. 如果有,說明此類被處理過,則不繼續(xù)。
  5. 如果沒有,將預先寫好的 static 函數(shù),通過 runtime 構建新的 Method 實例(方法名是 3 中的方法名),添加到類中。
  6. 將 5 添加的方法和原方法進行方法交換。

其中步驟 5 中預先寫好的 static 函數(shù)大致如下:

static void AOP_tableView_didSelectRowAtIndex(id<UITableViewDelegate> self, SEL _cmd, UITableView *tableView, NSIndexPath *indexPath) {
    //  先調用業(yè)務代碼
    SEL origSel = @selector(aop_tableView:didSelectRowAtIndex:);
    [self performSelector:origSel withObject:tableView withObject:indexPath];
    
    //  再埋點
    [[Tracker sharedTracker] trackEvent:@"xxxxx"];
}

分析一下此方案的弊端:

缺陷 1: 繼承導致重復埋點

如果某個業(yè)務代碼如下:

業(yè)務代碼

如果出現(xiàn)了某種代碼執(zhí)行順序:

  1. 子類實例化
  2. 父類實例化

則會出現(xiàn)如下情況:

  1. 捕獲到子類的對象,發(fā)現(xiàn)沒有經(jīng)過 AOP,則進行 AOP 處理,產生如下代碼:
子類對象被 AOP

捕獲到父類,發(fā)現(xiàn)沒有經(jīng)過 AOP (步驟 1 是在子類處理,所以父類無法檢測),則進行 AOP 處理,產生如下代碼:

父類對象被 AOP

此時,如果 1 實例化出來的子類對象還存在,或者在這之后實例化了新的子類對象,對應的埋點代碼邏輯會執(zhí)行兩次,邏輯如下:

最終子類會執(zhí)行兩次埋點邏輯

缺陷 2: 如果業(yè)務手動執(zhí)行 tableView:didSelectRowAtIndex: 也會觸發(fā)埋點

手動執(zhí)行應當是代碼產生的,而非用戶真實點擊。即使正常開發(fā)不會這么做,但是如果真的這么做了,就會產生一次不必要的埋點數(shù)據(jù)。

缺陷 3: 如果業(yè)務使用了 _cmd 參數(shù),可能取到錯誤的 SEL

上述文章中做了處理,不會有這種問題。但網(wǎng)絡上依舊有使用 performSelector 方法或通過聲明方法然后使用中括號語法來調用原方法代碼,這種方式會導致傳遞給業(yè)務的 _cmd 參數(shù)是 AOP 的 SEL,也就是上文的 aop_tableView:didSelectRowAtIndexPath:。如果業(yè)務方用到了這個 _cmd 參數(shù),則會出現(xiàn)和預期不一樣的數(shù)據(jù)。

2. UIGestureRecognizer

手勢的回調接口是 target-action,通過添加 target(回調對象) 和 action(對應的回調方法) 對,來完成手勢觸發(fā)的回調。手勢可以歸結到上述分類中的第二類。

UITableView 相比,最大的差異是方法名需要動態(tài)獲取,所以需要一個新的 AOP 邏輯:

  1. 使用 runtime 對 -[UIGestureRecognizer initWithTarget:action:] 進行方法交換,插入捕獲 targetaction 的代碼。
  2. 捕獲到 action 時,添加特殊前綴,得到 aop_action,并判斷 target 的類是否擁有 aop_action 方法。
  3. 如果有則說明此 target 對應的類已做 AOP 處理。
  4. 如果沒有,則通過預先寫好的 static 函數(shù)和 aop_action 創(chuàng)建一個 Method,添加到 target 的類中。
  5. 將 4 添加的 aop_action 方法和原 action 方法進行方法交換。

其中步驟 4 中的方法大致如下:

static void AOP_gestureRecognizerAction(id self, SEL _cmd, UIGestureRecognizer *sender) {
    //   調用原方法
    NSString *sel = [@"aop_" stringByAppendingString:NSStringFromSelector(_cmd)];
    [self performSelector:NSSelectorFromString(sel) withObject:sender];
    
    //  埋點
    [[Tracker sharedTracker] trackEvent:@"xxxxx"];
}

缺陷

手勢的缺陷將與 UIControl 的缺陷一并介紹。

3. UIControl

UIControl 和手勢類似,也是使用 target-action 接口來回調控件狀態(tài)變化事件。但這里需要和手勢分開介紹。

與手勢不同的是,UIControl 并不是通過動態(tài)添加方法來完成無痕埋點,而是直接攔截系統(tǒng)方法(調用 action 方法的方法)來完成埋點。具體如下:

  1. 使用 runtime 對 -[UIControl sendAction:to:forEvent:] 進行方法交換,插入捕獲發(fā)送事件的代碼。
  2. 捕獲到發(fā)送事件時,埋點。

UIGestureRecognizer 和 UIControl 的缺陷

缺陷1: 沒有攜帶 sender 參數(shù)的 action 可能會導致手勢埋點閃退

由于手勢預先寫好了函數(shù),而函數(shù)的參數(shù)列表包含了手勢本身(也就是通常的 sender 參數(shù)),但是業(yè)務寫的方法不一定都包含 sender 參數(shù),所以這里會有隱患。

缺陷2: 手勢埋點會遺漏使用 addTarget:action: 方法添加的回調

上述文章中只 hook 了初始化方法,并沒有 hook add 方法,所以如果業(yè)務使用 init 方法創(chuàng)建對象,再用 add 方法添加回調,埋點就會遺漏。

但這不是大問題。

缺陷3: 當業(yè)務同時綁定手勢和控件到同一個方法時可能會導致閃退

如果手勢和按鈕綁定到同一個 action 時,并且手勢和按鈕都被當做 sender 參數(shù)傳入到 action 中。當按鈕點擊時,就會觸發(fā)手勢埋點,如果手勢埋點取了手勢的 view 就會閃退:

static void AOP_gestureRecognizerAction(id self, SEL _cmd, UIGestureRecognizer *sender) {
    //   用戶點擊了按鈕,但是還是走了手勢的埋點,并且 sender 是個按鈕對象

    //   調用原方法
    NSString *sel = [@"aop_" stringByAppendingString:NSStringFromSelector(_cmd)];
    [self performSelector:NSSelectorFromString(sel) withObject:sender];

    //  取 View 來構建埋點參數(shù)
    UIView *view = sender.view;   //  此處會閃退,因為傳進來可能是 UIButton
    UIViewController *controller = sender.nextResponse;
    [[Tracker sharedTracker] trackEvent:@"xxxxx"];
}

手勢和按鈕綁定到同一個 action 的業(yè)務場景舉例:ActionSheet

如果你自己實現(xiàn)了 ActionSheet,你可能會在 ActionSheet 上方陰影部分添加手勢來隱藏 ActionSheet ,同時你也會增加一個取消按鈕,來隱藏 ActionSheet。這兩個綁定的 action 可能會是同一個方法。

缺陷4: 如果缺陷 2 不閃退,UIControl 觸發(fā)事件會導致兩次埋點

即使缺陷 3 沒有導致閃退,UIControl 的埋點在 - sendAction:to:forEvent: 方法中,手勢的埋點在 action 方法中。用戶點擊了按鈕,先執(zhí)行 - sendAction:to:forEvent: 的埋點,然后執(zhí)行業(yè)務的 action,也就會執(zhí)行手勢的埋點。這就導致產生了一次垃圾數(shù)據(jù)

缺陷5: 如果業(yè)務手動調用 action 會導致不必要的手勢埋點

如果業(yè)務主動調了一次 action,并非用戶實際操作,手勢埋點替換 action 的方式下也會被埋點。這樣也產生了一次垃圾數(shù)據(jù)。

缺陷6: 移除 target-action 后埋點依舊有效

移除 target-action 時并不能恢復被 runtime 改造的 action 方法,所以埋點依舊生效。

無痕埋點方案實現(xiàn)

為了讓業(yè)務代碼安全無誤的執(zhí)行,我們必須盡可能地不修改業(yè)務代碼,也就是不會對業(yè)務代碼進行 Runtime 處理。

在遵循此原則下,我們設計了如下的無痕埋點解決方案。

Delegate 系列無痕埋點

UITableView 介紹 Delegate 的無痕埋點。

在沒有無痕埋點的情況下,ControllerUITableView 的持有關系如下:

ViewController 和 Table View 持有關系

為了盡可能的不修改 View Controller 的內容,我們?yōu)?delegate 那條虛線添加了一個中間對象 Proxy:

插入一個中間對象

同時為了防止 Proxy 銷毀,我們使用關聯(lián)對象讓 TableView 強引用 Proxy。

為何不是 Controller 強持有 Proxy?

因為一個 table view 只能有一個 delegate,但是一個 controller 可以成為多個 table view 的 delegate

為何 Proxy 是弱引用 View Controller 而不是強引用?

因為不管是 VC 持有 Proxy 還是 TableView 持有 Proxy,只要 Proxy 持有 VC,就會產生循環(huán)引用。

而大體的步驟如下:

  1. 使用 runtime 對 -[UITabelView setDelegate:] 進行方法交換,替換 setter。
  2. 創(chuàng)建 Proxy 對象。
  3. 將 delegate 傳遞給 Proxy 對象。
  4. 將 Proxy 當做新的 delegate 參數(shù)傳遞給原方法
  5. 將 TableView 使用關聯(lián)對象強持有 Proxy

而 Proxy 要做的工作:

  1. 攔截 - tableView:didSelectRowAtIndexPath: 方法,并做埋點,同時將此事件轉發(fā)給 View Controller
  2. 由于攔截了 delegate,就會攔截所有 delegate 方法,所以 Proxy 要模擬 View Controller 對 UITableViewDelegate 協(xié)議中的幾個方法的響應情況。
  3. 對于 View Controller 實現(xiàn)的方法,需要將事件轉發(fā)給 View Controller。

Proxy 的代碼大致如下:

//  self.delegate 就是實際的 View Controller

//  攔截此 delegate 方法
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if ([self.delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) {
        [self.delegate tableView:tableView didSelectRowAtIndexPath:indexPath];
    }
    
    //  埋點
}

//  使用以下四個方法來模擬 delegate 其他方法的響應以及轉發(fā)這些方法

- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
    BOOL conforms = [super conformsToProtocol:aProtocol];
    if (!conforms) {
        conforms = [self.delegate conformsToProtocol:aProtocol];
    }
    return conforms;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    BOOL hasResponds = [super respondsToSelector:aSelector];
    hasResponds |= [self.delegate respondsToSelector:aSelector];
    return hasResponds;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:selector];
    if (!methodSignature) {
        if ([self.delegate respondsToSelector:selector]) {
            methodSignature = [(NSObject *)self.delegate methodSignatureForSelector:selector];
        }
    }
    return methodSignature;
}

- (void)forwardInvocation:(NSInvocation*)invocation {
    [invocation invokeWithTarget:self.delegate];
}

經(jīng)過以上處理,ViewController 的代碼無需任何改動,通過中間對象來攔截 - tableView:didSelectRowAtIndexPath: 方法來實現(xiàn)埋點。遵循盡可能不修改業(yè)務代碼的原則。

Target-Action 系列無痕埋點

UIGestureRecognizer 和 UIControl 的埋點實現(xiàn)方案相同,此處僅介紹 UIButton 的無痕埋點。

在沒有無痕埋點的情況下,UIViewController 和 UIButton 的持有關系如下:

View Controller 和 Button 持有關系

同樣,為了不修改 View Controller ,我們也添加了一個對象。但基于 Target-Action 是個集合的關系,這個對象并非中間對象,而是附屬對象:

增加一個 Action 對象

大體步驟如下:

  1. 使用 runtime 對 - addTarget:action: 方法交換,插入捕獲代碼
  2. 當捕獲到時,創(chuàng)建 Action 對象
  3. [target class]action 記錄到 Action 對象中(當做埋點參數(shù))
  4. 調用原方法,將 target 和 action 添加進 UIButton
  5. 調用原方法,將 Action 和 action: 添加進 UIButton

Action 對象實現(xiàn)大致如下:

- (void)action:(UIControl *)sender {
    // 埋點
}

接下來,當按鈕產生事件后,會依次執(zhí)行 View Controller 的代碼和 Action 的代碼,Action 則可以通過步驟 3 記錄的數(shù)據(jù)來完成埋點。

為了防止 Action 對象銷毀,我們需要拿其中一個對象關聯(lián)住 Action,但是用 View Controller 還是 UIButton 來持有需要進行一系列的場景模擬。

當 UIButton 持有 Action 時:

image.png

Button 先于 View Controller 銷毀

Action 銷毀,兩處 target 持有關系斷裂

View Controller 先于 Button 銷毀

Action 沒銷毀,且依舊響應 UIButton 的點擊事件(非預期效果)

當 View Controller 持有 Action 時:

image.png

Button 先于 View Controller 銷毀

Action 沒銷毀,兩處 target 持有關系斷裂

View Controller 先于 Button 銷毀

Action 銷毀,兩處 target 持有關系斷裂

通過模擬,發(fā)現(xiàn)使用 View Controller 持有 Action 對象更合適

接下來還有一些細節(jié)要處理:

  1. View Controller 可以接受多個 Button 的點擊事件,所以關聯(lián)對象的 Key 需要動態(tài)生成一個唯一 Key。可以通過 VC類名.控件類名.方法名.ControlEvent名 來生成 Key。
  2. UIControl 支持 remove 操作,所以也要 hook remove 方法,刪除 Action 對象。
  3. UIGestureRecognizer 有 - initWithTarget:action: 方法,也需要被 hook,然后按照 - addTarget:action: 同樣的方式處理
  4. Action 對象可以根據(jù)自己的埋點需求,通過屬性來存儲埋點時需要的參數(shù)。我們用 Action 記錄了 VC 的類名以及方法名稱,供生成埋點唯一 code。

解決的缺陷

現(xiàn)在再回來分析之前的缺陷是否還存在:

缺陷1(解決): 沒有攜帶 sender 參數(shù)的 action 可能會導致手勢埋點閃退

埋點和業(yè)務走兩套 target-action,不會因為是否包含 sender 參數(shù)導致出錯

缺陷2(解決): 手勢埋點會遺漏使用 addTarget:action: 方法添加的回調

不管是手勢還是控件,都可以埋到 addTarget:action: 的方法

缺陷3(解決): 當業(yè)務同時綁定手勢和控件到同一個方法時可能會導致閃退

手勢埋點和控件埋點的 Action 對象完全獨立,互不干擾。自然不會因為 sender 參數(shù)類型不對導致閃退

缺陷4(解決): 如果缺陷 2 不閃退,UIControl 觸發(fā)事件會導致兩次埋點

手勢埋點和控件埋點的 Action 對象完全獨立,互不干擾。所以手勢觸發(fā)時不會執(zhí)行按鈕埋點,按鈕觸發(fā)時不會執(zhí)行手勢埋點。

缺陷5(解決): 如果業(yè)務手動調用 action 會導致不必要的埋點

手勢埋點和控件埋點均沒有修改業(yè)務代碼,所以業(yè)務如果自己調用了 action 也不會觸發(fā)埋點。

缺陷6(解決): 移除 target-action 后埋點依舊有效

增加對 remove 的監(jiān)聽,實現(xiàn)埋點和 target-action 同步增刪。

總結

經(jīng)過分析,最終產生的無痕埋點方案安全高效,沒有大量的方法替換的操作,對象銷毀后 AOP 環(huán)境也會跟著銷毀,并且適應各種業(yè)務場景。

以上就是 iOS 端無痕埋點解決方案 AOP 部分的實現(xiàn)。

再接下來的篇幅中,將會介紹無痕埋點如何生成一個唯一事件標識,以及如何在無痕埋點中攜帶一些業(yè)務數(shù)據(jù)。

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

推薦閱讀更多精彩內容