使用Kotlin打造Android路由框架-KRouter

KRouter(https://github.com/richardwrq/KRouter)路由框架借助gradle插件站楚、kapt實現(xiàn)了依賴注入、為Android平臺頁面啟動提供路由功能乘综。
源碼不復(fù)雜,在關(guān)鍵地方也有注釋說明贸弥,建議打算或正在使用kapt+kotlinpoet遇到坑的同學(xué)可以fork一下項目禁荒,或許能找到你想要的答案,只要將整個流程了解清楚了尔崔,相信你自己也能擼一個輪子出來答毫,目前許多開源框架daggerbutter knife您旁、greendao等實現(xiàn)原理都是一致的烙常。

從startActivity開始說起

在組件化開發(fā)的實踐過程中,當(dāng)我完成一個模塊的開發(fā)后(比如說這個模塊中有一個Activity或者Service供調(diào)用者調(diào)用)鹤盒,其他模塊的開發(fā)者要啟動我這個模塊中的Activity的代碼我們再熟悉不過了:

val intent = Intent(this, MainActivity::class.java)
intent.putExtra("param1", "1")
intent.putExtra("param2", "2")
startActivity(intent)

當(dāng)然,其他模塊的開發(fā)人員需要知道我們這個Activity的類名以及傳入的參數(shù)對應(yīng)的key值(上面的param1和param2)侦副,這時候我就想侦锯,在每一個需要啟動這個頁面的地方都存在著類似的樣板代碼,而且被啟動的Activity在取出參數(shù)對屬性進行賦值時的代碼也比較繁瑣秦驯,于是在網(wǎng)上查找相關(guān)資料了解到目前主流的路由框架(ARouter尺碰、Router等)都支持這些功能,秉著盡量不重復(fù)造輪子的觀念我fork了ARouter項目译隘,但是閱讀源碼后發(fā)現(xiàn)其暫時不支持Service的啟動亲桥,而我負(fù)責(zé)的項目里面全是運行在后臺的Service。固耘。题篷。
緊接著也大概了解了一下其他一些框架,都存在一些不太滿意的地方厅目,考慮再三番枚,干脆自己擼一個輪子出來好了法严。


首先來看一段最簡單的發(fā)起路由請求的代碼(Java調(diào)用):

KRouter.INSTANCE.create("krouter/main/activity?test=32")
                .withFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
                .withString("test2", "this is test2")
                .request();

其中krouter/main/activity?test=32為對應(yīng)的路由路徑,可以使用類似http請求的格式葫笼,在問號后緊接著的是請求參數(shù)深啤,這些參數(shù)最終會自動包裝在intent的extras中,也可以通過調(diào)用with開頭的函數(shù)來配置請求參數(shù)路星。
上面的代碼執(zhí)行后最終會啟動一個Activity溯街,準(zhǔn)確來說是一個帶有@Route注解的Activity,它長這樣:

@Route(path = "krouter/main/activity")
public class MainActivity extends Activity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getIntent().getIntExtra("test", -1);//這里可以獲取到請求參數(shù)test
    }
    ...
}

這是一個最基本的功能洋丐,怎么樣呈昔,看起來還不錯吧?跟大部分路由框架的調(diào)用方式差不多〉姘ぃ現(xiàn)在主流的路由框架是怎么做到的呢韩肝?下面就看我一一道來。


在使用KRouter的API前首先需要為一些類添加注解:

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/2
 * Time: 上午10:53
 * Version: v1.0
 * Description:用于標(biāo)記可路由的組件
 */
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Route(
        /**
         * Path of route
         */
        val path: String,
        /**
         * PathPrefix of route
         */
        val pathPrefix: String = "",
        /**
         * PathPattern of route
         */
        val pathPattern: String = "",
        /**
         * Name of route
         */
        val name: String = "undefined",
        /**
         * Priority of route
         */
        val priority: Int = -1)

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/2
 * Time: 上午10:53
 * Version: v1.0
 * Description:用于攔截路由的攔截器
 */
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Interceptor(
        /**
         * Priority of interceptor
         */
        val priority: Int = -1,
        /**
         * Name of interceptor
         */
        val name: String = "DefaultInterceptor")

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/2
 * Time: 上午10:53
 * Version: v1.0
 * Description:屬性注入
 */
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class Inject(
        /**
         * Name of property
         */
        val name: String = "",
        /**
         * If true, app will be throws NPE when value is null
         */
        val isRequired: Boolean = false,
        /**
         * Description of the field
         */
        val desc: String = "No desc.")

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/2
 * Time: 上午10:53
 * Version: v1.0
 * Description:Provider
 */
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Provider(/**
                           * Path of Provider
                           */
                          val value: String)

被注解的元素的信息最終被保存在對應(yīng)的數(shù)據(jù)類中:

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/4
 * Time: 上午10:46
 * Version: v1.0
 * Description:Route元數(shù)據(jù)九榔,用于存儲被[com.github.richardwrq.krouter.annotation.Route]注解的類的信息
 */
data class RouteMetadata(
        /**
         * Type of Route
         */
        val routeType: RouteType = RouteType.UNKNOWN,
        /**
         * Priority of route
         */
        val priority: Int = -1,
        /**
         * Name of route
         */
        val name: String = "undefine",
        /**
         * Path of route
         */
        val path: String = "",
        /**
         * PathPrefix of route
         */
        val pathPrefix: String = "",
        /**
         * PathPattern of route
         */
        val pathPattern: String = "",
        /**
         * Class of route
         */
        val clazz: Class<*> = Any::class.java)
/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/8
 * Time: 下午10:46
 * Version: v1.0
 * Description:Interceptor元數(shù)據(jù)哀峻,用于存儲被[com.github.richardwrq.krouter.annotation.Interceptor]注解的類的信息
 */
data class InterceptorMetaData(
        /**
         * Priority of Interceptor
         */
        val priority: Int = -1,
        /**
         * Name of Interceptor
         */
        val name: String = "undefine",
        /**
         * Class desc of Interceptor
         */
        val clazz: Class<*> = Any::class.java)

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/3/14
 * Time: 上午1:28
 * Version: v1.0
 * Description:Injector元數(shù)據(jù),用于存儲被[com.github.richardwrq.krouter.annotation.Inject]注解的類的信息
 */
data class InjectorMetaData(
        /**
         * if true, throw NPE when the filed is null
         */
        val isRequired: Boolean = false,
        /**
         * key
         */
        val key: String = "",
        /**
         * field name
         */
        val fieldName: String = "")

其中被@Route注解的類是Android中的四大組件和Fragment或者它們的子類(目前尚不支持Broadcast以及ContentProvider)哲泊,被@Route注解的對象目前有3種處理方式:

  1. 若被注解的類是Activity的子類剩蟀,那么最終的處理方式是startActivity;
  2. 若被注解的類是Service的子類切威,最終的處理方式有兩種育特,也就 是Android中啟動Service的兩種方式,使用哪種啟動方式取決于是否調(diào)用了withServiceConn函數(shù)添加了ServiceConnection先朦;
  3. 若被注解的類是Fragment的子類缰冤,最終的處理方式是調(diào)用無參構(gòu)造函數(shù)構(gòu)造出這個類的實例,并調(diào)用setArguments(Bundle args)將請求參數(shù)傳入Fragment的bundle中喳魏,最后返回該實例

@Interceptor注解的類需實現(xiàn)IRouteInterceptor接口棉浸,這些類主要處理是否攔截路由的邏輯,比如某些需要登錄才能啟動的組件刺彩,就可以用到攔截器
@Inject用于標(biāo)記需要被注入的屬性
@Provider注解的類最終可以調(diào)用KRouter.getProvider(path: String)方法獲取該類的對象迷郑,如果該類實現(xiàn)了IProvider接口,那么init(context: Context)方法將被調(diào)用
這些注解最終都不會被編譯進class文件中创倔,在編譯時期這些注解會被收集起來最終交由不同的Annotation Processor去處理嗡害。

KRouter路由框架分為3個模塊:

  • KRouter-api模塊,作為SDK提供API供應(yīng)用調(diào)用畦攘,調(diào)用KRouter-compiler模塊生成的類中的方法加載路由表霸妹,處理路由請求
  • KRouter-compiler模塊,各種注解對應(yīng)的Processor的集合念搬,編譯期運行抑堡,負(fù)責(zé)收集路由組件摆出,并生成kotlin代碼
  • KRouter-gradle-plugin模塊,自定義gradle插件首妖,在項目構(gòu)建時期添加相關(guān)依賴以及相關(guān)參數(shù)的配置
各模塊運行時期.png
KRouter-compiler

在介紹該模塊之前如果有同學(xué)不知道Annotation Processor的話建議先閱讀 Annotation Processing-Tool詳解偎漫, 一小時搞明白注解處理器(Annotation Processor Tool)這兩篇文章,簡單來說有缆,APT就是javac提供的一個插件象踊,它會搜集被指定注解所注解的元素(類、方法或者屬性)棚壁,最終將搜集到的這些交給注解處理器Annotation Processor進行處理杯矩,注解處理器通常會生成一些新的代碼(推薦大名鼎鼎的square團隊造的輪子javapoet,這個開源庫提供了非常友好的API讓我們?nèi)ド蒍ava代碼)袖外,這些新生成的代碼會與源碼一起在同一個編譯時期進行編譯史隆。
但是Annotation Processorjavac提供的一個插件,也就是說它只認(rèn)識Java代碼曼验,它壓根不知道kotlin是什么泌射,所以如果是用kotlin編寫的代碼文件最終將會被javac給忽略,所幸的是JetBrains在2015年就推出了kapt來解決這一問題鬓照。而且既然有javapoet熔酷,那square那么牛逼的團隊肯定也會造一個生成kotlin代碼的輪子吧,果不其然豺裆,在github一搜kotlinpoet拒秘,還真有,所以最終決定KRouter-compiler模塊使用kapt+kotlinpoet來自動生成代碼(kotlinpoet文檔過于簡單了臭猜,建議使用該庫的同學(xué)通過它的測試用例或者參照J(rèn)avapoet文檔了解API的調(diào)用)躺酒。

開頭的例子中我們可以看到使用KRouter啟動一個Activity只需要知道該Activity的路徑即可,并不需要像Android原生的啟動方式一樣傳入Class<*>或者Class Name蔑歌,那么KRouter是怎么做到的呢阴颖?
原理很簡單,KRouter-compiler模塊生了初始化路由表的代碼丐膝,這些路由表內(nèi)部其實就是一個個map,這些map以路徑path作為key钾菊,數(shù)據(jù)類作為value(比如RouteMetadata)帅矗,SDK內(nèi)部會通過path獲取到數(shù)據(jù)類,像開頭啟動Activity的例子中煞烫,SDK就通過path獲取到一個RouteMetadata對象浑此,在這個對象中取出被注解的類的Class<*>,有了這個Class<*>就可以完成啟動Activity的操作滞详。
接下來說說路由表初始化代碼生成之后是怎么被執(zhí)行的凛俱,首先我定義了這樣一些接口:

/**
 * 加載路由
 *
 * @author: Wuruiqiang <a href="mailto:263454190@qq.com">Contact me.</a>
 * @version: v1.0
 * @since: 18/1/4 下午6:38
 */
interface IRouteLoader {
    fun loadInto(map: MutableMap<String, RouteMetadata>)
}

/**
 * 加載攔截器
 *
 * @author: Wuruiqiang <a href="mailto:263454190@qq.com">Contact me.</a>
 * @version: v1.0
 * @since: 18/1/5 上午9:12
 */
interface IInterceptorLoader {
    fun loadInto(map: TreeMap<Int, InterceptorMetaData>)
}

/**
 * 加載Provider
 *
 * @author: Wuruiqiang <a href="mailto:263454190@qq.com">Contact me.</a>
 * @version: v1.0
 * @since: 18/1/5 上午9:12
 */
interface IProviderLoader {
    fun loadInto(map: MutableMap<String, Class<*>>)
}

@Route注解為例紊馏,在KRouter-compiler中定義了一個繼承自AbstractProcessor的類RouteProcessor,在編譯期間編譯器會收集@Route注解的元素的信息然后交由RouteProcessor處理蒲犬,RouteProcessor會生成一個實現(xiàn)了IRouteLoader接口的類朱监,在loadInto方法中把注解中的元數(shù)據(jù)與被注解的元素的部分信息存到RouteMetadata對象,然后將注解的路徑path作為key原叮,RouteMetadata對象作為value保存在一個map當(dāng)中赫编。生成的代碼如下(項目build之后可以在(module)/build/generated/source/kaptKotlin/(buildType)目錄下找到這些自動生成的類):

/**
 *    ***************************************************
 *    * THIS CODE IS GENERATED BY KRouter, DO NOT EDIT. *
 *    ***************************************************
 */
class KRouter_RouteLoader_app : IRouteLoader {
    override fun loadInto(map: MutableMap<String, RouteMetadata>) {
        map["krouter/sample/MainActivity"] = RouteMetadata(RouteType.ACTIVITY, -1, "undefined", "krouter/sample/MainActivity", "", "", MainActivity::class.java)
        map["myfragment"] = RouteMetadata(RouteType.FRAGMENT_V4, -1, "undefined", "myfragment", "", "", MainActivity.MyFragment::class.java)
        map["krouter/sample/fragment1"] = RouteMetadata(RouteType.FRAGMENT, -1, "undefined", "krouter/sample/fragment1", "", "", Fragment1::class.java)
        map["krouter/sample/fragment2"] = RouteMetadata(RouteType.FRAGMENT, -1, "undefined", "krouter/sample/fragment2", "", "", Fragment2::class.java)
        map["krouter/sample/Main2Activity"] = RouteMetadata(RouteType.ACTIVITY, -1, "undefined", "krouter/sample/Main2Activity", "", "", Main2Activity::class.java)
        map["krouter/sample/Main3Activity"] = RouteMetadata(RouteType.ACTIVITY, -1, "undefined", "krouter/sample/Main3Activity", "", "", Main3Activity::class.java)
    }
}

代碼生成之后,我們需要執(zhí)行loadInto方法才算是把數(shù)據(jù)存入到map中去奋隶,我們可以通過Class.forName(ClassName).newInstance()獲取該類實例擂送,然后將其強制轉(zhuǎn)換為IRouteLoader類型,接著調(diào)用loadInto方法傳入map即可唯欣,現(xiàn)在問題來了嘹吨,加載一個類我們需要知道這個類的路徑和名稱:com.x.y.ClassA,但是SDK并不知道KRouter-compiler會生成哪些類境氢。
為此我準(zhǔn)備了兩種解決方案:

  1. 類似ARouter的做法蟀拷,掃描所有dex文件,找出實現(xiàn)了ARouter接口的類产还,然后將這些類的ClassName緩存至本地匹厘,下次應(yīng)用啟動時如果存在緩存且沒有新增文件則讀取緩存內(nèi)容即可;
  2. 第二種是生成的類及其路徑遵循一定的規(guī)則,比如由RouteProcessor生成的類路徑規(guī)定為com.github.richardwrq.krouter阔逼,類名規(guī)定以“KRouter_RouteLoader_”作為開頭然后拼接上Module名稱(以Module名稱作為后綴是避免在不同的Module下生成類名一樣的類淹辞,導(dǎo)致編譯時出現(xiàn)類重復(fù)定義異常),所以RouteProcessor名稱為app的Module下生成的類就是com.github.richardwrq.krouter.KRouter_RouteLoader_app炕柔,在程序運行的時候,我們的SDK只需要獲取項目中所有Module的名稱媒佣,然后依次加載它們并執(zhí)行loadInto方法即可匕累。

基于性能考慮我采取了第二種方案,這就需要解決一個問題默伍,因為RouteProcessor是無法知道當(dāng)前是處于哪個Module的欢嘿,所以我們需要在Module的build.gradle做如下配置:

kapt {
    arguments {
        arg("moduleName", project.getName())
    }
}

這樣我們就配置了一個名為“moduleName”的參數(shù),它的值就是當(dāng)前Module的名稱也糊。這個參數(shù)可以在ProcessingEnvironmentgetOptions()方法獲取的map中取出炼蹦,
RouteInterceptor狸剃、Provider三者的處理流程大致相同掐隐,就不一一贅述了。
在這里提一下關(guān)于依賴注入Inject的實現(xiàn)钞馁,關(guān)于如何對屬性進行注入我想了兩種解決方案:

  1. 第一種就是通過反射虑省,了解反射的同學(xué)都知道可以通過反射獲取類的運行時注解匿刮,并且通過反射API為類的屬性進行賦值,但由于時反射探颈,所以性能上有所損耗熟丸,但是可以無視屬性的訪問權(quán)限;
  2. 第二種是生成需要被注入的類的擴展方法膝擂,在擴展方法里面對接收者的屬性進行賦值虑啤,性能更好,但是缺點是無法對private以及protected成員進行賦值架馋。

一開始是希望偷懶狞山,就選擇了第一種方案,但是問題來了叉寂,我知道Java的反射會有一些性能上的問題萍启,但速度還不至于讓用戶感知明顯,但是當(dāng)我調(diào)用kotlin反射相關(guān)API時(最主要是獲取Properties相關(guān)API)屏鳍,發(fā)現(xiàn)第一次調(diào)用花費的在4~5s 左右勘纯,之后調(diào)用速度是毫秒級的,我猜測是第一次調(diào)用加載了大量數(shù)據(jù)钓瞭,然后將這些數(shù)據(jù)緩存起來了驳遵,但這4~5s的調(diào)用時間實在是惡心,所以最終還是決定采用方案2山涡,有興趣的同學(xué)可以查看com/github/richardwrq/krouter/compiler/processor/RouteProcessor.kt堤结,生成的代碼如下:

class com_github_richardwrq_krouter_activity_Main2Activity_KRouter_Injector : IInjector {
    override fun inject(any: Any, extras: Bundle?) {
        val bundle = getBundle(any, extras)//getBundle為自動生成的頂層方法
        (any as Main2Activity).exInject(bundle)
    }

    private fun Main2Activity.exInject(bundle: Bundle) {
        person = bundle.get("person") as? Person ?: KRouter.getProvider<Person>("person") ?: parseObject(bundle.getString("person"), object : TypeToken<Person>() {}.getType()) ?: throw java.lang.NullPointerException("Field [person] must not be null in [Main2Activity]!")//parseObject為自動生成的頂層方法
        provider = bundle.get("NoImplProvider") as? NoImplProvider ?: KRouter.getProvider<NoImplProvider>("NoImplProvider") ?: parseObject(bundle.getString("NoImplProvider"), object : TypeToken<NoImplProvider>() {}.getType()) ?: throw java.lang.NullPointerException("Field [provider] must not be null in [Main2Activity]!")
        myProvider = (KRouter.getProvider<MyProvider>("provider/myprovider")) ?: throw java.lang.NullPointerException("Field [myProvider] must not be null in [Main2Activity]!")
    }
}

生成的類路徑與擴展方法接收者的類路徑相同(解決Java包內(nèi)訪問權(quán)限問題),類名命名規(guī)則為擴展方法接收者類路徑的“.“替換為”_“作為前綴鸭丛,后綴為”_KRouter_Injector“竞穷,比如被被注入的類是com.github.richardwrq.krouter.activity.Main2Activity,那么自動生成的類為com.github.richardwrq.krouter.activity.com_github_richardwrq_krouter_activity_Main2Activity_KRouter_Injector

KRouter-api

該模塊其實就是提供API給用戶調(diào)用的SDK
上面提到SDK需要執(zhí)行KRouter-compiler模塊類的代碼才能真正完成路由表初始化的工作鳞溉,由于最終編譯器會將所有Module打包成一個apk瘾带,所以在APP運行時是不存在Module的概念的,但是按照解決方案2各Module生成的類會以Module名稱作為后綴熟菲,因此必須想辦法讓SDK獲取到項目中所有Module的名稱看政,考慮再三,我采取的解決方案是從assets目錄入手抄罕,在項目構(gòu)建時期創(chuàng)建一個task帽衙,這個task會在Module的src/main/assets目錄下生成一個“KRouter_ModuleName”的文件,在SDK初始化的時候只需要列出assets目錄下所有"KRouter_"開頭的文件并截取下劃線“_”后的內(nèi)容贞绵,即可得到一個包含所有Module名稱的列表。
下面給出SDK的類圖恍飘,同學(xué)們可以對照源碼參考

KRouter-api類圖.png

KRouter-gradle-plugin

完成上述兩個模塊后其實KRouter框架已經(jīng)可以正常使用了榨崩,引用方式如下:
在各Module的build.gradle加入下面的代碼

kapt {
    arguments {
        arg("moduleName", project.getName())
    }
}
dependencies {
    implementation 'com.github.richardwrq:krouter-api:x.y.z’
    kapt 'com.github.richardwrq:krouter-compiler:x.y.z'
}
afterEvaluate {
    //在assets目錄創(chuàng)建文件的task
    ...
}

當(dāng)項目中Module較多時谴垫,手動在每一個Module加入這些配置未免有些蠢。母蛛。所以我寫了一個gradle插件用來自動完成這些配置工作翩剪,具體實現(xiàn)參考源碼,邏輯非常簡單彩郊,最后使用引用方式變成下面這樣:
在項目根目錄build.gradle文件加入如下配置

buildscript {

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:x.y.z"
        classpath "com.github.richardwrq:krouter-gradle-plugin:x.y.z"
    }
}

然后在各Module的build.gradle文件加入如下配置

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: "com.github.richardwrq.krouter"

到這里KRouter路由框架就粗略的介紹了一遍前弯,由于kapt仍在不斷完善,所以使用過程中難免碰到一些坑或者本身API功能不夠完善秫逝,下面就列舉一些遇到的問題以及解決方法:

ToDoList
  • 通過gradle插件修改AndroidManifest.xml文件恕出,自動注冊路由組件(Activity、Service)
  • 目前尚不支持動態(tài)加載的插件的路由注冊违帆,但有解決方案浙巫,hook classloader裝載方法,在加載dex文件時掃描KRouter的路由組件
  • 支持多應(yīng)用多進程環(huán)境下的頁面路
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刷后,一起剝皮案震驚了整個濱河市的畴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌尝胆,老刑警劉巖丧裁,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異含衔,居然都是意外死亡煎娇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門抱慌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逊桦,“玉大人,你說我怎么就攤上這事抑进∏烤” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵寺渗,是天一觀的道長匿情。 經(jīng)常有香客問我,道長信殊,這世上最難降的妖魔是什么炬称? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮涡拘,結(jié)果婚禮上玲躯,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好跷车,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布棘利。 她就那樣靜靜地躺著,像睡著了一般朽缴。 火紅的嫁衣襯著肌膚如雪善玫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天密强,我揣著相機與錄音茅郎,去河邊找鬼。 笑死或渤,一個胖子當(dāng)著我的面吹牛系冗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播劳坑,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼毕谴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了距芬?” 一聲冷哼從身側(cè)響起涝开,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎框仔,沒想到半個月后舀武,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡离斩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年银舱,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跛梗。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡寻馏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出核偿,到底是詐尸還是另有隱情诚欠,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布漾岳,位于F島的核電站轰绵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏尼荆。R本人自食惡果不足惜左腔,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望捅儒。 院中可真熱鬧液样,春花似錦振亮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至撮抓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間摇锋,已是汗流浹背丹拯。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留荸恕,地道東北人乖酬。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像融求,于是被迫代替她去往敵國和親咬像。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,789評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理生宛,服務(wù)發(fā)現(xiàn)县昂,斷路器,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • 聽到閨蜜放年假要來重慶耍我欣喜若狂陷舅,和丹青有一年多沒有見倒彰,她在深圳,我在重慶莱睁,賞的是同一輪明月待讳,但相隔千里。...
    云影M閱讀 225評論 0 1
  • The sun has disappeared into a distance. 太陽慢慢消失在遠方仰剿, There...
    譚樹君閱讀 453評論 0 1
  • 能為人師和好為人師的區(qū)別很大创淡。
    十年一井閱讀 136評論 0 0