Android Jetpack -- ViewModel 簡析

ViewModel

不接受任何形式的轉(zhuǎn)發(fā),如有需要請聯(lián)系作者,謝謝理解!

Android Jetpack 是一系列助力您更容易打造優(yōu)秀 Android 應(yīng)用的工具和組件。這些組件能幫助您遵循最佳實踐啡浊、免除編寫繁復(fù)的樣板代碼并簡化復(fù)雜任務(wù)觅够,從而使您可以專注于最核心的代碼邏輯。 Jetpack 中的架構(gòu)指南由 Android 開發(fā)中四個關(guān)鍵領(lǐng)域中的一系列代碼庫和工具提供支持虫啥。它們分別是基礎(chǔ)蔚约、架構(gòu)、行為和 UI涂籽。每個 Jetpack 組件均可單獨采用苹祟,但它們依然可以流暢地協(xié)作。其中的 Lifecycle 庫,可以有效避免內(nèi)存泄漏和解決常見的 Android 生命周期難題,今天將介紹 Lifecycle 庫中的 ViewModel 類 评雌。

Architecture Component

介紹

簡單來說,ViewModel 是用來保存應(yīng)用 UI 數(shù)據(jù)的類,而且它會在配置變更后繼續(xù)存在树枫。它可以脫離 View 單純做 Junit 的測試,更方便大家做單元測試景东。ViewModel 有這樣的特性砂轻,但是它在實際項目能夠有何表現(xiàn),還需要看待各位是如何使用的斤吐。一般會和 Livedata搔涝、DataBinding 等其他組件進行組合使用,下面會介紹具體可以解決什么問題和措,和一般項目中會建議如何使用庄呈。在介紹的過程中,還會穿插一些架構(gòu)的思想以及如何看源碼分析源碼的思路派阱。

解決了什么問題

手機屏幕旋轉(zhuǎn)是配置變更的一種诬留,當(dāng)旋轉(zhuǎn)屏幕時, Activity 會被重新創(chuàng)建。如果數(shù)據(jù)沒有被正確的保存和恢復(fù)文兑,就有可能丟失盒刚。從而導(dǎo)致莫名其妙的 UI 錯誤,甚至應(yīng)用崩潰绿贞。Demo 請點擊這里

kyHxB9.gif
應(yīng)用 UI 數(shù)據(jù)丟失

相反的因块, ViewModel 會在配置更改后繼續(xù)存在,所以樟蠕,如果將應(yīng)用所有的 UI 數(shù)據(jù)保存在 ViewModel 中贮聂,而不是 Activity 中,這樣就可以保證數(shù)據(jù)不會受到配置變更的影響了寨辩。

kybeHA.gif
數(shù)據(jù)保存在 ViewModel 中
ViewModel 生命周期

新手常見犯的錯誤是將很多業(yè)務(wù)變量,邏輯和數(shù)據(jù)都擺在 Activity 或 Fragment 中歼冰,這樣的代碼比較混亂和難以維護靡狞,這種開發(fā)模式違犯了單一責(zé)任的原則。而 ViewModel 可以有效地劃分責(zé)任隔嫡,它可以用來保存 Activity 的所有 UI 數(shù)據(jù),然后 Activity 僅負責(zé)了解如何在屏幕上顯示數(shù)據(jù)和接收用戶互動甸怕,但是 Activity 不會處理這些互動所帶來的業(yè)務(wù)邏輯(如:點擊獲取短信驗證碼按鈕后的網(wǎng)絡(luò)請求)。但不要將太多的邏輯處理腮恩,數(shù)據(jù)獲取放到 ViewModel 類里面處理梢杭,它僅僅作為 UI 數(shù)據(jù)的保存!

簡單使用

上面提到秸滴,不要將太多的邏輯處理武契、數(shù)據(jù)存儲獲取放到 ViewModel 類里面處理,它僅僅作為 UI 數(shù)據(jù)的保存荡含!數(shù)據(jù)存儲獲取可以創(chuàng)建 Repository 類咒唆, UI 數(shù)據(jù)處理可以使用 Presenter 類。Demo 請點擊這里

AkWpKH.png

ViewModel 的使用方式如下:

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
//繼承系統(tǒng) ViewModel 類
class UserProfileViewModel:ViewModel(){
    //將跟 UI 界面有關(guān)的數(shù)據(jù)放至 UserProfileViewModel 中
    val user = User(name="",company="")
}
//在 Activity 中實例化 UserProfileViewModel 變量 
override fun onCreate(saveInstanceState:Bundle?){
    //Setup Activity
    val userViewModel =  ViewModelProviders.of(this).get(UserProfileViewModel::class.java)
    userViewModel.user.name = "Edgar Ng"
}

實際應(yīng)用

ViewModel 一般會搭載其他組件(如:LiveData释液、DataBinding 等)一同使用全释,本系列文章會專門出一篇文章就 ViewModel 及其他組件搭配使用進行說明講解,請點擊此處误债。

本章也會通過購買VIP會員這個業(yè)務(wù)功能點來簡述實際項目中會如何使用 ViewModel 浸船。具體代碼鏈接請點擊這里

k4TX7T.png
k4odiQ.png

k5FfHO.jpg

k5F4ED.jpg

kjtFoj.gif

該 Demo 總得來說分為三層:展示層(Presentation Layer)、域?qū)樱―omain Layer)寝蹈、數(shù)據(jù)層(Data Layer)李命,如下圖所示。

  • 展示層:View躺盛,ViewModel
  • 域?qū)樱簎se cases
  • 數(shù)據(jù)層:repositories

展示層無法直接與數(shù)據(jù)層溝通项戴,ViewModel 只可以通過一個或者多個 UseCase 去獲取 repository,這種分層的做法有助于降低耦合度和提高可測試性槽惫,同時方便管理周叮,讓所有的 UseCase 都在耗時線程中執(zhí)行辩撑,保證在 UI 線程中沒有耗時數(shù)據(jù)的入口。

Google I/O 2018 app數(shù)據(jù)架構(gòu)

展示層具體使用以下技術(shù):Views + ViewModels + LiveData

ViewModels 從 LiveData 那里獲取數(shù)據(jù)提供給 Views仿耽,通過 Data Binding 真正的 UI 控件就可以自動完成數(shù)據(jù)顯示合冀,可以從 Activity 和 Fragment 那些繁雜而單調(diào)的樣式代碼中解放出來。

我們以購買會員為例项贺,說明如何使用 ViewModel君躺、UseCase、Repository开缎。首先涉及到 BuyVIPFragment.kt,VIPViewModel.kt,VIPViewModelFactory.kt,VIPRepository.kt 等類棕叫。

//購買 VIP Fragment 類
class BuyVIPFragment : Fragment() {

    private lateinit var vipViewModel: VIPViewModel

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        activity?.let { activity ->
            //通過 ViewModelProviders.of() 獲得 VIPViewModel 
            //因為使用的是 ViewModelProviders.of(activity) ,所以獲取到的是上一個 Fragment 共有的 ViewModel,從而到達兩個 Fragment 的數(shù)據(jù)可以進行同步更新 
            vipViewModel =
                ViewModelProviders.of(activity, VIPViewModelFactory(activity.application!!))
                    .get(VIPViewModel::class.java)

            //liveData 的監(jiān)聽回調(diào)方法(LiveData 用法不在這里詳細講解)
            vipViewModel.vipAction.observe(this, Observer<VIPDto> {
                btnBuyVIP.visibility = View.VISIBLE
                txtUserName.text = it.userName
                txtDeadlineDate.text = SimpleDateFormat("yyyy-MM-dd").format(it.deadlineDate)
            })
        }
        //觸發(fā)購買 VIP 方法
        btnBuyVIP.setOnClickListener {
            btnBuyVIP.visibility = View.GONE
            vipViewModel.buyVIP()
        }
    }
}
//viewmodel 工廠生成類
class VIPViewModelFactory(val application: Application) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        //新建 VIPViewModel奕删,并觸發(fā)獲取數(shù)據(jù)方法 getData()
        //網(wǎng)上有些例子是不使用 Factory俺泣,在獲取 VIPViewModel 處,再調(diào)用初始化方法完残。這樣也可以實現(xiàn)伏钠,不過如果是多個 Fragment 共用 ViewModel 就有可能會有數(shù)據(jù)重復(fù)加載的問題
        return VIPViewModel(application).also { it.getData() } as T
    }
}
//viewmodel 類
class VIPViewModel(application: Application) : AndroidViewModel(application) {

    private val _VIPAction = MediatorLiveData<VIPDto>()
    //對外 LiveData 類,ViewModel 與 LiveData 結(jié)合使用谨设,方便 UI 控制器進行數(shù)據(jù)監(jiān)聽及更新熟掂,數(shù)據(jù)有更改時會通知觀察者
    val vipAction: LiveData<VIPDto>
        get() = _VIPAction


    //獲取數(shù)據(jù)
    fun getData() {
        //使用 UseCase 獲取數(shù)據(jù),一般 UseCase 的生命周期與 ViewModel 一樣長扎拣,所以他們之間也可以不使用 LiveData 赴肚, 不過這里也使用了 LiveData 進行轉(zhuǎn)化了
        val useCase = GetVIPUseCase(SharePreferencesHelper(getApplication(), "VIP"))
        _VIPAction.addSource(useCase.observe()) {
            //一般會這里處理一些數(shù)據(jù)成功失敗狀態(tài)
            if (it.isSuccess) {
                _VIPAction.value = it.getOrThrow()
            }
        }
        useCase.execute(Unit)
    }

    //購買 VIP
    fun buyVIP() {
        val useCase = BuyVIPUseCase(SharePreferencesHelper(getApplication(), "VIP"))
        _VIPAction.addSource(useCase.observe()) {
            if (it.isSuccess) {
                _VIPAction.value = it.getOrThrow()
            }
        }
        _VIPAction.value?.let { useCase.execute(it) }

    }
}
//獲得 VIP 用戶數(shù)據(jù)
class GetVIPUseCase(private val sharePreferencesHelper: SharePreferencesHelper) : MediatorUseCase<Unit, VIPDto>() {
    override fun execute(parameters: Unit) {
        //從 Repository 獲取數(shù)據(jù)
        val vipRepositoryData = VIPRepository(sharePreferencesHelper).getVIP()

        //一般還會有成功、失敗兩種狀態(tài)
        val switchMapFunction = Function<VIPDto, LiveData<Result<VIPDto>>> {
            MediatorLiveData<Result<VIPDto>>().apply { value = Result.success(it) }
        }

        //敲黑板E羟铩尊蚁!這是重點也是難點!侣夷!
        //1.為什么 UseCase 從 Repository 處獲取數(shù)據(jù)要使用 LiveData横朋?
        //因為生命周期的原因, UseCase 的生命周期一般與 ViewModel 一樣長百拓,
        //ViewModel 的生命周期一般會伴隨 UI 控制器的銷毀而銷毀琴锭。
        //有時候獲取數(shù)據(jù)時間會很漫長,漫長到 UI 控制器銷毀了之后才返回來衙传。
        //這時候就有可能會導(dǎo)致內(nèi)存泄漏或者回調(diào)的時候會出現(xiàn) View/ViewModel 不存在的情況决帖。

        //2.在 UseCase 中如何使用 LiveData?
        //通過使用 Transformations.switchMap() 的目的是可以將上一層( UI 控制器對ViewModel 的 LiveData 觀察狀態(tài)傳遞給 UseCase 蓖捶。
        //因為 LiveData.observe() 方法需要 LifecycleOwner,
        //但是 UseCase 沒有 LifecycleOwner地回,
        //所以通過這種辦法可以進行轉(zhuǎn)化傳遞
        result.addSource(
            Transformations.switchMap(
                vipRepositoryData,
                switchMapFunction
            ) as MediatorLiveData<Result<VIPDto>>
        ) {
            result.value = it
        }
    }
}
//常見的 Repository 類
class VIPRepository(private val sharePreferencesHelper: SharePreferencesHelper) {

    fun buyVIP(user: VIPDto): LiveData<VIPDto> {
        val result = MutableLiveData<VIPDto>()

        //異步賦值的
        GlobalScope.launch(Dispatchers.Main) {
            async(Dispatchers.IO) {
                delay(1000)
                val calendar = Calendar.getInstance()
                calendar.time = user.deadlineDate
                calendar.add(Calendar.MONTH, 1)

                val deadLineDate = calendar.time.toString()
                val userName = user.userName

                val contentValue = SharePreferencesHelper.ContentValue("deadLineDate", deadLineDate)
                val contentValue2 = SharePreferencesHelper.ContentValue("userName", userName)
                sharePreferencesHelper.putValues(contentValue, contentValue2)
                async(Dispatchers.Main) {
                    result.value = VIPDto(userName, Date(deadLineDate))
                }
            }.await()
        }

        return result
    }

    fun getVIP(): LiveData<VIPDto> {

        val result = MutableLiveData<VIPDto>()
        //異步賦值的
        GlobalScope.launch(Dispatchers.Main) {
            async(Dispatchers.IO) {
                delay(1500)
                val deadLineDate = sharePreferencesHelper.getString("deadLineDate") ?: Date().toString()
                val userName = sharePreferencesHelper.getString("userName") ?: "EdgarNg"
                async(Dispatchers.Main) {
                    result.value = VIPDto(userName, Date(deadLineDate))
                }
            }.await()
        }

        return result
    }
}

通過查看代碼可以知道,最后的狀態(tài)是下圖這樣的:

kjaeSI.png

這里留個小功課,當(dāng) APP 在購買 VIP 頁面刻像,翻轉(zhuǎn)屏幕后畅买,APP 會”回到“上一個頁面,請回答為什么细睡,并提出解決辦法谷羞。

ViewModel 的 Unit test (TODO)

內(nèi)部實現(xiàn)

偏向源碼以及設(shè)計思想分析,不感興趣的同學(xué)可以跳過這章溜徙,謝謝湃缎!

不過真得很精彩的窩,真得不看看嗎蠢壹?

源碼來源編文時最新 lifecycle-viewmodel-2.1.0-alpha02-sources.jar嗓违,截圖已經(jīng)附上行號,方便比對

為了更好地詳細了解 ViewModel知残,我們可以帶著幾個問題去學(xué)習(xí):

  1. ViewModel 是怎么生成的?
  2. ViewModel 為什么可以達到這么神奇的效果靠瞎,Activity/Fragment 都重新生成居然還可以活下來,它是怎么做到的?

1. 常見的 ViewModel 實現(xiàn)方式:

val ViewModel1 = ViewModelProviders.of(activity,factory).get(ViewModel::class.java)
val ViewModel2 = ViewModelProviders.of(fragment,factory).get(ViewModel::class.java)

ViewModel 主要實現(xiàn)機制采用了工廠模式求妹,傳入 Factory 去實例化指定的ViewModel類,如果不傳 Factory佳窑,則使用默認的制恍,如下圖。


默認的 Factory

具體的調(diào)用流程如下神凑,由此可以看出ViewModel的生成其實還是由我們傳入的 factory.create() 方法來生成

ViewModel 生成過程

Ps:請注意 ViewModelProvider 和 ViewModelProviders净神,是兩個獨立的類。具體可以從下列截圖看出溉委,ViewModelProviders 是 LifeCycle 庫的擴展類鹃唯,高內(nèi)聚低耦合,核心的東西放在一個類瓣喊,有可能變化的擴展的東西放在另外一個地方進行擴展坡慌,這也可以啟發(fā)到我們新手在設(shè)計框架時要考慮到擴展性,尤其是做 SDK 或者底層架構(gòu)的同學(xué)可以參考學(xué)習(xí)這一塊的設(shè)計方式藻三。

ViewModelProvider 和 ViewModelProviders 的區(qū)別.png

2.為什么 ViewModel 的生命周期這么長(比一般的 UI 數(shù)據(jù)長)

ViewModel 生命周期

首先我們重溫一下 ViewModel 的生命周期(如上圖)洪橘,ViewModel 的生命周期不會因為配置變更(Activity 的多次調(diào)用onCreate()方法)會終止,反而會和 Activity 的 onDestroy()方法一起結(jié)束棵帽。為什么可以這樣子熄求?!請接著往下看逗概。

kcd0C6.png

要想知道 ViewModel 的生命周期弟晚,思路上當(dāng)然是從 ViewModel 那里獲得答案(其實從 UI 控制器那里獲取更快,不過這算作弊)。根據(jù) ViewModel 生成的流程圖可以知道卿城,是調(diào)用了 ViewModelProvider.get(@NonNull String key, @NonNull Class<T> modelClass) 返回生成 ViewModel 的枚钓,查看源碼 get 方法(上圖)可以知道,一共有兩處地方生成 ViewModel 的藻雪,ViewModel viewModel = mViewModelStore.get(key);viewModel = mFactory.create(key, modelClass); 秘噪,但是生成完了之后都通過 mViewModelStore.put(key, viewModel); ,通過方法名稱應(yīng)該就是一個緩存器勉耀,查看 mViewModelStore.put(key, viewModel)的源碼后指煎,發(fā)現(xiàn)就是常見的內(nèi)部緩存了一個Map進行對ViewModel的管理。

ViewModelStore

那么這個 mViewModelStore 是什么便斥?為什么要緩存 viewModel至壤?

這個 mViewModelStore 就是實例化 ViewModelProvider 時候傳進來的(請見 ViewModel 生成的流程圖,由 activity.getViewModelStore()fragment.getViewModelStore() 獲得)枢纠。

kcwoJx.png

了解 mViewModelStore 是什么之后像街,我們就解決第二個疑問? UI 控制器中的屬性 mViewModelStore 為什么要緩存viewModel晋渺?難道是想重新拿出來用镰绎,來達到 ViewModel 的長生命周期?(因為剛剛上上圖提到第一處生成 ViewModel 的就是從 mViewModelStore 那里 get(key) 出來的)接下來我們就去了解 mViewModelStore 在 Activity 和 Fragment 中的作用了木西,我們先了解 Activity 的畴栖。

在 Activity 中能夠看到 getViewModelStore() 方法,mViewModelStore 有三種方式八千,1.自身不為 null 吗讶;2. nc.viewModelStore ;3. new ViewModelStore();。通過查看 mViewModelStore 在整個 Activity 類中恋捆,只有這個方法用到照皆,所以 mViewModelStore 只能從2 3中獲得實例。3不用說沸停,直接看2 --> getLastNonConfigurationInstance() 從名字上看相當(dāng)有嫌疑膜毁!

kc0zB4.png

點擊 getLastNonConfigurationInstance() 查看源碼,發(fā)現(xiàn)描述如下圖星立。通過描述可以知道爽茴,我們沒有找錯!绰垂!獲得之前由{@link #onRetainNonConfigurationInstance()}返回的 non-configuration instance data 室奏。這將從初始化 {@link #onCreate} 和 {@link #onStart} 調(diào)用新實例中獲得 non-configuration instance data ,允許您從前一個實例中提取任何有用的動態(tài)狀態(tài)劲装。話雖如此胧沫,但是還是想知道 mLastNonConfigurationInstances 它是怎么來的昌简?為什么可以把上一個 Activity 的 NonConfigurationInstances 數(shù)據(jù)拿到現(xiàn)在的 Activity 來

kcDusU.png

根據(jù)描述,是從 onRetainNonConfigurationInstance() 方法那里來的绒怨,但是查看了 Activity 的這個方法纯赎,發(fā)現(xiàn)方法里面是 null !然后難道是子類做了實現(xiàn)南蹂?為什么會這么想犬金?

kgIaA1.png
kc0zB4.png
kg7N7j.png

因為 public static ViewModelProvider of(@NonNull FragmentActivity activity,@Nullable Factory factory) 中使用的是 FragmentActivity 而不是 Activity ,在方法設(shè)計上會偏向使用基類來進行參數(shù)傳遞六剥。而且之前的 mViewModelStore 也是由 FragmentActivity 的父類 ComponentActivity 生成的晚顷,而且 NonConfigurationInstances 類也是 ComponentActivity 的內(nèi)部類。所以我們這才推斷是不是 Activity 的子類實現(xiàn)了疗疟。同時我們也可以學(xué)習(xí)到:

  1. 要對 Activity该默、ComponentActivityFragmentActivity策彤、AppCompatActivity 之間的異同有所涉獵栓袖,尤其在設(shè)計底層框架以及接口的時候,要慎重選擇店诗,不然會設(shè)計出來不好用裹刮,處處受阻。
  2. 設(shè)計底層的時候庞瘸,類適當(dāng)?shù)姆謱永^承有助于架構(gòu)的清晰解耦必指。

我們只需查看 FragmentActivityComponentActivity 哪個類實現(xiàn)了 onRetainNonConfigurationInstance() 就可以了。

查看 FragmentActivity 的父類 ComponentActivity 的時候可以見到

TODO:
已知是調(diào)用ComponentActivity的onRetainNonConfigurationInstance()來保存了NonConfigurationInstances數(shù)據(jù)的恕洲,這就涉及到APP的啟動、AMS等一系列比較復(fù)雜的過程梅割,推薦點擊ConfigurationChanged流程梳理(屏幕旋轉(zhuǎn)霜第、語言及字體切換)

  1. 這個方法是什么時候被調(diào)用的?
  2. 它是如何將數(shù)據(jù)給下一個Activity的户辞?

所以這也同時回答了“ ViewModel 為什么不能綁定 Activity泌类、Fragment 的問題?

而 ViewModel 什么時候才會被 clear 掉呢底燎?根據(jù) ViewModel 的生命周期圖標(biāo)刃榨,應(yīng)該是 activity 調(diào)用 onFinish() 會調(diào)用 ViewModel 的 clear()。我們進一步查看哪里調(diào)用了 ViewModel 的 clear() 方法双仍,可知會從 ComponentActivity.JavaFragmentManagerViewModel.java 處有所調(diào)用枢希,我們先分析 ComponentActivity.Java

由下圖可以看到朱沃,當(dāng) ComponentActivity 是 ON_DESTROY 狀態(tài)時苞轿,而且不是配置變更時茅诱,會調(diào)用 ViewModelStore.clear() 方法清空與之綁定的 ViewModel 數(shù)據(jù)。

ComponentActivity清空數(shù)據(jù)

由下圖 FragmentManagerViewModel 清空數(shù)據(jù)分析可見搬卒,當(dāng) Fragment 應(yīng)該 Destroy 時,而且不是配置變更時瑟俭,會調(diào)用 ViewModelStore.clear() 方法清空與之綁定的 ViewModel 數(shù)據(jù)。

k8zBnO.png
k8z29I.png

所以這就解析了契邀,ViewModel 的生命周期為什么可以跟 Activity 一樣長摆寄,以及為什么可以在配置變更后還可以繼續(xù)復(fù)用了。

總結(jié)

  1. ViewModel 本身只是一個保存數(shù)據(jù)的類坯门,需要搭配 livedata微饥、databinding 才能發(fā)揮效果。單獨用作用不大田盈,不過好處是解耦畜号,方便 unit test,效果很明顯允瞧!這篇只是推廣初級文章简软,很多注意的細節(jié)點,可以點擊這里
  2. ViewModel 盡量不要綁定跟Android包有關(guān)的類述暂,有可能造成內(nèi)存泄漏(具體請見[內(nèi)部實現(xiàn)]章節(jié))以及會給 Unit Test 造成困難痹升,Google 建立 JetPack 的其中一個目的就是解耦可測試!

源碼地址

https://github.com/EdgarNg1024/kaixue-docs/tree/master/viewmodel

https://github.com/google/iosched/

參考

  1. Android Jetpack - ViewModel | 中文教學(xué)視頻
  2. ViewModels and LiveData: Patterns + AntiPatterns (譯文)[強烈推薦畦韭,作者應(yīng)該是 Google 里面有參與 ViewModel 的實現(xiàn)開發(fā)的疼蛾,所以文章的推薦用法應(yīng)該是最貼近 ViewModel 開發(fā)思想]
  3. ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders (譯文)[強烈推薦,作者應(yīng)該是 Google 里面有參與 ViewModel 的實現(xiàn)開發(fā)的艺配,所以文章的推薦用法應(yīng)該是最貼近 ViewModel 開發(fā)思想]
  4. Google I/O 2018 app?—?Architecture and Testing[強烈推薦察郁,作者應(yīng)該是 Google 里面有參與 ViewModel 的實現(xiàn)開發(fā)的,所以文章的推薦用法應(yīng)該是最貼近 ViewModel 開發(fā)思想]
  5. SharePreference數(shù)據(jù)存儲工具類[借用了這哥們的SharePreference工具類]

撰稿人

Edgar Ng(https://github.com/EdgarNg1024)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末转唉,一起剝皮案震驚了整個濱河市皮钠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赠法,老刑警劉巖麦轰,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異砖织,居然都是意外死亡款侵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門侧纯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來新锈,“玉大人,你說我怎么就攤上這事茂蚓『攫模” “怎么了剃幌?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長晾浴。 經(jīng)常有香客問我负乡,道長,這世上最難降的妖魔是什么脊凰? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任抖棘,我火速辦了婚禮,結(jié)果婚禮上狸涌,老公的妹妹穿的比我還像新娘切省。我一直安慰自己,他們只是感情好帕胆,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布朝捆。 她就那樣靜靜地躺著,像睡著了一般懒豹。 火紅的嫁衣襯著肌膚如雪芙盘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天脸秽,我揣著相機與錄音儒老,去河邊找鬼。 笑死记餐,一個胖子當(dāng)著我的面吹牛驮樊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播片酝,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼囚衔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了雕沿?” 一聲冷哼從身側(cè)響起佳魔,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎晦炊,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宁脊,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡断国,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了榆苞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稳衬。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖坐漏,靈堂內(nèi)的尸體忽然破棺而出薄疚,到底是詐尸還是另有隱情碧信,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布街夭,位于F島的核電站砰碴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏板丽。R本人自食惡果不足惜呈枉,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望埃碱。 院中可真熱鬧猖辫,春花似錦、人聲如沸砚殿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽似炎。三九已至辛萍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間名党,已是汗流浹背叹阔。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留传睹,地道東北人耳幢。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像欧啤,于是被迫代替她去往敵國和親睛藻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359