美團貓眼電影android模塊化實戰(zhàn)--可能是最詳細的模塊化實戰(zhàn)

轉(zhuǎn)載請注明出處:
美團貓眼電影android模塊化實戰(zhàn)--可能是最詳細的模塊化實戰(zhàn)
地址:http://www.reibang.com/p/d372cc6802e5

目錄

1 寫這篇博客的初衷

首先一句話概括:我想把這幾個月做的事情記錄下來,并且希望盡量詳細涤久,希望讀者讀了這篇文章能夠知道項目進行模塊化箍铲,項目改業(yè)務框架可能會遇到哪些問題,具體每個步驟都做什么户辱,而不是大致的了解烫幕。

現(xiàn)在很多人都在談模塊化济炎,網(wǎng)上有一大堆的博客實踐都在講這個。很多談的只是模塊與模塊之間的解耦凯亮,并且大部分講的是通過router路由進行解耦边臼,其他談的不多,而且不乏泛泛而談触幼。但將一個app真正做到解耦硼瓣,運行。需要解決的事情遠遠不止解耦置谦。業(yè)務架構(gòu)堂鲤、進程間通信、資源等處理媒峡、解耦方式等都需要解決瘟栖。恰好對于貓眼模塊化整個過程的實施,從頭到尾谅阿,分析解決各種問題半哟,我陸陸續(xù)續(xù)的做了幾個月酬滤。貓眼app的歷史版本是一個耦合度很高的一個工程。從這樣的一個歷史版本到最終的各個業(yè)務模塊能夠獨立運行并且能夠做進程間通信寓涨,會涉及到各個方面的解耦和一些其他東西盯串。我今天我就以該app為例(其他的app進行解耦可能會遇到不同的問題,這點注意一下)戒良,完整的講下貓眼模塊化的整個過程体捏。每一個方面沒有照搬網(wǎng)絡(luò)的一些做法,而是分析對比糯崎,采用更好的設(shè)計方式几缭。比如解耦使用serviceloader,而不是路由進行沃呢;比如架構(gòu)使用更適合我們業(yè)務的一種帶生命周期的mvp變種年栓。我還會說下具體的花費時間和一些經(jīng)驗,這樣大家以后做模塊時也心中有數(shù)薄霜。(提示一下某抓,其實模塊化過程所涉及的東西除了文章提及的還有很多。有些未提及黄锤,是因為之前已經(jīng)完成搪缨,比如網(wǎng)絡(luò)庫的緩存由數(shù)據(jù)庫->文本食拜,這點讀者注意一下鸵熟。如果還有遺漏的地方,可以交流~)负甸。

主要內(nèi)容:serviceloader解耦流强,mvp變種框架,模塊通信呻待,lib獨立運行打月,多端復用。

2 為什么做模塊化

首先要說一點:做模塊化不是為了炫技蚕捉。如果沒有業(yè)務場景需求奏篙,不建議做。
為什么要做模塊化迫淹,網(wǎng)上已經(jīng)闡述了很多原因了秘通。這里我簡單說下貓眼為什么要做:

  • 貓眼需要快速移植到其他app(美團,點評..)敛熬。
  • 解耦首頁肺稀,減少冷啟動時間。
  • 開發(fā)時減少build時間应民,代碼責任制话原。
  • 服務快捷替換

3 解耦到什么程度夕吻?

首先說下,模塊化究竟是什么呢繁仁?這個大家肯定都耳熟能詳了:能夠?qū)⒉煌臉I(yè)務分離成不同的lib module涉馅。那么做完模塊化,我們的某個業(yè)務lib 具有哪些功能呢黄虱?我認為是:

總結(jié)一句話:無溝通成本控漠,快速,傻瓜式的在任何app上運行悬钳。具體就是:這個lib不耦合具體app的服務盐捷,不耦合具體app的activity。只要給我一個app(或假的app殼子)默勾,通過它的baseActivity碉渡,和他們的服務,我就可以非衬赴快速的將這個lib在那個app上運行滞诺。停!你可能會說這個服務是什么東西环疼,讓我詳細的說下吧~

3.1 可以無侵入式的配置各種服務

我們知道每個app都會提供賬戶信息习霹,設(shè)備信息,網(wǎng)絡(luò)服務炫隶,圖片加載服務淋叶,打點服務,下拉刷新樣式伪阶,錯誤狀態(tài)等煞檩。每一個app的這些服務可能都不一樣,比如美團使用的網(wǎng)絡(luò)服務是okhttp栅贴,而點評使用的是長連接斟湃。所以我們的業(yè)務邏輯lib不能耦合這些具體的服務。只能耦合服務抽象而來的接口檐薯。在具體app使用的時候凝赛,我們再把app的服務提供給這個lib。那么這些服務怎么給呢坛缕?如果當需要服務時墓猎,我都留了一個傳參的口子,這樣我就需要把app的服務一個個塞到lib中需要的地方祷膳。這樣成本太大了陶衅。我不希望這么麻煩,我希望的方式是直接把服務實現(xiàn)作為txt文本放在app的某個文件夾直晨,你這個lib就能給我運行搀军。這樣我?guī)缀醪挥霉躭ib里面是什么東西膨俐。你只要給我一個業(yè)務lib,我添加一個txt文本罩句,就能運行了焚刺。

3.2 lib快速便捷多端使用

說下不耦合activity。我們知道每個app有自己的baseAtivity门烂,在里面做統(tǒng)計乳愉,處理異常、某些庫的初始化等功能屯远。除此之外蔓姚,每個app的actionBar也不一樣,每個頁面在不用的app中manifest的schema也不一樣慨丐。所以在lib中的業(yè)務坡脐,如果是一個業(yè)務,我們不能直接寫成Activity房揭,而應該是一個view/fragment备闲,這樣對于任何一個app,我們直接新建一個activty捅暴,然后把lib中的頁面放到那個activity中即可恬砂。同樣,考慮的是協(xié)同合作的成本問題蓬痒,我不希望在放這個頁面的時候泻骤,我需要處理很多其他的東西,比如數(shù)據(jù)加載乳幸。我希望你給我這個業(yè)務頁面pager(其實是一個view)瞪讼,我放到activity onCreate()的setContentView()中即可,它就能運行粹断。別讓我做其他處理生命周期,數(shù)據(jù)綁定嫡霞,銷毀等的事瓶埋,那都是你pager內(nèi)部需要做的。

3.3 demo示例

前面這兩點說的可能云里霧里的诊沪。最近我寫一個貓眼問答需求养筒,涉及5個頁面,所以做成了一個lib端姚。那么就結(jié)合我最近寫的lib來圖文闡述一下晕粪。

這個lib就是問答的業(yè)務lib。這不耦合具體的服務渐裸,只耦合服務接口巫湘。里面的頁面(page包下)不是activity装悲,而是view。
那么尚氛,這時候另外一個同事想把這個lib用到貓眼app上诀诊。怎么做呢?

  • 在貓眼app的build.gradle下添加這個lib的依賴:

     compile 'com.maoyan.android.business:movie:1.0.2.3'
    
  • 在貓眼宿主app中添加一個lib需要的服務配置:服務實現(xiàn)txt文本(因為是宿主app阅嘶,之前其實已經(jīng)存在)属瓣。


    txt里面是:

  • 在宿主app中創(chuàng)建activity,并放置lib中的頁面讯柔,填寫manifest抡蛙,比如(可能有時候需要在里面寫入actionBar的交互邏輯)

這就完成了,就運行了魂迄。所使用的各種服務溜畅,下拉刷新等都是這個app提供的。是不是快速极祸、無需溝通寫作慈格、傻瓜。
如果我們想測試這個app遥金,那么也很簡單浴捆。隨便建一個app殼子,新建activity稿械,把lib中的頁面page放進去选泻。然后添加所需要的服務實現(xiàn)txt文本(因為是測試,所以服務實現(xiàn)可以自由一些美莫,可隨意配置)页眯,
就大功告成了。這種方式來修bug調(diào)ui厢呵,比啟動宿主app修改代碼節(jié)省很多時間窝撵。
我們看下我隨意寫了一個app來測試lib:

我們可以看到下拉刷新,狀態(tài)服務等和貓眼app中的都不一樣襟铭,都可以定制碌奉。如果都這么寫,其實所有的模塊我們都可以快速寒砖,傻瓜赐劣,可定制的做成app這種解耦程度是不是更好呢

如果感覺還不錯的話,那么我們開始工作吧~


4 開始模塊化之旅

4.1 原項目耦合結(jié)構(gòu)

開始模塊化工作哩都,我首先得給大家呈現(xiàn)下之前未模塊時高度耦合的貓眼app魁兼。我們這里以電影詳情頁為例,看看他的耦合情況:

電影詳情頁是建立在一層層的基類之上漠嵌,這些基類耦合了具體的網(wǎng)絡(luò)加載等各種服務咐汞。因為詳情頁有想看盖呼、評分、點贊等可編輯狀態(tài)碉考,所以還耦合了greendao數(shù)據(jù)庫(以前網(wǎng)絡(luò)加載也耦合了這個數(shù)據(jù)庫塌计,后來換成了retrofit+rxjava,所以替換到了這層耦合侯谁,謝天謝地)锌仅。該頁面因為需要和其他頁面互動(比如跳轉(zhuǎn)、評分同步等)墙贱,所以也同時耦合了其他頁面的類热芹。除此之外,還有utils惨撇,view伊脓,model等。如果想把電影詳情頁抽離出來魁衙,這些所有的耦合都要剝離报腔。具體需要解決的問題,如下:


4.2 準備工作

4.2.1 工作量評估

首先我們說下解耦時需要做的準備工作剖淀。因為這些工作是解耦拆分的基礎(chǔ)纯蛾。有兩點需要做,如下所示:

首先說明一下纵隔,并不是我喜歡打五顆星翻诉。確實是這部分工作量比較大~~~

4.2.2 公共資源,model捌刮,utils等的拆分

4.2.2.1 耦合情況示例

第一點是公共資源碰煌,model,utils等的拆分绅作。這些事情雖然不用考慮太多事情芦圾,但是很繁瑣。在做模塊化的時候棚蓄,這個地方耗費了不少時間堕扶。很大一部分原因是,之前的貓眼歷史版本代碼不夠規(guī)范梭依,對代碼耦合這些事情不夠敏感。舉幾個例子吧:

  • 我們之前的utils基本都寫在一個類MovieUtils里面了典尾。這個類就像大染缸役拴。什么都向里面放。在傳入的參數(shù)方面也不夠規(guī)范钾埂,甚至MaoyanBaseFragment這種業(yè)務代碼都作為參數(shù)傳入河闰。導致這個東西及其難拆科平。
  • utils的方法不傳context。前人寫的時候圖省事姜性,在項目中統(tǒng)一加了一個靜態(tài)的context瞪慧,導致幾乎所有的utils都沒有傳入context,這樣的后果是這些工具方法直接以來宿主app部念。
  • 之前寫的common view 不夠獨立弃酌。既然想寫common view,那么就盡量讓這個view能夠獨立儡炼,不要耦合其他第三方庫妓湘,盡量使用android 官方庫。
4.2.2.2 資源拆分經(jīng)驗

對于資源的拆分乌询,其實是非常繁瑣的榜贴。尤其是如果string宦棺, color对粪,dimens這些資源分布在了代碼的各個角落,一個個去拆给涕,非常繁瑣鬼佣。其實大可不必這么做驶拱。因為android在build時,會進行資源的merge和shrink沮趣。res/values下的各個文件(styles.xml需注意)最后都只會把用到的放到intermediate/res/merged/../valus.xml屯烦,無用的都會自動刪除。并且最后我們可以使用lint來自動刪除房铭。所以這個地方不要耗費太多的時間驻龟。剛才說了,styles.xml需注意缸匪。那么需要注意什么呢翁狐?這個東西是這么寫的:

我們在寫屬性名字的時候,一定要加上前綴限定詞凌蔬。如果不加的話露懒,你這個lib打包成aar后給其他app使用的時候,會出現(xiàn)屬性名名字重復的沖突砂心,為什么呢懈词?因為BezelImageView這個名字根本不會出現(xiàn)在intermediate/res/merged/../valus.xml里, 所以不要以為這是屬性的限定詞辩诞!

4.2.3 集成式vs組合式(選做)

前面說了資源utils等的拆分坎弯,那么接下來說下第二點,基類的處理。我們看到電影詳情頁是建立在一堆的基類之上抠忘。每一層的基類都做了一些事情撩炊。(當時這么寫是為了頁面的快速開發(fā))如果我們想將電影詳情頁獨立出來,就需要把這些基類打包成一個aar崎脉,下沉到基礎(chǔ)庫拧咳,供所有頁面使用。但是我們以前的這種基類耦合了很多貓眼的東西囚灼,像下拉刷新骆膝,頁面狀態(tài)什么的都是寫死的,并且如果我需要寫個頁面啦撮,我就需要繼承那么一大堆的fragment谭网。當然這種改一改也可以移植。但對以后的代碼迭代肯定是不好的(修改赃春,添加業(yè)務)愉择。因為它靈活性差。比如如果點評app上需要貓眼某個頁面的一部分而不是整個頁面织中,原來那種改起來就不是很方便锥涕。我希望的方式是這些頁面都是view,而不是fragment狭吼。并且也不是這種繼承方式层坠,而是組合方式。即如果我想要一個帶下拉刷新的列表view刁笙,那么我直接build出來這么一個view破花,需要什么配置就set進來,它就能夠使用疲吸。這個view你可以放到任何一個view座每,fragment中和其他view進行組合。即:

這個MovieRcPagePullToRefreshStatusBlock是一個view摘悴,可以用在任何頁面進行view的組合峭梳。

4.2.3.1 組件的插拔式,組合式設(shè)計

其實我的做法更大膽蹂喻,或者更“懶”一些葱椭。我希望我這個MovieRcPagePullToRefreshStatusBlock build成功以后,放到頁面中就能顯示運行口四,自動加載數(shù)據(jù)了孵运。就像小時候玩積木那樣,組件與組件都是插拔即用式的蔓彩。至于這個block是怎么加載數(shù)據(jù)的掐松,使用者無需關(guān)系踱侣。使用者只需要拿到這個block粪小,然后build時set進去需要的東西大磺。放到頁面中就可以運行了√讲玻可以參考這個作為示例:

我們可以看到這個頁面杠愧,我只是build出來了兩個view,然后放到這個page中逞壁,并沒有關(guān)心數(shù)據(jù)加載什么的流济,數(shù)據(jù)加載是在這個block內(nèi)部完成的。然后這個page就像前面說的那樣腌闯,放到某個app的activty中就可以運行了绳瘟。插拔式、傻瓜式的思想姿骏,可能我這個人比較“懶”~~

那這種架構(gòu)怎么實現(xiàn)呢糖声?,接下來粗略的看看這種框架大體的實現(xiàn)思路吧(具體的可以看下我寫的這一篇android 官方mvp框架優(yōu)化:lifecycle-mvp分瘦,像前端那樣組合式寫頁面)蘸泻。其實這個框架大體也是mvp框架的思想,不過同時解決了業(yè)務場景的一些問題嘲玫,比如悦施,生命周期,移植性去团,溝通成本抡诞,使用方便與否等。既然要說下實現(xiàn)思路土陪,那么從開始說起昼汗,對自己是個總結(jié),對讀者們可能有有些許幫助旺坠。先說下mvp框架的含義:

4.2.3.2 mvp框架含義

mvp框架總體來說適用于android的場景需求乔遮。m代表model,提供數(shù)據(jù)取刃;v即view蹋肮,提供的是供presenter調(diào)用的view相關(guān)的方法;p 即presenter璧疗,提供的是頁面里觸發(fā)動作的邏輯方法坯辩。

4.2.3.3 官方mvp框架的缺點

mvp框架網(wǎng)上有很多,官方也推薦了mvp框架崩侠。和一般的區(qū)別是:用contract來承載view和presenter的接口定義漆魔。用fragment來實現(xiàn)view接口。不過官方使用fragment來實現(xiàn)view,也有它的無奈改抡。為什么說它無奈呢矢炼?對于view層的接口,使用fragment來進行實現(xiàn)阿纤,主要是因為fragment有生命周期句灌。但fragment太笨重了。試想一下欠拾,我有一個頁面胰锌,里面有四五塊內(nèi)容。為了以后的各塊內(nèi)容的移動藐窄、去除资昧、移植更方面,我希望每一塊內(nèi)容都做成mvp形式荆忍,塊與塊之間不耦合格带。那么官方的這個mvp框架就不適用了。因為你不可能在一個頁面寫5個fragment把东揣。android的activity中不建議寫那么多的fragment践惑,fragment典型的使用場景是ViewPager。

4.2.3.4 常規(guī)變通

那么變通一下嘶卧,5塊內(nèi)容的view層尔觉,不再用fragment實現(xiàn),而只是一個個普通的view芥吟,每個view監(jiān)聽事件的響應還是在view中進行(調(diào)用各自的presenter方法)侦铜。而對于整個頁面的初始化加載或者下拉刷新加載,這5塊內(nèi)容共用一個fragment钟鸵,在這個fragment的onStart()和下拉刷新的監(jiān)聽回調(diào)中加載5塊內(nèi)容對應的presenter的方法钉稍。然后在fragment的onCreateView()中把5塊內(nèi)容的view填充進來。5塊內(nèi)容之間可能還需要通信棺耍,數(shù)據(jù)交流贡未,這些借助presenter在fragment中進行。

4.2.3.5 帶生命周期的mvp:lifecycle-mvp

上面那么做完全沒有問題蒙袍,并且上面那種做法也存在于我們的項目中俊卤。但通過幾個版本的迭代,我發(fā)現(xiàn)了一些問題:presenter太亂害幅,太散消恍。fragment需要持有所有的presenter,在onStart()時load()數(shù)據(jù)以现。各自的view也需要持有各自的presenter狠怨。并且view和presenter之間需要互相set()约啊。你還需要在activty或者fragment的onDetroy()方法中管理presenter∮独担總體讓人覺得很亂恰矩。尤其是如果你的組件需要被別人使用,或組件用需要用到其他app時茵汰,其他人拿到你的組件枢里,你要關(guān)心兩個東西view和presenter,他得知道這兩個東西里面的方法蹂午,并且他需要在activty/fragment的生命周期中關(guān)聯(lián)他們并調(diào)用一些方法。嗯彬碱。這個過程肯定存在的大量溝通成本~
所以才想到了前面講的那種build方式來實例化組件豆胸,然后用pager組合組件。特點是(具體可以看下android 官方mvp框架優(yōu)化:lifecycle-mvp巷疼,像前端那樣組合式寫頁面):

  • 使用lifecycle-component這個組件提供生命周期晚胡。
  • presenter被view層內(nèi)部持有,不向外暴露嚼沿。
  • build創(chuàng)建view實例時估盘,提供TypeFactory,用于業(yè)務的擴展骡尽。
  • 業(yè)務代碼分層遣妥。
    用這種mvp的變種框架改寫項目的原代/寫新業(yè)務,就可以使頁面更容易移植攀细、拓展箫踩,頁面內(nèi)的模塊也可以移動改變。當然谭贪,這種框架是建立在我們的業(yè)務基礎(chǔ)之上境钟,框架還是需要因項目而已,沒有最好俭识,只有更適合~

4.3 接口的抽離

前面已經(jīng)闡述了模塊化的準備工作慨削,接下來我們需要做什么呢?根據(jù)前面介紹的原項目耦合結(jié)構(gòu)套媚,我們知道我們以前的項目直接依賴了各種service的具體實現(xiàn)缚态。我們接下來要做的是把這些具體service實現(xiàn)用接口來剝離:

4.3.1 使用servieloader進行解耦---非顯式的調(diào)用服務實現(xiàn)類

4.3.1.1 官方serviceloader

從圖上可以看到,我們的實現(xiàn)類都被對應的接口所代替凑阶。但就這一步本身來說猿规,并沒有太大的難度:找到以前服務調(diào)用的地方,然后換成接口調(diào)用宙橱。無非就是有些服務用的比較多姨俩,換起來繁瑣一些蘸拔。但我們現(xiàn)在需要考慮一個問題:服務的實現(xiàn),我們怎么給环葵?首先想到的是调窍,我們留一個參數(shù)來傳入。但這種方式會導致將來使用lib的時候张遭,溝通成本太大:你需要告訴別人哪里哪里我需要傳入什么類型的參數(shù)邓萨。不然你這個lib就沒法運行。我不希望別人在使用你lib的時候菊卷,還需要去內(nèi)部查下你的代碼是什么缔恳,應該怎么傳參數(shù)。我希望 別人在使用的時候洁闰,對他們來說歉甚,lib是盡量透明的。不需要知道lib內(nèi)部寫的是什么扑眉,只需要在外部配置一個txt的文本就可以運行l(wèi)ib纸泄!那應該怎么做呢?
其實java很早就提供了這種類似的功能:在資源目錄META-INF/services中放置提供者配置文件腰素,然后在app運行時聘裁,遇到Serviceloader.load(XxxInterface.class)時,會到META-INF/services的配置文件中尋找這個接口對應的實現(xiàn)類全路徑名弓千,然后使用反射去生成一個無參的實例衡便。
我們大體的使用方式也是基于java提供的這種功能:

4.3.1.2 對官方serviceloader改造
4.3.1.2.1 官方serviceloader缺陷

從前面的闡述來看,java官方提供的serviceloader至少有三個地方需要改進计呈。

  • serviceloader沒有緩存功能砰诵。因為對于服務來說,大部分我們都需要使用單例模式捌显,而不會頻繁的生成新的實例茁彭。
  • serviceloader使用無參的構(gòu)造方法進行構(gòu)建實例。這點不用多說扶歪,肯定需要改進理肺。誰的服務構(gòu)建的時候不需要傳入?yún)?shù)呢?
  • serviceloader沒有預檢查等問題善镰。因為在運行時妹萨,需要在配置文件中去尋找接口對應的實現(xiàn)類名。那么肯定會遇到接口名寫錯了炫欺,類名寫錯了乎完,配置方式寫錯了,找不到接口實現(xiàn)類等品洛,這些錯誤在編譯器是發(fā)現(xiàn)不了的树姨。同時摩桶,使用serviceloader是一種非顯式的調(diào)用服務實現(xiàn)類方式,如果不在proguard中保護這些實現(xiàn)類帽揪,那么肯定會被shrink掉硝清。除了proguard問題外,配置文件寫在資源目錄META-INF/services下對于一些手機(三星)也有兼容問題转晰。最后芦拿,考慮servic配置文件手動注冊的缺點,serviceloader需要提供自動注冊功能查邢。

對于上面三種情況的處理蔗崎,第一點很容易解決。提供一個緩存就可以了侠坎,不多說蚁趁。

4.3.1.2.2 serviceloader構(gòu)造實例

第二點我們是這么解決的:我們讓所有使用serviceloader加載服務的接口都實現(xiàn)Iprovider接口。Iprovider接口提供了一個init(Context context)方法实胸。這樣所有的服務實現(xiàn)類都需要實現(xiàn)init(Context)方法,在里面做原構(gòu)造方法里需要做的初始化邏輯番官。因此庐完,我們在調(diào)用serviceloader加載服務的時候就類似這樣:

          ImageLoader  imageLoader = MovieServiceLoader.getService(context, ImageLoader.class);

在MovieServiceLoader內(nèi)部,生成的實例會調(diào)用一下init(Context)方法徘熔。這樣我們就解決了第二個問題门躯。這里可能也會有一些朋友有些疑問(比如和美團平臺的童鞋就此事討論過):為什么只傳入context參數(shù)。如果一個服務實現(xiàn)類還需要其他參數(shù)怎么辦酷师?就我們的服務和而言讶凉,我認為只需要傳入context,基本上通過context能夠獲得android絕大部分的參數(shù)山孔。并且對于服務來說懂讯,既然它是一種服務,按理說不會依賴你項目一個具體的一個組件台颠。所以我認為傳入context就夠了褐望,而不是傳入不定格式的object參數(shù):

    MovieServiceLoader.getService(Object... params, ImageLoader.class);

這種方式固然能夠解決所有問題。但是這種設(shè)計的思想已經(jīng)違背了接口和實現(xiàn)的隔離概念串前。比如說瘫里,我想使用圖片加載服務,按理說我只需要調(diào)用一下

    imageLoader = MovieServiceLoader.getService(context, ImageLoader.class);

就ok了荡碾,你這個具體的服務是Picasso還是glide別讓我知道谨读,我也不想知道。如果使用第二種方案坛吁,我難道還要知道你這個具體服務需要哪些參數(shù)劳殖,然后傳入嗎铐尚?這感覺太不友好了。使用Iprovider還有一個好處闷尿,那就是我們只需要在MovieServiceLoader倉庫的proguard中添加:

就可以了塑径。其他的地方在使用或新建服務接口時,都不用再考慮proguard問題了填具。

4.3.1.2.3 serviceloader預處理---gradle插件

第三個需要解決的問題是serviceloader的預檢查等统舀。這個解決方法就是寫一個gradle插件。插件的大體流程是

  • 我們在build的某個階段拿到所有編譯后的class文件(夾)和jar包劳景。
  • 使用javassit確定哪些類被@autoService修飾誉简,配置文件中如果不存在,在其添加盟广。
  • 查看serviceConfig配置文件里面的格式是不是正確闷串。
  • 通過javassit來確定serviceConfig配置文件里面的類是不是在項目中存在,接口類是不是實現(xiàn)了Iprovider接口筋量。
4.3.1.2.4 需要用到的知識:build流程烹吵,javassit,groovy

本來這里不想說太多東西桨武,但是考慮到這三樣很多讀者可能不熟悉肋拔。直接去網(wǎng)上google這三方面的東西,單就這些東西呀酸,可能還需要學上一學凉蜂。那么我還是把我的一些經(jīng)驗寫上(為了切題,就不詳細展開了)性誉,讀者可以參考參考窿吩,些許能夠事半功倍。
因為你需要拿到編譯后的class文件和jar包错览,你需要知道build的大體流程纫雁,各個task的輸入輸出是什么,是以文件夾的形式還是jar包的形式蝗砾。
比如說拿所有class的時機先较,可以在assembleXxx這個task時(dex task已經(jīng)完成了),從dex task的輸入文件夾/jar包中拿到所有的class悼粮。同理javac task的輸入也可以闲勺。但javac task的輸出就不可以,因為javac task輸出的intermediate/class文件夾只包含項目中的class文件扣猫,不含有aar對應的intermediate/exploded-aar文件里面的class文件菜循。當然transform也是一種實現(xiàn)方式。transform的輸入申尤,輸出文件路徑已經(jīng)給好了癌幕,輸入的class為所有的class衙耕。
除build流程之外,你還可能使用groovy來寫插件邏輯勺远。不過如果你實在不想用groovy橙喘,那么也可以用java,兩者兼容胶逢,只是groovy的很多特性像循環(huán)等就沒法用了。這里有個小經(jīng)驗:寫goovy初坠,ide不能很好提示錯誤和簸,比如你使用了一個變量或一個方法,如果方法用錯了碟刺,變量沒定義锁保。也不會給你提示找不到。所以最好還是使用先寫到.java里面半沽,然后再移動到.groovy里面吧爽柒。
最后你還需要知道javassit的一些知識,這個是處理class文件的工具者填。很強大霉赡,和java很像,大部分的使用都會落腳到ctClass的使用幔托。所以這個類最好熟悉。這里有個小經(jīng)驗:有時候需要ctClass->class的轉(zhuǎn)化蜂挪,記得使用靜態(tài)變量儲存這個class對象重挑,不然會報 classloader多次加載同一個路徑的異常。
ok棠涮,使用serviceloader來進行解耦的原理谬哀,改進,好處已經(jīng)說完了严肪。

4.3.2 serviceloader解耦 vs 路由方式解耦

網(wǎng)上有關(guān)模塊化的博客史煎,大部分使用的是路由的方式解耦。路由的方式解耦是怎么一回事呢驳糯?

4.3.2.1 路由方式解耦闡述

我們看下大體的路由框架圖 ,截取于
Android組件化之通信

那么這個路由框架是怎么工作的呢篇梭?這里的action是一個服務,provider是一個map集合酝枢,盛放一個lib里的所有(action名字:action實例)鍵值對恬偷。在宿主app中注冊各個lib的provider。這樣module A請求moduleB的服務時帘睦,通過(代碼來源):

即通過提供provider的名字袍患,action的名字坦康,參數(shù)名,值诡延,到注冊的map中尋找對應的action實例滞欠,然后調(diào)用其對應的方法。核心就是使用字符串來匹配對應的實例進行解耦肆良。

4.3.2.1.1 路由方式解耦優(yōu)點

這種方式的最大好處是筛璧,新建一個服務時,不需要寫接口妖滔,所有的都用字符串來進行標志隧哮,進行匹配,兩個model之間不需要耦合任何東西座舍,甚至接口聲明都不需要耦合沮翔。如果一個lib中有很多需要被外界調(diào)用的服務,并且調(diào)用的次數(shù)不多曲秉,或者我不僅僅對服務解耦采蚀,那么用這種路由的方式很好,因為不用寫接口了承二。

4.3.2.1.2 路由方式在服務解耦方面不適用性的討論

但為什么沒選擇這種解耦方式呢榆鼠?因為這種方式,對于android整體的服務解耦來說亥鸠,我還是提出了如下的顧慮(僅代表自己的觀點妆够,可能比較粗鄙,并不是說人家的項目不夠優(yōu)秀):

  • 對于大面積的解耦负蚊,肯定大部分是app界別的服務進行解耦神妹。特點是大量使用,這時候我寫幾個接口家妆,下沉到base庫鸵荠,無傷大雅。這樣我在使用的時候伤极,serviceloader好處就突出來了:使用服務的時候蛹找,我不需要關(guān)心實現(xiàn)類的類名,包名是什么哨坪,需要傳入什么參數(shù)庸疾,調(diào)用的方法的名字是什么。如果使用路由方式接口齿税,我需要關(guān)心的事情就多了彼硫,如果我需要關(guān)心這么多東西,它就不應該叫服務了。如果另一個lib在你不知情的情況下改了名字怎么辦拧篮?并且在代碼移植到其他app或獨立運行時词渤,配置方式也不夠友好。serviceloader只需要寫個配置txt文件放在apk中即可串绩,并且每一個lib的服務寫到自己的serviceCinfig即可缺虐,不需要宿主app關(guān)心。使用路由方式礁凡,即使action可以自動注冊高氮,也需要在application處理一些注冊的事情。
  • 路由這種服務框架和serviceloader顷牌,本質(zhì)來說剪芍,并不能進行真正意義上的模塊間的通信。說的通俗點窟蓝,路由框架能做的是:b lib可以在不依賴a lib項目的情況下罪裹,b可以new 出來a中一個類的實例(或提前new好),然后調(diào)用那個實例的方法运挫。這并非通信状共,只是能夠調(diào)用其他倉庫的方法。而通信指的是監(jiān)聽狀態(tài)谁帕,回調(diào)峡继。serviceloader同樣也做不到真正意義上的通信。模塊間通信只能通過非顯式的監(jiān)聽機制才能進行匈挖,比如eventbus碾牌,廣播,contentprovider等來進行儡循。為什么要說這一點呢小染?因為我看到很多模塊化的博客都在說使用路由框架進行模塊間通信。但就前面提到的這種路由框架贮折,確實做不到真正意義上的模塊間通信。
    ok资盅,serviceloader解耦 vs 路由方式解耦就到這里调榄。

4.4 解耦方面的其他工作

4.4.1 工作評估

前面很大一個篇幅都在講使用serviceloader進行服務的解耦。那么除了這個呵扛,還需要做什么每庆?這里我先大體總結(jié)一下,再逐個闡述:

4.4.2 服務實現(xiàn)的抽離

第一點的后半句需要注意一下:如果你希望所有模塊都能夠獨立打包運行今穿,那么需要把所有的服務實現(xiàn)也抽離出來缤灵。如果不想獨立運行,只是想進行解耦,那么還是留在宿主app中即可腮出。雖然說這么一句話很輕松帖鸦。但是抽離一個服務實現(xiàn),真正實施起來卻需要花費很多的時間胚嘲,因為一個服務可能耦合了很多的東西作儿,不留神不好拆中跌。這一點讀者們心里要先有個數(shù)缠沈。 不過能抽離就盡量抽離吧,不只是lib的獨立運行熟吏。對之后服務的替換也有很大的好處妓雾。比如網(wǎng)絡(luò)加載庫娶吞,以前使用的是retrofi+okhttp,后來升級成了retrofit+長連接械姻。替換的時候只是在服務配置文件中改一句話的事情妒蛇。如果打算抽離,要注意接口的定義策添,不要耦合具體某個庫的類材部,考慮要全面,設(shè)計要合理唯竹。比如INet庫乐导,接口定義為:

         public interface INetService extends IProvider {
      <T> T create(final Class<T> service, String getdataPolicy, String        cacheTime);
       }

雖然retrofit是一個很棒的庫,但接口也沒有耦合這個庫浸颓。說不定哪一天就替換了物臂。

4.4.3 數(shù)據(jù)庫的抽離

第二點說起來很痛苦,數(shù)據(jù)庫的抽離真的是很麻煩产上。不知道在哪個版本開始棵磷,貓眼耦合了greendao。這個數(shù)據(jù)庫本身來說挺優(yōu)秀的晋涣,但是架不住它太大仪媒!如果我想把一個lib給別人用,難道我這個lib還得耦合一個大的第三方數(shù)據(jù)庫谢鹊?K惴浴!因為之前沒有考慮過模塊化佃扼,所以基本所有的網(wǎng)絡(luò)數(shù)據(jù)偎巢,敏感數(shù)據(jù)等都進行了grrendao的保存。所以解耦的時候每每看到daossion兼耀,我都是虎軀一震压昼。網(wǎng)絡(luò)數(shù)據(jù)使用文件存儲且對業(yè)務代碼透明求冷。敏感數(shù)據(jù)使用數(shù)據(jù)庫存儲,但用接口隔離窍霞,并且數(shù)據(jù)庫建議使用官方的數(shù)據(jù)庫sqlite或者room匠题。

4.4.4 和butterKnife說再見

第三點的意思是如果你想將業(yè)務代碼獨立模塊化,那么就得跟像butterknife框架的view注入功能說拜拜了官撼。因為android adt14開始梧躺,library的R資源不再是final類型的了,所以在library中你不能使用R.id.xx傲绣,需要使用findViewById()來代替;也不能使用switch(R.id.xx),需要使用if..else來代替掠哥。
第四點是第一點的后續(xù)工作。不存在多少工作量秃诵。

4.4.5 頁面跳轉(zhuǎn)

4.4.5.1 頁面跳轉(zhuǎn)需要做的事情

頁面跳轉(zhuǎn)也是app中需要重視的一個事情续搀,因為它是模塊化的門戶,涉及到頁面與頁面菠净,其他app禁舷、i版到頁面之間的通信問題。雖然看起來簡單毅往,但如果設(shè)計不合理牵咙,那么模塊化入口的代碼優(yōu)雅度,crash數(shù)量攀唯,頁面降級洁桌,運營協(xié)作等方面都會受到影響。
對于頁面間的跳轉(zhuǎn)侯嘀,我們的一般做法:

  • 如果這個類頁面沒有隱式跳轉(zhuǎn)功能:

    • 那么直接在其他頁面首先
      獲取intent(getContext()另凌,TargetActivity.class),然后intent添加參數(shù)戒幔。
      最后starActivity(getContext()吠谢, intent)。
    • 在目標activty 的onCreate()里面getIntent().getString(xx_key,defaultValue)等獲取參數(shù);
    • 如果xx_key對應的value不合法或者解析錯誤诗茎,比如movieId=0工坊,或者等于“”。那么應該跳轉(zhuǎn)到一個其他頁面或者跳轉(zhuǎn)失敗敢订。
  • 如果這個頁面配置了隱式跳轉(zhuǎn)功能:

    • 那么在其他頁面你首先得創(chuàng)建一個createXxxActivityIntent()的utils方法栅组,在里面?zhèn)魅肼涞仨摰膒ath,參數(shù)key枢析,參數(shù)value。
    • 在manifest中聲明刃麸。
    • 在目標activty 的onCreate()里面getIntent().getData().parseBoolean(xx_key,defaultValue)...等獲取參數(shù)
    • 如果xx_key對應的value不合法或者解析錯誤醒叁,比如movieId=0,或者等于“”。那么應該跳轉(zhuǎn)到一個其他頁面或者跳轉(zhuǎn)失敗把沼。
4.4.5.2 android原生頁面跳轉(zhuǎn)存在的問題

下面說下這種使用原生頁面跳轉(zhuǎn)存在的問題~

  • 在獲取參數(shù)的時候啊易,需要寫一大推的intent.get(xx),如果這個頁面既含有隱式跳轉(zhuǎn)饮睬,又含有顯示跳轉(zhuǎn)租谈, 那么肯定上面那個過程都需要,這樣在onCreate()里面就會非常的亂捆愁。要進行if else
  • 如果想進行隱式跳轉(zhuǎn)割去,那么都需要在manifest進行注冊intent-filter。一是麻煩昼丑,二是我需要在另外一個地方去配置某一個activity的東西呻逆,管理不方便。
  • 需要另外寫一個utils獲取隱式intent菩帝。
  • 沒有降級策略咖城,如果運營配錯了,那么只能到錯誤頁面呼奢,而無法進行一個補救措施宜雀,比如進入i版頁面。
  • 開發(fā)人員或者后臺配置錯誤參數(shù)的時候握础,我們需要寫兜底邏輯辐董。每一個頁面解析都需要寫一段相同的邏輯。
  • 如果一個頁面需要登錄用戶才可以打開的權(quán)限弓候,那么我們經(jīng)常會寫if(isLogin()){//跳轉(zhuǎn)頁面}else{//跳轉(zhuǎn)到登錄頁面} 郎哭,每次操作都要寫這些個相同的邏輯。

如果覺得在這方面沒那么多要求菇存,針對頁面間的跳轉(zhuǎn)夸研,為了不耦合其他的模塊的類,所有頁面都可以采用隱式跳轉(zhuǎn)機制來進行依鸥。這基本已經(jīng)可以滿足情況了亥至。但我這里還是想說下阿里推出的開源框架Arouter。其具有攔截功能贱迟,這樣跳轉(zhuǎn)失敗可以有降級處理(比如呈現(xiàn)i版頁面)姐扮,讓頁面具有登錄用戶可打開權(quán)限;獲取參數(shù)方式統(tǒng)一等衣吠。還是挺不錯的茶敏。基本解決了上面所面臨的問題缚俏。具體就不展開了惊搏,具體可以看開源最佳實踐:Android平臺頁面路由框架ARouter

4.5 模塊間/頁面間通信

4.5.0 使用ViewModel來進行頁面間數(shù)據(jù)共享

這一段是新加的內(nèi)容贮乳。我覺得放到這里比較合適。ViewModel是google新推出的lifecycle-component中的類恬惯,官方文檔中闡述使用ViewModel可以解決頁面旋轉(zhuǎn)等配置改變時數(shù)據(jù)保存的問題向拆。我思考了下,覺得它在解耦頁面內(nèi)數(shù)據(jù)共享的問題也能發(fā)揮作用酪耳。

舉一個我以前遇到過的例子:一個頁面做完了浓恳,pm找我做頁面的埋點。埋點需要頁面的movieId信息碗暗,但是需要埋點的那個block中并沒有movieId颈将。并且我這個block層級很深。如果想拿到movieId讹堤,我需要從activity頁面層級一層層傳到我這個block中吆鹤,免不了中間層級的耦合和方法的創(chuàng)建。當時覺得這件事真是讓人頭大洲守。那時候多么需要有個像事件監(jiān)聽形式的eventbus那樣的東西疑务,我只需要把數(shù)據(jù)放到bus里面,然后這個頁面的任何一個地方都能很方便的獲取梗醇≈剩總結(jié)一下:直白點說就是頁面block/fragment之間需要使用對方的數(shù)據(jù)/view時,無需之間硬性的引用叙谨,只需要activity的context參數(shù)就可以獲取對方的數(shù)據(jù)/view温鸽,從而進行數(shù)據(jù)交流、view訪問手负。而頁面的context是系統(tǒng)類型且是很容易獲取的涤垫,并不存在耦合。
具體使用可以參考我之前寫的一篇文章使用ViewModel共享頁面內(nèi)的數(shù)據(jù):ActivityDataBus

4.5.1 為什么要去掉eventbus竟终,使用廣播

如果已經(jīng)到了這一步蝠猬,那么大體上一個頁面已經(jīng)抽離出來了,剩下的是與其他模塊统捶、其他頁面間的互動了榆芦。
前面說了serviceloader和路由方式都沒辦法做這些事情喘鸟。我們首先想到的是使用eventbus來做這些事情愕把。使用eventbus的前提是圣絮,需要定義一些Event事件棒搜。比如:

但如果你將業(yè)務代碼各自模塊化之后,就有一個尷尬的問題擺在面前:Event事件放在哪里赤屋?因為很多庫都需要收聽這個Event事件欣范,所以只能將Event下沉到基礎(chǔ)庫狠半。這樣導致的結(jié)果是基礎(chǔ)庫越來越大垛耳,還無法拆分哥纫。關(guān)于這點
微信Android模塊化架構(gòu)重構(gòu)實踐也提到了這件事情松靡,并且自創(chuàng)性的使用了一種叫“.api”化的方式來解決這件事情。原理是在編譯期將公用接口下沉到基礎(chǔ)庫建椰,供其他module使用雕欺,而這段代碼的維護仍然放到非基礎(chǔ)庫中。這種base庫不會膨脹棉姐,代碼維護的責任制更明確屠列,確定挺不錯∩【兀可惜最近沒有那么多時間來寫這個gradle插件了笛洛。不知道哪個讀者有時間和興趣可以實現(xiàn)這個插件。意義還是很大的乃坤,基礎(chǔ)庫的代碼不會越來越膨脹苛让。eventbus除了使基礎(chǔ)庫膨脹之外,還有一個問題是湿诊,不能進行app間的進程通信狱杰。
我們使用廣播來取代eventbus。android推出的LocalBroadcast實現(xiàn)機制簡單來說是looper-handler并維護一個全局的map厅须。性能上和eventbus類似仿畸,使用字符串而不是Event model來匹配事件。我們?nèi)绻褂?br> 一個接口來包裝BroadcastManager,那么我們在app內(nèi)部可以使用域內(nèi)廣播進行错沽,對于模塊化后的lib簿晓,我們可以使用域外廣播來進行app間的通信。

4.5.2 不要亂發(fā)廣播

如果你項目中大量的使用eventbus千埃,那么你會看到一個類中有大量的onEventMainThread()方法憔儿,寫起來很爽,閱讀起來很痛苦放可。如果項目中發(fā)送這個Event的地方非常多皿曲,接收這個Event的地方也很多。在進行代碼拆分時吴侦,你都不敢輕舉妄動,生怕哪些事件沒有被接收坞古。廣播和eventbus類似备韧,如果項目中存在同一事件的大量發(fā)送和接收,那么項目的可讀性和可維護性就會變得相當差痪枫。這種情況在敏感數(shù)據(jù)的同步問題上尤為突出:

其實對于敏感數(shù)據(jù)的同步织堂,不需要發(fā)送廣播或eventbus來進行同步∧坛拢可以借助數(shù)據(jù)庫將想看數(shù)據(jù)本地化來完成同步易阳。大體的思想就是我們從網(wǎng)絡(luò)中獲取的數(shù)據(jù)都同步到數(shù)據(jù)庫。在進行敏感數(shù)據(jù)填充view時吃粒,采用的數(shù)據(jù)都來自數(shù)據(jù)庫潦俺。在頁面返回時,如果頁面不觸發(fā)填充敏感數(shù)據(jù)view的邏輯徐勃,那么在onResume()手動調(diào)用事示,即:

那么模塊間/頁面間通信大體的就講完了,這里需要做的工作不多:

4.6 lib獨立運行

4.6.1 為什么需要與宿主app進程通信

到這一步僻肖,一個業(yè)務模塊既可以作為library放在宿主app中肖爵,也可以作為application獨立運行了。作為library很容易理解臀脏,和文章前面的問答模塊闡述的一樣劝堪,做宿主app中添加幾個activty的殼子,然后添加上lib中的page揉稚,然后在manifest中注冊即可秒啦,即:



當然如果還需要做一些actionBar的交互,需要在宿主activty中寫入相應的邏輯窃植。整個app的框架圖如:



當業(yè)務lib需要調(diào)試時帝蒿,我們需要讓這個lib獨立運行,就如同文章前面的問答業(yè)務模塊demo所示巷怜。那這時候就有一個問題葛超,我們lib獨立運行時暴氏,賬戶的數(shù)據(jù)從何而來,和app相關(guān)的地理位置绣张,城市等等這些數(shù)據(jù)怎么得到答渔?讀者可能會說這些不是服務嗎?服務的話侥涵,不應該和網(wǎng)絡(luò)加載沼撕,圖片加載的服務一樣使用serviceloader加載嗎?按道理講是這樣的芜飘,但賬戶等一些信息的服務實現(xiàn)類并不是那么容易從宿主app中抽離出來务豺,因為那么服務實現(xiàn)類需要application中進行初始化,還要考慮很多其他東西嗦明。所以真實的賬戶信息并不那么容易通過以前的那種方式獲取笼沥,那怎么辦呢?最簡單的辦法是制作假數(shù)據(jù)娶牌,比如造一個我自己賬戶的信息奔浅,作為服務實現(xiàn)類使用。但這樣的話诗良,賬戶信息只能是一個人的汹桦,對賬戶信息的修改不可行,賬戶也不能退出登錄鉴裹。所以還得想新的辦法舞骆。

4.6.2 與宿主app進程通信過程

最后發(fā)現(xiàn)如果我們獨立運行的lib能夠監(jiān)聽宿主app的賬戶,位置径荔,城市葛作,登錄類型,設(shè)備等信息并能夠進行同步猖凛,那么獨立運行的lib中的這些信息就都是真實信息了赂蠢,并且是動態(tài)的。當宿主app退出登錄辨泳,lib中也是無登錄狀態(tài)虱岂。具體的操作是:

  • 在宿主app中,我們提供一些contentProvider菠红,各方法提供的內(nèi)容就是宿主app真實的的賬戶等數(shù)據(jù)第岖。當對宿主app賬戶等信息改變時,通知contentProvider的監(jiān)聽者试溯,比如:

            public void onEventMainThread(LoginEvent loginEvent) {
      getContext().getContentResolver().notifyChange(Uri.parse("content://com.maoyan.android.runenv/loginsession"),null);
       }
    
  • 在獨立app中蔑滓,其扮演contentProvider的監(jiān)聽者:

           mContentResolver.registerContentObserver(Uri.parse("content://com.maoyan.android.runenv/devicesession"), false, new ContentObserver(null) {
          @Override
          public void onChange(boolean selfChange) {
              super.onChange(selfChange);
              reloadEnviroment();
          }
      });
    

這樣的話,lib中賬戶等數(shù)據(jù)就和宿主app的數(shù)據(jù)保持一致了。我們使用服務接口包一層键袱,這樣使用方式和之前的服務使用方式就一致了燎窘。
大體的示意圖如:

當宿主app退出登錄,lib中也是無登錄狀態(tài)蹄咖,我們看下demo:

最后褐健,按照慣例,當一個模塊要獨立運行時澜汤,需要做的事情評估:

5 最后的話

整個流程終于結(jié)束了蚜迅,希望讀者看完后,對模塊化有個整體的認識俊抵,對每一步需要做什么谁不,耗時多少都有個大致的了解。進行模塊化并不是
為了炫技徽诲,表明自己多厲害拍谐。如果只是這樣,那大可不必這么做馏段。因為模塊化是一個繁瑣,枯燥践瓷,耗費時間長院喜,你做了大量的工作,但是在
表面功能上晕翠,老板們可能看不到喷舀。還不如花一點時間,引入一個第三方庫看著花哨淋肾。很大一部分工作量是為以前欠設(shè)計的代碼邏輯買單硫麻。我做這件事件也是為了業(yè)務服務,因為貓眼電影需要服務的客戶端不少樊卓。所示做業(yè)務解耦拿愧,業(yè)務進行模塊化是必然的事情。通過模塊化后碌尔,可以很方便的將代碼移植到其他端浇辜,app內(nèi)頁面的調(diào)整也變得簡單。
最后的最后唾戚,在整個模塊化的過程中柳洋,有一些經(jīng)驗感悟可以分享給大家,道理都很簡單叹坦,更重要的是落實:

6 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末熊镣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绪囱,老刑警劉巖测蹲,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異毕箍,居然都是意外死亡弛房,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門而柑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來文捶,“玉大人,你說我怎么就攤上這事媒咳〈馀牛” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵涩澡,是天一觀的道長顽耳。 經(jīng)常有香客問我,道長妙同,這世上最難降的妖魔是什么射富? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮粥帚,結(jié)果婚禮上胰耗,老公的妹妹穿的比我還像新娘。我一直安慰自己芒涡,他們只是感情好柴灯,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著费尽,像睡著了一般赠群。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上旱幼,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天查描,我揣著相機與錄音,去河邊找鬼柏卤。 笑死叹誉,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的闷旧。 我是一名探鬼主播长豁,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼忙灼!你這毒婦竟也來了匠襟?” 一聲冷哼從身側(cè)響起钝侠,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎酸舍,沒想到半個月后帅韧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡啃勉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年忽舟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淮阐。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡叮阅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出泣特,到底是詐尸還是另有隱情浩姥,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布状您,位于F島的核電站勒叠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏膏孟。R本人自食惡果不足惜眯分,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柒桑。 院中可真熱鬧弊决,春花似錦、人聲如沸幕垦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽先改。三九已至,卻和暖如春蒸走,著一層夾襖步出監(jiān)牢的瞬間仇奶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工比驻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留该溯,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓别惦,卻偏偏與公主長得像狈茉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掸掸,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,185評論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫氯庆、插件蹭秋、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,107評論 4 62
  • 關(guān)于Android模塊化我有一些話不知當講不當講 最近公司一個項目使用了模塊化設(shè)計,本人參與其中的一個小模塊開發(fā)堤撵,...
    流水不腐小夏閱讀 12,589評論 21 57
  • 奧運前幾年仁讨,京城的公交便宜,9字打頭不到一塊实昨,9以下的四毛洞豁,老年人乘公交免費。但京城人多荒给,堵的厲害丈挟。半小時的路也得...
    vitors閱讀 297評論 1 2