iOS 組件化(一)常見(jiàn)方案解析

關(guān)于組件化的探討已經(jīng)有不少了拔恰,綜合比較了各種方案后因谎,我傾向于使用面向接口的方式進(jìn)行組件化。

這是一篇從代碼層面講解模塊解耦的文章颜懊,會(huì)全方位地展示如何實(shí)踐面向接口的思想财岔,盡量全面地探討在模塊管理和解耦的過(guò)程中,需要考慮到的各種問(wèn)題河爹,并且給出實(shí)際的解決方案匠璧,以及對(duì)應(yīng)的模塊管理開(kāi)源工具:ZIKRouter。你也可以根據(jù)本文的內(nèi)容改造自己現(xiàn)有的方案咸这,即使你的項(xiàng)目不進(jìn)行組件化夷恍,也可以參考本文進(jìn)行代碼解耦。

文章主要內(nèi)容:

  • 如何衡量模塊解耦的程度
  • 對(duì)比不同方案的優(yōu)劣
  • 在編譯時(shí)進(jìn)行靜態(tài)路由檢查媳维,避免使用不存在的模塊
  • 如何進(jìn)行模塊解耦酿雪,包括模塊重用、模塊適配侄刽、模塊間通信指黎、子模塊交互
  • 模塊的接口和依賴(lài)管理
  • 管理界面跳轉(zhuǎn)邏輯

什么是組件化

將模塊單獨(dú)抽離、分層州丹,并制定模塊間通信的方式袋励,從而實(shí)現(xiàn)解耦,以及適應(yīng)團(tuán)隊(duì)開(kāi)發(fā)当叭。

為什么需要組件化

主要有4個(gè)原因:

  • 模塊間解耦
  • 模塊重用
  • 提高團(tuán)隊(duì)協(xié)作開(kāi)發(fā)效率
  • 單元測(cè)試

當(dāng)項(xiàng)目越來(lái)越大的時(shí)候茬故,各個(gè)模塊之間如果是直接互相引用,就會(huì)產(chǎn)生許多耦合蚁鳖,導(dǎo)致接口濫用磺芭,當(dāng)某天需要進(jìn)行修改時(shí),就會(huì)牽一發(fā)而動(dòng)全身醉箕,難以維護(hù)钾腺。

問(wèn)題主要體現(xiàn)在:

  • 修改某個(gè)模塊的功能時(shí),需要修改許多其他模塊的代碼讥裤,因?yàn)檫@個(gè)模塊被其他模塊引用
  • 模塊對(duì)外的接口不明確放棒,外部甚至?xí){(diào)用不應(yīng)暴露的私有接口,修改時(shí)會(huì)耗費(fèi)大量時(shí)間
  • 修改的模塊涉及范圍較廣己英,很容易影響其他團(tuán)隊(duì)成員的開(kāi)發(fā)间螟,產(chǎn)生代碼沖突
  • 當(dāng)需要抽離模塊到其他地方重用時(shí),會(huì)發(fā)現(xiàn)耦合導(dǎo)致根本無(wú)法單獨(dú)抽離
  • 模塊間的耦合導(dǎo)致接口和依賴(lài)混亂,難以編寫(xiě)單元測(cè)試

所以需要減少模塊之間的耦合厢破,用更規(guī)范的方式進(jìn)行模塊間交互荣瑟。這就是組件化,也可以叫做模塊化摩泪。

你的項(xiàng)目是否需要組件化

組件化也不是必須的笆焰,有些情況下并不需要組件化:

  • 項(xiàng)目較小,模塊間交互簡(jiǎn)單见坑,耦合少
  • 模塊沒(méi)有被多個(gè)外部模塊引用嚷掠,只是一個(gè)單獨(dú)的小模塊
  • 模塊不需要重用,代碼也很少被修改
  • 團(tuán)隊(duì)規(guī)模很小
  • 不需要編寫(xiě)單元測(cè)試

組件化也是有一定成本的荞驴,你需要花時(shí)間設(shè)計(jì)接口不皆,分離代碼,所以并不是所有的模塊都需要組件化戴尸。

不過(guò)粟焊,當(dāng)你發(fā)現(xiàn)這幾個(gè)跡象時(shí),就需要考慮組件化了:

  • 模塊邏輯復(fù)雜孙蒙,多個(gè)模塊間頻繁互相引用
  • 項(xiàng)目規(guī)模逐漸變大项棠,修改代碼變得越來(lái)越困難
  • 團(tuán)隊(duì)人數(shù)變多,提交的代碼經(jīng)常和其他成員沖突
  • 項(xiàng)目編譯耗時(shí)較大
  • 模塊的單元測(cè)試經(jīng)常由于其他模塊的修改而失敗

組件化方案的8條指標(biāo)

決定了要開(kāi)始組件化之路后挎峦,就需要思考我們的目標(biāo)了香追。一個(gè)組件化方案需要達(dá)到怎樣的效果呢?我在這里給出8個(gè)理想情況下的指標(biāo):

  1. 模塊間沒(méi)有直接耦合坦胶,一個(gè)模塊內(nèi)部的修改不會(huì)影響到另一個(gè)模塊
  2. 模塊可以被單獨(dú)編譯
  3. 模塊間能夠清晰地進(jìn)行數(shù)據(jù)傳遞
  4. 模塊可以隨時(shí)被另一個(gè)提供了相同功能的模塊替換
  5. 模塊的對(duì)外接口容易查找和維護(hù)
  6. 當(dāng)模塊的接口改變時(shí)透典,使用此模塊的外部代碼能夠被高效地重構(gòu)
  7. 盡量用最少的修改和代碼,讓現(xiàn)有的項(xiàng)目實(shí)現(xiàn)模塊化
  8. 支持 Objective-C 和 Swift顿苇,以及混編

前4條用于衡量一個(gè)模塊是否真正解耦峭咒,后4條用于衡量在項(xiàng)目實(shí)踐中的易用程度。最后一條必須支持 Swift纪岁,是因?yàn)?Swift 是一個(gè)必然的趨勢(shì)凑队,如果你的方案不支持 Swift,說(shuō)明這個(gè)方案在將來(lái)的某個(gè)時(shí)刻必定要改進(jìn)改變幔翰,而到時(shí)候所有基于這個(gè)方案實(shí)現(xiàn)的模塊都會(huì)受到影響漩氨。

基于這8個(gè)指標(biāo),我們就能在一定程度上對(duì)我們的方案做出衡量了遗增。

方案對(duì)比

現(xiàn)在主要有3種組件化方案:URL 路由叫惊、target-action、protocol 匹配做修。

接下來(lái)我們就比較一下這幾種組件化方案霍狰,看看它們各有什么優(yōu)缺點(diǎn)抡草。必須要先說(shuō)明的是,沒(méi)有一個(gè)完美的方案能滿(mǎn)足所有場(chǎng)景下的需求蚓耽,需要根據(jù)每個(gè)項(xiàng)目的需求選擇最適合的方案渠牲。

URL 路由

目前 iOS 上絕大部分的路由工具都是基于 URL 匹配的旋炒,或者是根據(jù)命名約定步悠,用 runtime 方法進(jìn)行動(dòng)態(tài)調(diào)用。

這些動(dòng)態(tài)化的方案的優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單瘫镇,缺點(diǎn)是需要維護(hù)字符串表鼎兽,或者依賴(lài)于命名約定,無(wú)法在編譯時(shí)暴露出所有問(wèn)題铣除,需要在運(yùn)行時(shí)才能發(fā)現(xiàn)錯(cuò)誤谚咬。

代碼示例:

// 注冊(cè)某個(gè)URL

[URLRouter registerURL:@"app://editor" handler:^(NSDictionary *userInfo) {

    UIViewController *editorViewController = [[EditorViewController alloc] initWithParam:userInfo];

    return editorViewController;

}];
// 調(diào)用路由

[URLRouter openURL:@"app://editor/?debug=true" completion:^(NSDictionary *info) {

}];

URL router 的優(yōu)點(diǎn):

  • 極高的動(dòng)態(tài)性,適合經(jīng)常開(kāi)展運(yùn)營(yíng)活動(dòng)的 app尚粘,例如電商
  • 方便地統(tǒng)一管理多平臺(tái)的路由規(guī)則
  • 易于適配 URL Scheme

URL router 的缺點(diǎn):

  • 傳參方式有限择卦,并且無(wú)法利用編譯器進(jìn)行參數(shù)類(lèi)型檢查,因此所有的參數(shù)都只能從字符串中轉(zhuǎn)換而來(lái)
  • 只適用于界面模塊郎嫁,不適用于通用模塊
  • 不能使用 designated initializer 聲明必需參數(shù)
  • 要讓 view controller 支持 url秉继,需要為其新增初始化方法,因此需要對(duì)模塊做出修改
  • 不支持 storyboard
  • 無(wú)法明確聲明模塊提供的接口泽铛,只能依賴(lài)于接口文檔尚辑,重構(gòu)時(shí)無(wú)法確保修改正確
  • 依賴(lài)于字符串硬編碼,難以管理
  • 無(wú)法保證所使用的模塊一定存在
  • 解耦能力有限盔腔,url 的”注冊(cè)”杠茬、”實(shí)現(xiàn)”、”使用”必須用相同的字符規(guī)則弛随,一旦任何一方做出修改都會(huì)導(dǎo)致其他方的代碼失效瓢喉,并且重構(gòu)難度大

字符串解耦的問(wèn)題

如果用上面的8個(gè)指標(biāo)來(lái)衡量,URL 路由只能滿(mǎn)足”支持模塊單獨(dú)編譯”舀透、”支持 OC 和 Swift”兩條栓票。它的解耦程度非常一般。

所有基于字符串的解耦方案其實(shí)都可以說(shuō)是偽解耦盐杂,它們只是放棄了編譯依賴(lài)逗载,但是當(dāng)代碼變化之后,即便能夠編譯運(yùn)行链烈,邏輯仍然是錯(cuò)誤的厉斟。

例如修改了模塊定義時(shí)的 URL:

// 注冊(cè)某個(gè)URL

[URLRouter registerURL:@"app://editorView" handler:^(NSDictionary *userInfo) {

    ...

}];

那么調(diào)用者的 URL 也必須修改,代碼仍然是有耦合的强衡,只不過(guò)此時(shí)編譯器無(wú)法檢查而已擦秽。這會(huì)導(dǎo)致維護(hù)更加困難,一旦 URL 中的參數(shù)有了增減,或者決定替換為另一個(gè)模塊感挥,參數(shù)命名有了變化缩搅,幾乎沒(méi)有高效的方式來(lái)重構(gòu)代碼〈ビ祝可以使用宏定義來(lái)管理字符串硼瓣,不過(guò)這要求所有模塊都使用同一個(gè)頭文件,并且也無(wú)法解決參數(shù)類(lèi)型和數(shù)量變化的問(wèn)題置谦。

URL 路由適合用來(lái)做遠(yuǎn)程模塊的網(wǎng)絡(luò)協(xié)議交互堂鲤,而在管理本地模塊時(shí),最大的甚至是唯一的優(yōu)勢(shì)媒峡,就是適合經(jīng)澄疗埽跨多端運(yùn)營(yíng)活動(dòng)的 app,因?yàn)榭梢杂蛇\(yùn)營(yíng)人員統(tǒng)一管理多平臺(tái)的路由規(guī)則谅阿。

代表框架

改進(jìn):避免字符串管理

改進(jìn) URL 路由的方式半哟,就是避免使用字符串,通過(guò)接口管理模塊签餐。

參數(shù)可以通過(guò) protocol 直接傳遞寓涨,能夠利用編譯器檢查參數(shù)類(lèi)型,并且在 ZIKRouter 中贱田,能通過(guò)路由聲明和編譯檢查缅茉,保證所使用的模塊一定存在。在為模塊創(chuàng)建路由時(shí)男摧,也無(wú)需修改模塊的代碼蔬墩。

但是必須要承認(rèn)的是,盡管 URL 路由缺點(diǎn)多多耗拓,但它在跨平臺(tái)路由管理上的確是最適合的方案拇颅。因此 ZIKRouter 也對(duì) URL 路由做出了支持,在用 protocol 管理的同時(shí)乔询,可以通過(guò)字符串匹配 router樟插,也能和其他 URL router 框架對(duì)接。

Target-Action 方案

有一些模塊管理工具基于 Objective-C 的 runtime竿刁、category 特性動(dòng)態(tài)獲取模塊黄锤。例如通過(guò)NSClassFromString獲取類(lèi)并創(chuàng)建實(shí)例,通過(guò)performSelector: NSInvocation動(dòng)態(tài)調(diào)用方法食拜。

例如基于 target-action 模式的設(shè)計(jì)鸵熟,大致是利用 category 為路由工具添加新接口,在接口中通過(guò)字符串獲取對(duì)應(yīng)的類(lèi)负甸,再用 runtime 創(chuàng)建實(shí)例流强,動(dòng)態(tài)調(diào)用實(shí)例的方法痹届。

示例代碼:

// 模塊管理者,提供了動(dòng)態(tài)調(diào)用 target-action 的基本功能

@interface Mediator : NSObject

+ (instancetype)sharedInstance;

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;

@end
// 在 category 中定義新接口

@interface Mediator (ModuleActions)

- (UIViewController *)Mediator_editorViewController;

@end

@implementation Mediator (ModuleActions)

- (UIViewController *)Mediator_editorViewController {

    // 使用字符串硬編碼打月,通過(guò) runtime 動(dòng)態(tài)創(chuàng)建 Target_Editor队腐,并調(diào)用 Action_viewController:

    UIViewController *viewController = [self performTarget:@"Editor" action:@"viewController" params:@{@"key":@"value"}];

    return viewController;

}

@end

// 調(diào)用者通過(guò) Mediator 的接口調(diào)用模塊

UIViewController *editor = [[Mediator sharedInstance] Mediator_editorViewController];
// 模塊提供者提供 target-action 的調(diào)用方式

@interface Target_Editor : NSObject

- (UIViewController *)Action_viewController:(NSDictionary *)params;

@end

@implementation Target_Editor

- (UIViewController *)Action_viewController:(NSDictionary *)params {

    // 參數(shù)通過(guò)字典傳遞,無(wú)法保證類(lèi)型安全

    EditorViewController *viewController = [[EditorViewController alloc] init];

    viewController.valueLabel.text = params[@"key"];

    return viewController;

}

@end

優(yōu)點(diǎn):

  • 利用 category 可以明確聲明接口奏篙,進(jìn)行編譯檢查
  • 實(shí)現(xiàn)方式輕量

缺點(diǎn):

  • 需要在 mediator 和 target 中重新添加每一個(gè)接口柴淘,模塊化時(shí)代碼較為繁瑣
  • 在 category 中仍然引入了字符串硬編碼,內(nèi)部使用字典傳參报破,一定程度上也存在和 URL 路由相同的問(wèn)題
  • 無(wú)法保證所使用的模塊一定存在悠就,target 模塊在修改后千绪,使用者只有在運(yùn)行時(shí)才能發(fā)現(xiàn)錯(cuò)誤
  • 過(guò)于依賴(lài) runtime 特性充易,無(wú)法應(yīng)用到純 Swift 上。在 Swift 中擴(kuò)展 mediator 時(shí)荸型,無(wú)法使用純 Swift 類(lèi)型的參數(shù)
  • 可能會(huì)創(chuàng)建過(guò)多的 target 類(lèi)
  • 使用 runtime 相關(guān)的接口調(diào)用任意類(lèi)的任意方法盹靴,需要注意別被蘋(píng)果的審核誤傷。參考:Are performSelector and respondsToSelector banned by App Store?

字典傳參的問(wèn)題

字典傳參時(shí)無(wú)法保證參數(shù)的數(shù)量和類(lèi)型瑞妇,只能依賴(lài)調(diào)用約定稿静,就和字符串傳參一樣,一旦某一方做出修改辕狰,另一方也必須修改改备。

相比于 URL 路由,target-action 通過(guò) category 的接口把字符串管理的問(wèn)題縮小到了 mediator 內(nèi)部蔓倍,不過(guò)并沒(méi)有完全消除悬钳,而且在其他方面仍然有很多改進(jìn)空間。上面的8個(gè)指標(biāo)中其實(shí)只能滿(mǎn)足第2個(gè)”支持模塊單獨(dú)編譯”偶翅,另外在和接口相關(guān)的第3默勾、5、6點(diǎn)上聚谁,比 URL 路由要有改善母剥。

代表框架

CTMediator

CTMediator源碼分析

通過(guò)分類(lèi)中調(diào)用的performTarget來(lái)到CTMediator中的具體實(shí)現(xiàn),即performTarget:action:params:shouldCacheTarget:形导,主要是通過(guò)傳入的name环疼,找到對(duì)應(yīng)的target 和 action

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    if (targetName == nil || actionName == nil) {
        return nil;
    }
    //在swift中使用時(shí),需要傳入對(duì)應(yīng)項(xiàng)目的target名稱(chēng)朵耕,否則會(huì)找不到視圖控制器
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // generate target 生成target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        //swift中target文件名拼接
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        //OC中target文件名拼接
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    //緩存中查找target
    NSObject *target = [self safeFetchCachedTarget:targetClassString];
    //緩存中沒(méi)有target
    if (target == nil) {
        //通過(guò)字符串獲取對(duì)應(yīng)的類(lèi)
        Class targetClass = NSClassFromString(targetClassString);
        //創(chuàng)建實(shí)例
        target = [[targetClass alloc] init];
    }

    // generate action 生成action方法名稱(chēng)
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    //通過(guò)方法名字符串獲取對(duì)應(yīng)的sel
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 這里是處理無(wú)響應(yīng)請(qǐng)求的地方之一炫隶,這個(gè)demo做得比較簡(jiǎn)單,如果沒(méi)有可以響應(yīng)的target憔披,就直接return了等限。實(shí)際開(kāi)發(fā)過(guò)程中是可以事先給一個(gè)固定的target專(zhuān)門(mén)用于在這個(gè)時(shí)候頂上爸吮,然后處理這種請(qǐng)求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    //是否需要緩存
    if (shouldCacheTarget) {
        [self safeSetCachedTarget:target key:targetClassString];
    }
    //是否響應(yīng)sel
    if ([target respondsToSelector:action]) {
        //動(dòng)態(tài)調(diào)用方法
        return [self safePerformAction:action target:target params:params];
    } else {
        // 這里是處理無(wú)響應(yīng)請(qǐng)求的地方,如果無(wú)響應(yīng)望门,則嘗試調(diào)用對(duì)應(yīng)target的notFound方法統(tǒng)一處理
        SEL action = NSSelectorFromString(@"notFound:");
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 這里也是處理無(wú)響應(yīng)請(qǐng)求的地方形娇,在notFound都沒(méi)有的時(shí)候,這個(gè)demo是直接return了筹误。實(shí)際開(kāi)發(fā)過(guò)程中桐早,可以用前面提到的固定的target頂上的。
            [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            @synchronized (self) {
                [self.cachedTarget removeObjectForKey:targetClassString];
            }
            return nil;
        }
    }
}
  • 進(jìn)入safePerformAction:target:params:實(shí)現(xiàn)厨剪,主要是通過(guò)invocation進(jìn)行參數(shù)傳遞+消息轉(zhuǎn)發(fā)
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
    //獲取方法簽名
    NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
    if(methodSig == nil) {
        return nil;
    }
    //獲取方法簽名中的返回類(lèi)型哄酝,然后根據(jù)返回值完成參數(shù)傳遞
    const char* retType = [methodSig methodReturnType];
    //void類(lèi)型
    if (strcmp(retType, @encode(void)) == 0) {
        ...
    }
    //...省略其他類(lèi)型的判斷
}

改進(jìn):避免字典傳參

Target-Action 方案最大的優(yōu)點(diǎn)就是整個(gè)方案實(shí)現(xiàn)輕量,并且也一定程度上明確了模塊的接口祷膳。只是這些接口都需要通過(guò) Target-Action 封裝一次陶衅,并且每個(gè)模塊都要?jiǎng)?chuàng)建一個(gè) target 類(lèi),既然如此直晨,直接用 protocol 進(jìn)行接口管理會(huì)更加簡(jiǎn)單搀军。

ZIKRouter 避免使用 runtime 獲取和調(diào)用模塊,因此可以適配 OC 和 swift勇皇。同時(shí)罩句,基于 protocol 匹配的方式,避免引入字符串硬編碼敛摘,能夠更好地管理模塊门烂,也避免了字典傳參。

protocol class

protocol匹配的實(shí)現(xiàn)思路是:

  • 1兄淫、將protocol和對(duì)應(yīng)的類(lèi)進(jìn)行字典匹配

  • 2屯远、通過(guò)用protocol獲取class,在動(dòng)態(tài)創(chuàng)建實(shí)例

protocol比較典型的三方框架就是阿里的BeeHive拖叙。BeeHive借鑒了Spring Service氓润、Apache DSO的架構(gòu)理念,采用AOP+擴(kuò)展App生命周期API形式薯鳍,將業(yè)務(wù)功能咖气、基礎(chǔ)功能模塊以模塊方式以解決大型應(yīng)用中的復(fù)雜問(wèn)題,并讓模塊之間以Service形式調(diào)用挖滤,將復(fù)雜問(wèn)題切分崩溪,以AOP方式模塊化服務(wù)。

BeeHive 核心思想

  • 1斩松、各個(gè)模塊間調(diào)用從直接調(diào)用對(duì)應(yīng)模塊伶唯,變成調(diào)用Service的形式,避免了直接依賴(lài)惧盹。

  • 2乳幸、App生命周期的分發(fā)瞪讼,將耦合在AppDelegate中邏輯拆分,每個(gè)模塊以微應(yīng)用的形式獨(dú)立存在粹断。

示例如下:

//******** 1符欠、注冊(cè)
[[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];

//******** 2、使用
#import "BHService.h"

id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];

優(yōu)點(diǎn)

  • 1瓶埋、利用接口調(diào)用希柿,實(shí)現(xiàn)了參數(shù)傳遞時(shí)的類(lèi)型安全

  • 2、直接使用模塊的protocol接口养筒,無(wú)需再重復(fù)封裝

缺點(diǎn)

  • 1曾撤、用框架來(lái)創(chuàng)建所有對(duì)象,創(chuàng)建方式不同晕粪,即不支持外部傳入?yún)?shù)

  • 2挤悉、用OC runtime創(chuàng)建對(duì)象,不支持swift

  • 3兵多、只做了protocolclass 的匹配尖啡,不支持更復(fù)雜的創(chuàng)建方式 和依賴(lài)注入

  • 4、無(wú)法保證所使用的protocol 一定存在對(duì)應(yīng)的模塊剩膘,也無(wú)法直接判斷某個(gè)protocol是否能用于獲取模塊

除了BeeHive,還有Swinject

BeeHive 模塊注冊(cè)

BeeHive主要是通過(guò)BHModuleManager來(lái)管理各個(gè)模塊的盆顾。BHModuleManager中只會(huì)管理已經(jīng)被注冊(cè)過(guò)的模塊怠褐。

BeeHive提供了三種不同的調(diào)用形式,靜態(tài)plist您宪,動(dòng)態(tài)注冊(cè)奈懒,annotation。Module宪巨、Service之間沒(méi)有關(guān)聯(lián)磷杏,每個(gè)業(yè)務(wù)模塊可以單獨(dú)實(shí)現(xiàn)Module或者Service的功能。

1捏卓、 Annotation方式注冊(cè)
這種方式主要是通過(guò)BeeHiveMod宏進(jìn)行Annotation標(biāo)記

//***** 使用
BeeHiveMod(ShopModule)

//***** BeeHiveMod的宏定義
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";

//***** BeeHiveDATA的宏定義 
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))

//*****  全部轉(zhuǎn)換出來(lái)后為下面的格式
char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";

這里針對(duì)__attribute需要說(shuō)明以下幾點(diǎn)

  • 第一個(gè)參數(shù)used:用來(lái)修飾函數(shù)极祸,被used修飾以后,意味著即使函數(shù)沒(méi)有被引用怠晴,在Release下也不會(huì)被優(yōu)化遥金。如果不加這個(gè)修飾,那么Release環(huán)境鏈接器下會(huì)去掉沒(méi)有被引用的段蒜田。

  • 通過(guò)使用__attribute__((section("name")))來(lái)指明哪個(gè)段稿械。數(shù)據(jù)則用__attribute__((used))來(lái)標(biāo)記,防止鏈接器會(huì)優(yōu)化刪除未被使用的段冲粤,然后將模塊注入到__DATA

此時(shí)Module已經(jīng)被存儲(chǔ)到Mach-O文件的特殊段中美莫,那么如何取呢页眯?

  • 進(jìn)入BHReadConfiguration方法,主要是通過(guò)Mach-O找到存儲(chǔ)的數(shù)據(jù)段厢呵,取出放入數(shù)組中
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{

    NSMutableArray *configs = [NSMutableArray array];
    unsigned long size = 0;
#ifndef __LP64__
    // 找到之前存儲(chǔ)的數(shù)據(jù)段(Module找BeehiveMods段 和 Service找BeehiveServices段)的一片內(nèi)存
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
#else
    const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif

    unsigned long counter = size/sizeof(void*);
    // 把特殊段里面的數(shù)據(jù)都轉(zhuǎn)換成字符串存入數(shù)組中
    for(int idx = 0; idx < counter; ++idx){
        char *string = (char*)memory[idx];
        NSString *str = [NSString stringWithUTF8String:string];
        if(!str)continue;

        BHLog(@"config = %@", str);
        if(str) [configs addObject:str];
    }

    return configs; 
}

2餐茵、讀取本地Pilst文件

  • 首先,需要設(shè)置好路徑
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可選述吸,默認(rèn)為BeeHive.bundle/BeeHive.plist

  • 創(chuàng)建plist文件忿族,Plist文件的格式也是數(shù)組中包含多個(gè)字典。字典里面有兩個(gè)Key蝌矛,一個(gè)是@"moduleLevel"道批,另一個(gè)是@"moduleClass"。注意的數(shù)組的名字叫@“moduleClasses”入撒。

    image
  • 進(jìn)入loadLocalModules方法隆豹,主要是從Plist里面取出數(shù)組,然后把數(shù)組加入到BHModuleInfos數(shù)組里面茅逮。

//初始化context時(shí)璃赡,加載Modules和Services
-(void)setContext:(BHContext *)context
{
    _context = context;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self loadStaticServices];
        [self loadStaticModules];
    });
}
??
//加載modules
- (void)loadStaticModules
{
    // 讀取本地plist文件里面的Module,并注冊(cè)到BHModuleManager的BHModuleInfos數(shù)組中
    [[BHModuleManager sharedManager] loadLocalModules];
    //注冊(cè)所有modules献雅,在內(nèi)部根據(jù)優(yōu)先級(jí)進(jìn)行排序
    [[BHModuleManager sharedManager] registedAllModules];

}
??
- (void)loadLocalModules
{
    //plist文件路徑
    NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"];
    //判斷文件是否存在
    if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath]) {
        return;
    }
    //讀取整個(gè)文件[@"moduleClasses" : 數(shù)組]
    NSDictionary *moduleList = [[NSDictionary alloc] initWithContentsOfFile:plistPath];
    //通過(guò)moduleClasses key讀取 數(shù)組 [[@"moduleClass":"aaa", @"moduleLevel": @"bbb"], [...]]
    NSArray<NSDictionary *> *modulesArray = [moduleList objectForKey:kModuleArrayKey];
    NSMutableDictionary<NSString *, NSNumber *> *moduleInfoByClass = @{}.mutableCopy;
    //遍歷數(shù)組
    [self.BHModuleInfos enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [moduleInfoByClass setObject:@1 forKey:[obj objectForKey:kModuleInfoNameKey]];
    }];
    [modulesArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (!moduleInfoByClass[[obj objectForKey:kModuleInfoNameKey]]) {
            //存儲(chǔ)到 BHModuleInfos 中
            [self.BHModuleInfos addObject:obj];
        }
    }];
}

3碉考、load方法注冊(cè)

該方法注冊(cè)Module就是在Load方法里面注冊(cè)Module的類(lèi)

+ (void)load
{
    [BeeHive registerDynamicModule:[self class]];
}

  • 進(jìn)入registerDynamicModule實(shí)現(xiàn)
+ (void)registerDynamicModule:(Class)moduleClass
{
    [[BHModuleManager sharedManager] registerDynamicModule:moduleClass];
}
??
- (void)registerDynamicModule:(Class)moduleClass
{
    [self registerDynamicModule:moduleClass shouldTriggerInitEvent:NO];
}
??
- (void)registerDynamicModule:(Class)moduleClass
       shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent
{
    [self addModuleFromObject:moduleClass shouldTriggerInitEvent:shouldTriggerInitEvent];
}

其底層還是同第一種方式一樣,最終會(huì)走到addModuleFromObject:shouldTriggerInitEvent:方法中

  • load方法挺身,還可以使用BH_EXPORT_MODULE宏代替
#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}

BH_EXPORT_MODULE宏里面可以傳入一個(gè)參數(shù)侯谁,代表是否異步加載Module模塊,如果是YES就是異步加載章钾,如果是NO就是同步加載墙贱。

2、BeeHive 模塊事件

BeeHive會(huì)給每個(gè)模塊提供生命周期事件贱傀,用于與BeeHive宿主環(huán)境進(jìn)行必要信息交互惨撇,感知模塊生命周期的變化。

BeeHive各個(gè)模塊會(huì)收到一些事件府寒。在BHModuleManager中魁衙,所有的事件被定義成了BHModuleEventType枚舉。如下所示椰棘,其中有2個(gè)事件很特殊纺棺,一個(gè)是BHMInitEvent,一個(gè)是BHMTearDownEvent

typedef NS_ENUM(NSInteger, BHModuleEventType)
{
    //設(shè)置Module模塊
    BHMSetupEvent = 0,
    //用于初始化Module模塊邪狞,例如環(huán)境判斷祷蝌,根據(jù)不同環(huán)境進(jìn)行不同初始化
    BHMInitEvent,
    //用于拆除Module模塊
    BHMTearDownEvent,
    BHMSplashEvent,
    BHMQuickActionEvent,
    BHMWillResignActiveEvent,
    BHMDidEnterBackgroundEvent,
    BHMWillEnterForegroundEvent,
    BHMDidBecomeActiveEvent,
    BHMWillTerminateEvent,
    BHMUnmountEvent,
    BHMOpenURLEvent,
    BHMDidReceiveMemoryWarningEvent,
    BHMDidFailToRegisterForRemoteNotificationsEvent,
    BHMDidRegisterForRemoteNotificationsEvent,
    BHMDidReceiveRemoteNotificationEvent,
    BHMDidReceiveLocalNotificationEvent,
    BHMWillPresentNotificationEvent,
    BHMDidReceiveNotificationResponseEvent,
    BHMWillContinueUserActivityEvent,
    BHMContinueUserActivityEvent,
    BHMDidFailToContinueUserActivityEvent,
    BHMDidUpdateUserActivityEvent,
    BHMHandleWatchKitExtensionRequestEvent,
    BHMDidCustomEvent = 1000

};

主要分為三種

  • 1、系統(tǒng)事件:主要是指Application生命周期事件!

一般的做法是AppDelegate改為繼承自BHAppDelegate

@interface TestAppDelegate : BHAppDelegate <UIApplicationDelegate>

  • 2帆卓、應(yīng)用事件:官方給出的流程圖巨朦,其中modSetup米丘、modInit等,可以用于編碼實(shí)現(xiàn)各插件模塊的設(shè)置與初始化糊啡。

  • 3拄查、自定義事件

以上所有的事件都可以通過(guò)調(diào)用BHModuleManagertriggerEvent:來(lái)處理。

- (void)triggerEvent:(NSInteger)eventType
{
    [self triggerEvent:eventType withCustomParam:nil];

}
??
- (void)triggerEvent:(NSInteger)eventType
     withCustomParam:(NSDictionary *)customParam {
    [self handleModuleEvent:eventType forTarget:nil withCustomParam:customParam];
}
??
#pragma mark - module protocol
- (void)handleModuleEvent:(NSInteger)eventType
                forTarget:(id<BHModuleProtocol>)target
          withCustomParam:(NSDictionary *)customParam
{
    switch (eventType) {
            //初始化事件
        case BHMInitEvent:
            //special
            [self handleModulesInitEventForTarget:nil withCustomParam :customParam];
            break;
            //析構(gòu)事件
        case BHMTearDownEvent:
            //special
            [self handleModulesTearDownEventForTarget:nil withCustomParam:customParam];
            break;
            //其他3類(lèi)事件
        default: {
            NSString *selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
            [self handleModuleEvent:eventType forTarget:nil withSeletorStr:selectorStr andCustomParam:customParam];
        }
            break;
    }

}

從上面的代碼中可以發(fā)現(xiàn)棚蓄,除去BHMInitEvent初始化事件和BHMTearDownEvent拆除Module事件這兩個(gè)特殊事件以外堕扶,所有的事件都是調(diào)用的handleModuleEvent:forTarget:withSeletorStr:andCustomParam:方法,其內(nèi)部實(shí)現(xiàn)主要是遍歷 moduleInstances 實(shí)例數(shù)組梭依,調(diào)用performSelector:withObject:方法實(shí)現(xiàn)對(duì)應(yīng)方法調(diào)用

- (void)handleModuleEvent:(NSInteger)eventType
                forTarget:(id<BHModuleProtocol>)target
           withSeletorStr:(NSString *)selectorStr
           andCustomParam:(NSDictionary *)customParam
{
    BHContext *context = [BHContext shareInstance].copy;
    context.customParam = customParam;
    context.customEvent = eventType;
    if (!selectorStr.length) {
        selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
    }
    SEL seletor = NSSelectorFromString(selectorStr);
    if (!seletor) {
        selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
        seletor = NSSelectorFromString(selectorStr);
    }
    NSArray<id<BHModuleProtocol>> *moduleInstances;
    if (target) {
        moduleInstances = @[target];
    } else {
        moduleInstances = [self.BHModulesByEvent objectForKey:@(eventType)];
    }
    //遍歷 moduleInstances 實(shí)例數(shù)組稍算,調(diào)用performSelector:withObject:方法實(shí)現(xiàn)對(duì)應(yīng)方法調(diào)用
    [moduleInstances enumerateObjectsUsingBlock:^(id<BHModuleProtocol> moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([moduleInstance respondsToSelector:seletor]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            //進(jìn)行方法調(diào)用
            [moduleInstance performSelector:seletor withObject:context];
#pragma clang diagnostic pop

            [[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]];

        }
    }];
}

注意:這里所有的Module必須是遵循BHModuleProtocol的,否則無(wú)法接收到這些事件的消息役拴。

3糊探、BeeHive模塊調(diào)用

在BeeHive中是通過(guò)BHServiceManager來(lái)管理各個(gè)Protocol的。BHServiceManager中只會(huì)管理已經(jīng)被注冊(cè)過(guò)的Protocol河闰。

注冊(cè)Protocol的方式總共有三種科平,和注冊(cè)Module是一樣一一對(duì)應(yīng)的

1、Annotation方式注冊(cè)

//****** 1姜性、通過(guò)BeeHiveService宏進(jìn)行Annotation標(biāo)記
BeeHiveService(HomeServiceProtocol,BHViewController)

//****** 2瞪慧、宏定義
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";

//****** 3、轉(zhuǎn)換后的格式污抬,也是將其存儲(chǔ)到特殊的段
char * kHomeServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ \"""HomeServiceProtocol""\" : \"""BHViewController""\"}";

2汞贸、讀取本地plist文件

  • 首先同Module一樣,需要先設(shè)置好路徑
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";

  • 設(shè)置plist文件

  • 同樣也是在setContext時(shí)注冊(cè)services

//加載services
-(void)loadStaticServices
{
    [BHServiceManager sharedManager].enableException = self.enableException;

    [[BHServiceManager sharedManager] registerLocalServices];

}
??
- (void)registerLocalServices
{
    NSString *serviceConfigName = [BHContext shareInstance].serviceConfigName;
    //獲取plist文件路徑
    NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
    if (!plistPath) {
        return;
    }

    NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];

    [self.lock lock];
    //遍歷并存儲(chǔ)到allServicesDict中
    for (NSDictionary *dict in serviceList) {
        NSString *protocolKey = [dict objectForKey:@"service"];
        NSString *protocolImplClass = [dict objectForKey:@"impl"];
        if (protocolKey.length > 0 && protocolImplClass.length > 0) {
            [self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
        }
    }
    [self.lock unlock];
}

3印机、load方法注冊(cè)

在Load方法里面注冊(cè)Protocol協(xié)議,主要是調(diào)用BeeHive里面的registerService:service:完成protocol的注冊(cè)

+ (void)load
{
   [[BeeHive shareInstance] registerService:@protocol(UserTrackServiceProtocol) service:[BHUserTrackViewController class]];
}
??
- (void)registerService:(Protocol *)proto service:(Class) serviceClass
{
    [[BHServiceManager sharedManager] registerService:proto implClass:serviceClass];
}

到此门驾,三種方式就創(chuàng)建完成了

Protocol的獲取

ProtocolModule的區(qū)別在于射赛,ProtocolModule多了一個(gè)方法,可以返回Protocol實(shí)例對(duì)象

- (id)createService:(Protocol *)proto;
{
    return [[BHServiceManager sharedManager] createService:proto];
}
??
- (id)createService:(Protocol *)service
{
    return [self createService:service withServiceName:nil];
}
??
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName {
    return [self createService:service withServiceName:serviceName shouldCache:YES];
}
??
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache {
    if (!serviceName.length) {
        serviceName = NSStringFromProtocol(service);
    }
    id implInstance = nil;
    //判斷protocol是否已經(jīng)注冊(cè)過(guò)
    if (![self checkValidService:service]) {
        if (self.enableException) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol does not been registed", NSStringFromProtocol(service)] userInfo:nil];
        }

    }

    NSString *serviceStr = serviceName;
    //如果有緩存奶是,則直接從緩存中獲取
    if (shouldCache) {
        id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
        if (protocolImpl) {
            return protocolImpl;
        }
    }
    //獲取類(lèi)后楣责,然后響應(yīng)下層的方法
    Class implClass = [self serviceImplClass:service];
    if ([[implClass class] respondsToSelector:@selector(singleton)]) {
        if ([[implClass class] singleton]) {
            if ([[implClass class] respondsToSelector:@selector(shareInstance)])
                //創(chuàng)建單例對(duì)象
                implInstance = [[implClass class] shareInstance];
            else
                //創(chuàng)建實(shí)例對(duì)象
                implInstance = [[implClass alloc] init];
            if (shouldCache) {
                //緩存
                [[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr];
                return implInstance;
            } else {
                return implInstance;
            }
        }
    }
    return [[implClass alloc] init];
}

createService會(huì)先檢查Protocol協(xié)議是否是注冊(cè)過(guò)的。然后接著取出字典里面對(duì)應(yīng)的Class聂沙,如果實(shí)現(xiàn)了shareInstance方法秆麸,那么就創(chuàng)建一個(gè)單例對(duì)象,如果沒(méi)有及汉,那么就創(chuàng)建一個(gè)實(shí)例對(duì)象沮趣。如果還實(shí)現(xiàn)了singleton,就能進(jìn)一步的把implInstanceserviceStr對(duì)應(yīng)的加到BHContextservicesByName字典里面緩存起來(lái)坷随。這樣就可以隨著上下文傳遞了

  • 進(jìn)入serviceImplClass實(shí)現(xiàn)弧腥,從這里可以看出 protocol和類(lèi)是通過(guò)字典綁定的屡久,protocol作為key徽千,serviceImp(類(lèi)的名字)作為value
- (Class)serviceImplClass:(Protocol *)service
{
    //通過(guò)字典將 協(xié)議 和 類(lèi) 綁定,其中協(xié)議作為key翁狐,serviceImp(類(lèi)的名字)作為value
    NSString *serviceImpl = [[self servicesDict] objectForKey:NSStringFromProtocol(service)];
    if (serviceImpl.length > 0) {
        return NSClassFromString(serviceImpl);
    }
    return nil;
}

Module & Protocol

這里簡(jiǎn)單總結(jié)下:

  • 對(duì)于Module:數(shù)組存儲(chǔ)

  • 對(duì)于Protocol:通過(guò)字典將protocol與類(lèi)進(jìn)行綁定,keyprotocol凌蔬,valueserviceImp即類(lèi)名

輔助類(lèi)

  • BHConfig類(lèi):是一個(gè)單例露懒,其內(nèi)部有一個(gè)NSMutableDictionary類(lèi)型的config屬性,該屬性維護(hù)了一些動(dòng)態(tài)的環(huán)境變量砂心,作為BHContext的補(bǔ)充存在

  • BHContext類(lèi):是一個(gè)單例懈词,其內(nèi)部有兩個(gè)NSMutableDictionary的屬性,分別是modulesByNameservicesByName计贰。這個(gè)類(lèi)主要用來(lái)保存上下文信息的钦睡。例如在application:didFinishLaunchingWithOptions:的時(shí)候,就可以初始化大量的上下文信息

//保存信息
[BHContext shareInstance].application = application;
[BHContext shareInstance].launchOptions = launchOptions;
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可選躁倒,默認(rèn)為BeeHive.bundle/BeeHive.plist
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";

  • BHTimeProfiler類(lèi):用來(lái)進(jìn)行計(jì)算時(shí)間性能方面的Profiler

  • BHWatchDog類(lèi):用來(lái)開(kāi)一個(gè)線(xiàn)程荞怒,監(jiān)聽(tīng)主線(xiàn)程是否堵塞

參考鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末秧秉,一起剝皮案震驚了整個(gè)濱河市褐桌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌象迎,老刑警劉巖荧嵌,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異砾淌,居然都是意外死亡啦撮,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)汪厨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)赃春,“玉大人,你說(shuō)我怎么就攤上這事劫乱≈校” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵衷戈,是天一觀的道長(zhǎng)狭吼。 經(jīng)常有香客問(wèn)我,道長(zhǎng)殖妇,這世上最難降的妖魔是什么刁笙? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上采盒,老公的妹妹穿的比我還像新娘旧乞。我一直安慰自己,他們只是感情好磅氨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布尺栖。 她就那樣靜靜地躺著,像睡著了一般烦租。 火紅的嫁衣襯著肌膚如雪延赌。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天叉橱,我揣著相機(jī)與錄音挫以,去河邊找鬼。 笑死窃祝,一個(gè)胖子當(dāng)著我的面吹牛掐松,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播粪小,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼大磺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了探膊?” 一聲冷哼從身側(cè)響起杠愧,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎逞壁,沒(méi)想到半個(gè)月后流济,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡腌闯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年绳瘟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姿骏。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡稽荧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出工腋,到底是詐尸還是另有隱情,我是刑警寧澤畅卓,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布擅腰,位于F島的核電站,受9級(jí)特大地震影響翁潘,放射性物質(zhì)發(fā)生泄漏趁冈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望渗勘。 院中可真熱鬧沐绒,春花似錦、人聲如沸旺坠。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)取刃。三九已至蹋肮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間璧疗,已是汗流浹背坯辩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留崩侠,地道東北人漆魔。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像却音,于是被迫代替她去往敵國(guó)和親改抡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

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