【原】iOS動(dòng)態(tài)性(三) Method Swizzling以及AOP編程:在運(yùn)行時(shí)進(jìn)行代碼注入

聲明:本文是本人編程小翁原創(chuàng)档悠,轉(zhuǎn)載請(qǐng)注明左医。

用戶統(tǒng)計(jì).jpeg

用戶行為統(tǒng)計(jì)(User Behavior Statistics, UBS)一直是移動(dòng)互聯(lián)網(wǎng)產(chǎn)品中必不可少的環(huán)節(jié)牙言,也俗稱埋點(diǎn)艘儒。在保證移動(dòng)端流量不會(huì)受較大影響的前提下袜漩,PM們總是希望埋點(diǎn)覆蓋面越廣越好通殃。目前常規(guī)的做法是將埋點(diǎn)代碼封裝成工具類度液,但凡工程中需要埋點(diǎn)(如點(diǎn)擊事件、頁(yè)面跳轉(zhuǎn))的地方都插入埋點(diǎn)代碼画舌。一旦項(xiàng)目越來(lái)越復(fù)雜堕担,你會(huì)發(fā)現(xiàn)埋點(diǎn)的代碼散落在程序的各個(gè)角落,不利于維護(hù)以及復(fù)用曲聂。本文旨在探討利用iOS的運(yùn)行時(shí)機(jī)制實(shí)現(xiàn)一種可復(fù)霹购、解耦、容易維護(hù)的用戶統(tǒng)計(jì)方案朋腋。探討畢竟是探討齐疙,歡迎到在簡(jiǎn)書(shū)留言討論膜楷。本文雖有些長(zhǎng)卻是用心之作,希望你有耐心看完贞奋。

注:本文需要一些iOS的Runtime基礎(chǔ)

該方案的完成將會(huì)用到以下知識(shí):

Method Swizzling(Hook)

單元測(cè)試

一赌厅、常規(guī)埋點(diǎn)做法

接著開(kāi)頭的話題,我們先回顧一下主流的埋點(diǎn)是怎么做的轿塔。我粗糙地將埋點(diǎn)分為兩種:1特愿、頁(yè)面統(tǒng)計(jì),包括頁(yè)面停留時(shí)間勾缭、頁(yè)面進(jìn)入次數(shù)揍障;2、交互事件統(tǒng)計(jì)俩由,包括單擊毒嫡、雙擊、手勢(shì)交互等幻梯。

1)常規(guī)頁(yè)面統(tǒng)計(jì)埋點(diǎn)

以統(tǒng)計(jì)頁(yè)面進(jìn)入次數(shù)為例审胚,最簡(jiǎn)單粗暴的做法是在所有頁(yè)面的viewDidAppear:以及viewDidDisappear:中分別埋點(diǎn),將自己對(duì)應(yīng)的pageID上傳給服務(wù)端礼旅。代碼大概長(zhǎng)醬紫:

@implementationHomeViewController//...other methods- (void)viewDidAppear:(BOOL)animated{? ? [superviewWillAppear:animated];? ? [WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_ENTER"];}- (void)viewDidDisappear:(BOOL)animated{? ? [superviewDidDisappear:animated];? ? [WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_LEAVE"];}@end

+[WUserStatistics sendEventToServer:]封裝網(wǎng)絡(luò)請(qǐng)求膳叨,將ID上傳給服務(wù)器。上述方案有以下弊端:

1痘系、復(fù)用性差菲嘴。這部分埋點(diǎn)代碼很難給其他項(xiàng)目復(fù)用

2、工作量大汰翠。尤其當(dāng)頁(yè)面較多時(shí)龄坪,需要修改的代碼較多

3、引入“臟代碼”复唤,不易維護(hù)

第3點(diǎn)提到的“臟代碼”意思是用戶行為分析這種業(yè)務(wù)其實(shí)跟主業(yè)務(wù)沒(méi)太大關(guān)系健田,不應(yīng)該保持如此高的耦合度,因?yàn)檫@些代碼會(huì)干擾我們對(duì)項(xiàng)目主業(yè)務(wù)的維護(hù)佛纫。這個(gè)我個(gè)人看法妓局。

2)常規(guī)交互事件埋點(diǎn)

常規(guī)做法一般在交互事件的selector中獲取該事件的ID并上傳給服務(wù)端,代碼大概長(zhǎng)醬紫:

- (IBAction)onFavBtnPressed:(id)sender{? ? [WUserStatistics sendEventToServer:@"CTRL_EVENT_HOME_FAV"];//...do other things}

稍微大一點(diǎn)的APP如果采用這種方式呈宇,那諸如此類的埋點(diǎn)代碼將遍地都是好爬。它的缺點(diǎn)參考頁(yè)面統(tǒng)計(jì)埋點(diǎn)部分,其復(fù)用性基本為零甥啄,也就是在新項(xiàng)目中根本無(wú)法復(fù)用埋點(diǎn)代碼存炮。

小總結(jié)一下,采用常規(guī)的做法雖然直觀方便,但在可復(fù)用性穆桂、可維護(hù)性等方面有所欠缺宫盔。在我看來(lái),借助運(yùn)行時(shí)可以很好地避開(kāi)這些缺點(diǎn)享完。

二飘言、Method Swizzling、Hook與代碼注入

由于Runtime知識(shí)不屬于本文的重點(diǎn)驼侠,這里只簡(jiǎn)單介紹姿鸿。

在iOS中,我們可以在運(yùn)行時(shí)替換兩個(gè)方法的實(shí)現(xiàn)倒源,達(dá)到“勾住”某個(gè)方法并注入代碼的目的苛预。具體做法是:

重載類的“+(void)load”方法,在程序加載到內(nèi)存時(shí)利用Runtime的method_exchangeImplementations等接口將方法(設(shè)為M)的實(shí)現(xiàn)互相交換笋熬。當(dāng)方法M被調(diào)用時(shí)就會(huì)被勾住(Hook)热某,執(zhí)行我們的方法。

這種技術(shù)也稱為Method Swizzling胳螟,屬于面向切面編程(Aspect-Oriented Programming)的一種實(shí)現(xiàn)昔馋。

替換兩個(gè)方法的實(shí)現(xiàn),代碼一般長(zhǎng)醬紫:

@interfaceWHookUtility : NSObject+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;@end@implementationWHookUtility+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{? ? Classclass= cls;? ? Method originalMethod = class_getInstanceMethod(class,originalSelector);? ? Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);? ? ? ? BOOL didAddMethod =? ? class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if(didAddMethod) {? ? ? ? class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));? ? }else{? ? ? ? method_exchangeImplementations(originalMethod, swizzledMethod);? ? }}@end

這個(gè)WHookUtility工具類下文會(huì)用到糖耸。比如現(xiàn)在我們要勾住UIViewController的viewWillAppear:方法秘遏,可以這樣做:

@implementationUIViewController(userStastistics)+ (void)load {staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{? ? ? ? SEL originalSelector =@selector(viewWillAppear:);? ? ? ? SEL swizzledSelector =@selector(swiz_viewWillAppear:);? ? ? ? [WHookUtility swizzlingInClass:[selfclass] originalSelector:originalSelector swizzledSelector:swizzledSelector];? ? });}#pragma mark - Method Swizzling- (void)swiz_viewWillAppear:(BOOL)animated{//插入需要執(zhí)行的代碼NSLog(@"我在viewWillAppear執(zhí)行前偷偷插入了一段代碼");//不能干擾原來(lái)的代碼流程,插入代碼結(jié)束后要讓本來(lái)該執(zhí)行的代碼繼續(xù)執(zhí)行[selfswiz_viewWillAppear:animated];}@end

更多關(guān)于Runtime嘉竟、method swizzling邦危、面向切面編程的介紹請(qǐng)參考這里

三、基于運(yùn)行時(shí)的埋點(diǎn)方案

為了便于下文敘述舍扰,先引入一個(gè)簡(jiǎn)單的項(xiàng)目倦蚪,共有兩個(gè)頁(yè)面(HomeViewController,DetailViewController)边苹,如下:

1.gif

需求是

統(tǒng)計(jì)兩個(gè)頁(yè)面的展示與離開(kāi)次數(shù)

統(tǒng)計(jì)收藏陵且、分享單擊事件的次數(shù)

對(duì)現(xiàn)有工程代碼影響越小越好

1)統(tǒng)計(jì)兩個(gè)頁(yè)面的展示與離開(kāi)次數(shù)

這部分應(yīng)該比較直觀了,摒棄掉在每個(gè)controller中埋點(diǎn)的方式个束,我們對(duì)UIViewController添加category從而Hook到viewWillAppear:與viewWillDisappear:慕购。在這兩個(gè)方法中注入埋點(diǎn)代碼:

埋點(diǎn)代碼注入.jpg

這時(shí)候問(wèn)題來(lái)了,項(xiàng)目中每個(gè)頁(yè)面都會(huì)有自己的頁(yè)面事件編號(hào)(pageEventID)播急,此處的埋點(diǎn)代碼如何知道要發(fā)送什么pageEventID給服務(wù)端呢脓钾?輕松祭出if-else神器:

- (NSString*)pageEventID:(BOOL)bEnterPage{NSString*selfClassName =NSStringFromClass([selfclass]);NSString*pageEventID =nil;if([selfClassName isEqualToString:@"HomeViewController"]) {? ? ? ? pageEventID = bEnterPage ?@"EVENT_HOME_ENTER_PAGE":@"EVENT_HOME_LEAVE_PAGE";? ? }elseif([selfClassName isEqualToString:@"DetailViewController"]) {? ? ? ? pageEventID = bEnterPage ?@"EVENT_DETAIL_ENTER_PAGE":@"EVENT_DETAIL_LEAVE_PAGE";? ? }//else if (<#expression#>)...}

當(dāng)然,我們可以有更優(yōu)雅的方式桩警,比如用一個(gè)配置表替代上面一長(zhǎng)串的if判斷,這樣無(wú)論頁(yè)面數(shù)怎么增加昌妹,代碼始終是那么一小段捶枢。我們新建一個(gè)WGlobalUserStatisticsConfig.plist的配置表來(lái)存放每個(gè)頁(yè)面在進(jìn)入以及離開(kāi)時(shí)的pageEventID握截,結(jié)構(gòu)如下:

配置表結(jié)構(gòu).png

因此爪幻,頁(yè)面進(jìn)出統(tǒng)計(jì)中獲取pageEventID的代碼始終是以下這幾句:

- (NSString*)pageEventID:(BOOL)bEnterPage{NSDictionary*configDict = [selfdictionaryFromUserStatisticsConfigPlist];NSString*selfClassName =NSStringFromClass([selfclass]);returnconfigDict[selfClassName][@"PageEventIDs"][bEnterPage ?@"Enter":@"Leave"];}- (NSDictionary*)dictionaryFromUserStatisticsConfigPlist{NSString*filePath = [[NSBundlemainBundle] pathForResource:@"WGlobalUserStatisticsConfig"ofType:@"plist"];NSDictionary*dic = [NSDictionarydictionaryWithContentsOfFile:filePath];returndic;}

效果如下:

頁(yè)面埋點(diǎn).gif

以上就是完成了頁(yè)面進(jìn)出統(tǒng)計(jì)的埋點(diǎn)堤器,并且達(dá)到了我們的第三點(diǎn)預(yù)期:對(duì)現(xiàn)有代碼基本無(wú)影響。通過(guò)Method Swizzling的方式現(xiàn)有的工程甚至不需要import任何文件躲胳!后期代碼變動(dòng)時(shí)需要維護(hù)的僅僅是plist配置表蒜鸡。

2)統(tǒng)計(jì)收藏胯努、分享單擊事件的次數(shù)

與上一節(jié)思路一致,要做到解耦顯然需要通過(guò)category+hook來(lái)實(shí)現(xiàn)逢防。本文demo中收藏跟分享都是UIButton類型叶沛,可以考慮添加UIButton的catogory。但更好的方式是添加UIControl的category忘朝,這樣可以讓埋點(diǎn)代碼覆蓋到所有UIControl的子類中去灰署,比如button、switch局嘁、segment等溉箕,提高復(fù)用性。

既然要hook悦昵,那就要清楚到底要hookUIControl的哪(幾)個(gè)方法肴茄,只有部分方法是滿足埋點(diǎn)需求的,最好是所hook的方法能提供target但指、actionName等信息独郎。這是個(gè)嘗試的過(guò)程。

UIControl的方法列表有以下:

UIControl方法列表.png

通過(guò)觀察方法名和參數(shù)枚赡,我們有理由懷疑是倒數(shù)第二個(gè)氓癌,因其攜帶了不少貌似有價(jià)值的信息:

- (void)sendAction:(SEL)action to:(nullableid)target forEvent:(nullableUIEvent*)event;

于是寫(xiě)出測(cè)試代碼看看:

@implementationUIControl(userStastistics)+ (void)load {staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{? ? ? ? SEL originalSelector =@selector(sendAction:to:forEvent:);? ? ? ? SEL swizzledSelector =@selector(swiz_sendAction:to:forEvent:);? ? ? ? [WHookUtility swizzlingInClass:[selfclass] originalSelector:originalSelector swizzledSelector:swizzledSelector];? ? });}#pragma mark - Method Swizzling- (void)swiz_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event;{//插入埋點(diǎn)代碼[selfperformUserStastisticsAction:action to:target forEvent:event];? ? [selfswiz_sendAction:action to:target forEvent:event];}- (void)performUserStastisticsAction:(SEL)action to:(id)target forEvent:(UIEvent*)event;{NSLog(@"\n***hook success.\n[1]action:%@\n[2]target:%@ \n[3]event:%ld",NSStringFromSelector(action), target, (long)event);}@end

Log如下圖:

need-to-insert-img

Log.png

可以看到,通過(guò)category+method swizzling的方式在沒(méi)有修改現(xiàn)有工程任何代碼的情況下已經(jīng)成功Hook到所有點(diǎn)擊事件贫橙,在Hook代碼中我們知道了一個(gè)點(diǎn)擊事件的target也就是ViewController贪婉,也知道了點(diǎn)擊事件的響應(yīng)函數(shù)名,知道了點(diǎn)擊的TouchSet卢肃。這些信息已經(jīng)能滿足埋點(diǎn)需求了疲迂。

與頁(yè)面統(tǒng)計(jì)埋點(diǎn)類似,我們同樣采用plist配置表的方式避免一大長(zhǎng)串的if-else判斷:

need-to-insert-img

單擊事件配置表結(jié)構(gòu).png

有了這張配置表就很容易得到某次單擊事件的事件ID(ControlEventID):

NSString*actionString =NSStringFromSelector(action);//獲取SEL stringNSString*targetName =NSStringFromClass([targetclass]);//viewController nameNSDictionary*configDict = [selfdictionaryFromUserStatisticsConfigPlist];eventID = configDict[targetName][@"ControlEventIDs"][actionString];

事實(shí)上莫湘,我把某個(gè)頁(yè)面單元的所有事件ID分成了兩類:頁(yè)面事件ID(PageEventIDs尤蒿,頁(yè)面的進(jìn)出等)、交互事件ID(ControlEventIDs幅垮,單擊腰池、雙擊、手勢(shì)等)。分類有助于下文使用單元測(cè)試(Unit Test)進(jìn)行自動(dòng)化后期維護(hù)示弓。

埋點(diǎn)效果如圖:

need-to-insert-img

單擊埋點(diǎn)效果.gif

到這里先做了階段性的總結(jié)讳侨,本文提出的思路有以下優(yōu)越性:

與工程代碼基本解耦,避免引入“臟代碼”

即使后期工程代碼發(fā)生重構(gòu)奏属,需要修改的僅僅是plist配置表

維護(hù)配置表比維護(hù)散落在工程各個(gè)角落的代碼簡(jiǎn)單

四跨跨、基于單元測(cè)試的后期維護(hù)

俗話說(shuō),創(chuàng)業(yè)難守業(yè)更難囱皿。前面的思路基本可以完成初步的埋點(diǎn)需求勇婴。但是在實(shí)際項(xiàng)目中代碼重構(gòu)是很頻繁的。這意味著在多人協(xié)作開(kāi)發(fā)嘱腥、代碼重構(gòu)頻繁的項(xiàng)目中響應(yīng)事件方法甚至頁(yè)面名稱都可能被改掉耕渴,造成事件ID獲取不到導(dǎo)致埋點(diǎn)失效。

代碼變動(dòng)的情況無(wú)非以下幾種(這里只介紹響應(yīng)事件發(fā)生改變的情況):

1爹橱、響應(yīng)事件方法名稱改變或者刪除

比如收藏事件原先是onFavBtnPressed:萨螺,之后被改成onFavouriteBtnPressed:。代碼發(fā)生變動(dòng)但是plist配置表中由于開(kāi)發(fā)人員疏忽忘記同步修改了愧驱。這種疏忽在開(kāi)發(fā)壓力大進(jìn)度趕的情況下是有很大概率發(fā)生的慰技。由于代碼與配置表不匹配將導(dǎo)致eventID為nil。在這種情況下單元測(cè)試就很有必要了组砚,使用完備的測(cè)試用例能在發(fā)版前檢測(cè)到這種不匹配情況從而避免埋點(diǎn)失效吻商。

在單元測(cè)試中我們首先讀取plist配置文件,遍歷所有的頁(yè)面糟红。在一個(gè)頁(yè)面內(nèi)遍歷所有的ControlEventIDs艾帐,對(duì)每個(gè)響應(yīng)函數(shù)名進(jìn)行respondsToSelector:判斷:

need-to-insert-img

單元測(cè)試介紹.png

單測(cè)代碼如下:

- (void)testIfUserStatisticsConfigPlistValid{NSDictionary*configDict = [selfdictionaryFromUserStatisticsConfigPlist];XCTAssertNotNil(configDict,@"WGlobalUserStatisticsConfig.plist加載失敗");? ? ? ? [configDict enumerateKeysAndObjectsUsingBlock:^(NSString*? _Nonnull key,id_Nonnull obj,BOOL* _Nonnull stop) {XCTAssert([obj isKindOfClass:[NSDictionaryclass]],@"plist文件結(jié)構(gòu)可能已經(jīng)改變,請(qǐng)確認(rèn)");NSString*targetPageName = key;? ? ? ? Class pageClass =NSClassFromString(targetPageName);idpageInstance = [[pageClass alloc] init];//一個(gè)pageDict對(duì)應(yīng)一個(gè)頁(yè)面盆偿,存放pageID,所有的action及對(duì)應(yīng)的eventIDNSDictionary*pageDict = (NSDictionary*)obj;//頁(yè)面配置信息NSDictionary*pageEventIDDict = pageDict[@"PageEventIDs"];//交互配置信息NSDictionary*controlEventIDDict = pageDict[@"ControlEventIDs"];XCTAssert(pageEventIDDict,@"plist文件未包含PageID字段或者該字段值為空");XCTAssert(controlEventIDDict,@"plist文件未包含EventIDs字段或者該字段值為空");? ? ? ? ? ? ? ? [pageEventIDDict enumerateKeysAndObjectsUsingBlock:^(NSString*? _Nonnull key,id_Nonnull value,BOOL* _Nonnull stop) {XCTAssert([value isKindOfClass:[NSStringclass]],@"plist文件結(jié)構(gòu)可能已經(jīng)改變柒爸,請(qǐng)確認(rèn)");XCTAssertNotNil(value,@"EVENT_ID為空,請(qǐng)確認(rèn)");? ? ? ? }];? ? ? ? ? ? ? ? [controlEventIDDict enumerateKeysAndObjectsUsingBlock:^(NSString*? _Nonnull key,id_Nonnull value,BOOL* _Nonnull stop) {XCTAssert([value isKindOfClass:[NSStringclass]],@"plist文件結(jié)構(gòu)可能已經(jīng)改變事扭,請(qǐng)確認(rèn)");NSString*actionName = key;? ? ? ? ? ? SEL actionSel =NSSelectorFromString(actionName);XCTAssert([pageInstance respondsToSelector:actionSel],@"代碼與plist文件函數(shù)不匹配捎稚,請(qǐng)確認(rèn):-[%@ %@]", targetPageName, actionName);//EVENT_ID不能為空XCTAssertNotNil(value,@"EVENT_ID為空,請(qǐng)確認(rèn)");? ? ? ? }];? ? }];? ? }

我們來(lái)測(cè)試一下求橄,如果把HomeViewController的onFavBtnPressed:改成onMyFavBtnPressed:后單元測(cè)試的結(jié)果就是:

單元測(cè)試不通過(guò).png

這種改變給單測(cè)輕松捕捉到了今野,

只要XCTAssert的log夠詳細(xì),維護(hù)起來(lái)其實(shí)相當(dāng)輕松的罐农。

上圖中的log已經(jīng)明確指出-[HomeViewController onFavBtnPressed:]方法發(fā)生了改變条霜。

2、代碼中新增了響應(yīng)事件

這種情況常見(jiàn)于新版本中有新的埋點(diǎn)需求涵亏。如果代碼中新增了響應(yīng)事件并且該響應(yīng)事件是在PM要求的埋點(diǎn)列表中宰睡,但是plist有可能會(huì)漏掉該事件蒲凶。這種情況是比較棘手的。上一種情況是基于plist列表去校驗(yàn)代碼夹厌,這里就要反過(guò)來(lái)豹爹,根據(jù)代碼去校驗(yàn)plist是否有缺失裆悄。但問(wèn)題來(lái)了矛纹,一個(gè)項(xiàng)目中響應(yīng)函數(shù)往往是非常多的,并不是任何響應(yīng)函數(shù)都需要埋點(diǎn)光稼。需要埋點(diǎn)的響應(yīng)函數(shù)與其他響應(yīng)函數(shù)并沒(méi)有區(qū)別或南。

對(duì)于這種情況,一種方式是加強(qiáng)code review避免忘記往配置表中添加埋點(diǎn)(這簡(jiǎn)直就是廢話)艾君;一種是:要求埋點(diǎn)響應(yīng)函數(shù)的方法名中包含約定的字符串采够,比如收藏事件的方法名為onFavBtnPressed_UA:表示這個(gè)事件是需要埋點(diǎn)的。然后在單元測(cè)試中使用運(yùn)行時(shí)APIclass_copyMethodList取出標(biāo)記了_UA的所有函數(shù)冰垄,隨后到plist中校驗(yàn)是否存在蹬癌。不存在則表示測(cè)試用例不通過(guò),提示開(kāi)發(fā)人員校驗(yàn)虹茶。

代碼略逝薪。如果對(duì)單元測(cè)試不熟悉,可以參考單元測(cè)試

小總結(jié):

合理的單元測(cè)試可以為本文方案的后期維護(hù)減輕相當(dāng)大的負(fù)擔(dān)蝴罪,測(cè)試用例的完備性很重要董济,需要用心設(shè)計(jì)考慮周全。

五要门、結(jié)語(yǔ)

以上就是結(jié)合運(yùn)行時(shí)所設(shè)計(jì)出的用戶統(tǒng)計(jì)思路全部?jī)?nèi)容虏肾。應(yīng)該說(shuō)該方案的可復(fù)用性與解耦程度都是不錯(cuò)的,既適合于新建的工程欢搜,也適合于已經(jīng)創(chuàng)建的工程封豪。看起來(lái)內(nèi)容多炒瘟,其實(shí)總結(jié)起來(lái)無(wú)非幾個(gè)步驟:plist配置表+Hook+單元測(cè)試吹埠。利用Method Swizzling把埋點(diǎn)代碼集中管理其實(shí)也是合理的,有利于專人開(kāi)發(fā)唧领、跟蹤及維護(hù)藻雌。當(dāng)然以上思路只考慮簡(jiǎn)單的情形,更復(fù)雜的情況就需要變通了斩个,但總體思路就是如此胯杭。

思路可能不完美,但作為一種嘗試也未嘗不可受啥。路都是走出來(lái)的做个。

本文demo地址鸽心,記得star噢!

喜歡本文可以點(diǎn)一下喜歡關(guān)注我居暖,或者留個(gè)言示個(gè)愛(ài)(拋媚眼中)

不喜歡可以留言提建議顽频,我必虛心接受

歡迎轉(zhuǎn)載

作者:編程小翁

鏈接:http://www.reibang.com/p/0497afdad36b

來(lái)源:簡(jiǎn)書(shū)

簡(jiǎn)書(shū)著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處太闺。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末糯景,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子省骂,更是在濱河造成了極大的恐慌蟀淮,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钞澳,死亡現(xiàn)場(chǎng)離奇詭異怠惶,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)轧粟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)策治,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人兰吟,你說(shuō)我怎么就攤上這事通惫。” “怎么了揽祥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵讽膏,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拄丰,道長(zhǎng)府树,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任料按,我火速辦了婚禮奄侠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘载矿。我一直安慰自己垄潮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布闷盔。 她就那樣靜靜地躺著弯洗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪逢勾。 梳的紋絲不亂的頭發(fā)上牡整,一...
    開(kāi)封第一講書(shū)人閱讀 52,255評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音溺拱,去河邊找鬼逃贝。 笑死谣辞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沐扳。 我是一名探鬼主播泥从,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼沪摄!你這毒婦竟也來(lái)了躯嫉?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤卓起,失蹤者是張志新(化名)和其女友劉穎和敬,沒(méi)想到半個(gè)月后凹炸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體戏阅,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年啤它,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了奕筐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡变骡,死狀恐怖离赫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情塌碌,我是刑警寧澤渊胸,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站台妆,受9級(jí)特大地震影響翎猛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜接剩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一切厘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧懊缺,春花似錦疫稿、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至俊扳,卻和暖如春途蒋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拣度。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工碎绎, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留螃壤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓筋帖,卻偏偏與公主長(zhǎng)得像奸晴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子日麸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

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