Android架構(gòu)模式之MVC棚亩、MVP蓖议、MVVM

在開始講解各種架構(gòu)模式時,我們先來看下沒有經(jīng)過設(shè)計的代碼是如何編寫的讥蟆。為了不分散重點勒虾,筆者舉的例子會比較簡單,初始時從數(shù)據(jù)庫緩存中獲取用戶信息展示到界面上攻询,點擊刷新按鈕可以從服務(wù)器上拉取最新的用戶信息并進(jìn)行展示从撼。

由于從數(shù)據(jù)庫和服務(wù)器上獲取數(shù)據(jù)都屬于更底層的邏輯,因此這兩個操作一開始就會進(jìn)行封裝,不會列入討論范圍低零,并且為了使程序更加簡單婆翔,這兩個操作都是使用的測試代碼進(jìn)行模擬。

User.java

// User實體類掏婶,再沒有封裝意識的人啃奴,實體類總會有一個吧
public class User {
    public String name;
    public int age;
}

DbUtils.java

// 數(shù)據(jù)庫工具
public class DbUtils {
    // 查詢數(shù)據(jù)庫記錄并返回cursor,這里使用測試代碼直接返回null
    public static Cursor query(String sql) {
        return null;
    }
    
    // 更新數(shù)據(jù)庫記錄雄妥,這里使用測試代碼不進(jìn)行任何實際處理
    public static void update(String sql) {
        
    }
}

HttpUtils.java

// http工具
public class HttpUtils {
    private static Handler sHandler = new Handler(Looper.getMainLooper());

    public interface ResponseCallback {
        void onResponseSuccessed(String json);
        void onResponseFailed(int reason);
    }

    // 發(fā)起http請求最蕾,這里使用模擬的數(shù)據(jù),并有一定機(jī)率請求失敗
    public static void request(Map params, final ResponseCallback callback) {
        sHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                int value = new Random(System.currentTimeMillis()).nextInt(5);
                if(callback != null) {
                    if(value == 2) {
                        callback.onResponseFailed(1);
                    } else {
                        callback.onResponseSuccessed("{\"name\": \"純爺們\", \"age\": 20}");
                    }
                }
            }
        }, 500);
    }
}

activity_user.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/tv_name"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:id="@+id/tv_age"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_marginTop="20dp"
        android:text="刷新"
        android:id="@+id/btn_refresh"/>

</LinearLayout>

UserActivity.java

public class UserActivity extends Activity {
    private TextView mNameView;
    private TextView mAgeView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);

        mNameView = (TextView)findViewById(R.id.tv_name);
        mAgeView = (TextView)findViewById(R.id.tv_age);
        
        // 加載緩存的用戶數(shù)據(jù)并展示
        User user = loadUser();
        if(user != null) {
            mNameView.setText("昵稱:" + user.name);
            mAgeView.setText("年齡:" + user.age);
        }

        findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 從服務(wù)器拉取最新用戶數(shù)據(jù)并顯示
                refresh();
            }
        });
    }

    private User loadUser() {
        // 這里本應(yīng)從cursor中獲取數(shù)據(jù)老厌,但為求程序盡量簡單瘟则,我們直接使用模擬數(shù)據(jù)。之所以要加入query這段代碼枝秤,是為了盡可能模擬真實的流程醋拧。
        Cursor cursor = DbUtils.query(null);
        if(cursor != null) {
            try {

            } catch (Exception e) {

            } finally {
                cursor.close();
            }
        }
        User user = new User();
        user.name = "小蝦米";
        user.age = 19;
        return user;
    }

    private void refresh() {
        HttpUtils.request(null, new HttpUtils.ResponseCallback() {
            @Override
            public void onResponseSuccessed(String json) {
                try {
                    User user = new Gson().fromJson(json, User.class);
                    
                    // 用戶信息更新了,要同步更新數(shù)據(jù)庫中的記錄
                    String updateSql = null;
                    DbUtils.update(updateSql);
                   
                    mNameView.setText("昵稱:" + user.name);
                    mAgeView.setText("年齡:" + user.age);
                } catch (Exception e) {
                }
            }

            @Override
            public void onResponseFailed(int reason) {
                Toast.makeText(UserActivity.this, "刷新失敗", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

點擊刷新前顯示如下界面

image

點擊刷新后顯示如下界面
image

上面的例子請讀者務(wù)必記勞淀弹,后續(xù)講到的幾種架構(gòu)模式全部使用的都是這個例子丹壕。

上述例子一個非常突出的問題是,用戶信息可能在多個界面上都需要顯示薇溃,而在這些界面上菌赖,從數(shù)據(jù)庫和服務(wù)器上獲取用戶信息的流程都要寫一遍,重復(fù)編寫不僅容易出錯也不容易維護(hù)沐序。解決該問題的方法就是封裝一個可復(fù)用的Model琉用,MXX模式也由此產(chǎn)生。

MVC

MVC由Model策幼、View辕羽、Controller組成,Android提供的xml布局是View層的主要部分垄惧,View本身已經(jīng)是相對比較獨立的了,因此MVC中主要考慮的就是Model的設(shè)計绰寞。我們來看下MVC中各層在Android中的主要應(yīng)用:

  1. Model:表示模型層到逊,模型的一個核心特點就是可復(fù)用。包含數(shù)據(jù)業(yè)務(wù)實體(entity/bean)本身滤钱,以及圍繞該實體進(jìn)行的業(yè)務(wù)操作(本地或遠(yuǎn)程增刪改查操作)觉壶。即Model層主要針對數(shù)據(jù),包含數(shù)據(jù)實體和數(shù)據(jù)訪問件缸,如果非要以模型來稱呼和理解的話铜靶,前者為數(shù)據(jù)模型,后者為業(yè)務(wù)模型他炊。
  2. View:表示視圖層争剿,負(fù)責(zé)界面數(shù)據(jù)的展示已艰,以及響應(yīng)用戶操作。
  3. Controller:表示控制層蚕苇,負(fù)責(zé)邏輯處理哩掺,由其連接Model和View。Controller通過Model獲取數(shù)據(jù)并傳遞給View進(jìn)行展示涩笤;通過響應(yīng)View傳遞過來的用戶事件調(diào)用Model的接口進(jìn)行業(yè)務(wù)處理嚼吞。

后續(xù)內(nèi)容都使用簡稱,M代表Model蹬碧,V代表View舱禽,C代表Controller。

其中恩沽,V和C一般又統(tǒng)稱為UI層誊稚,由于Android已經(jīng)提供了xml布局,因此在Android中V和C并不需要刻意區(qū)分飒筑,可以統(tǒng)一以UI層來理解片吊,UI層的核心代碼包含xml和Activity(或Fragment,或另外封裝的控制器)协屡,后者既扮演著部分V的角色俏脊,又扮演著全部的C角色。我們來看下使用MVC重構(gòu)后的例子肤晓,增加了UserBusiness類爷贫,修改了UserActivity的代碼,以下只貼出更新的部分代碼补憾,其余代碼請參考之前的例子漫萄。

UserBusiness.java

public class UserBusiness {
    private static final UserBusiness INSTANCE = new UserBusiness();

    private List<UserListener> mListeners = new LinkedList<>();

    public static UserBusiness get() {
        return INSTANCE;
    }

    public void addListener(UserListener listener) {
        if(listener == null) {
            return;
        }
        synchronized (mListeners) {
            if(!mListeners.contains(listener)) {
                mListeners.add(listener);
            }
        }
    }

    public void removeListener(UserListener listener) {
        if(listener == null) {
            return;
        }
        synchronized (mListeners) {
            mListeners.remove(listener);
        }
    }

    public User getUser() {
        Cursor cursor = DbUtils.query(null);
        if(cursor != null) {
            try {

            } catch (Exception e) {

            } finally {
                cursor.close();
            }
        }
        User user = new User();
        user.name = "小蝦米";
        user.age = 19;
        return user;
    }

    public void requestUser() {
        HttpUtils.request(null, new HttpUtils.ResponseCallback() {
            @Override
            public void onResponseSuccessed(String json) {
                User user = null;
                try {
                    user = new Gson().fromJson(json, User.class);
                } catch (Exception e) {
                }
                if(user != null) {
                    String updateSql = null;
                    DbUtils.update(updateSql);
                    notifyRequestUser(0, user);
                } else {
                    notifyRequestUser(1, null);
                }
            }

            @Override
            public void onResponseFailed(int reason) {
                notifyRequestUser(reason, null);
            }
        });
    }

    private void notifyRequestUser(int code, User user) {
        List<UserListener> listeners = new LinkedList<>();
        synchronized (mListeners) {
            listeners.addAll(mListeners);
        }
        for(UserListener listener : listeners) {
            listener.onRequestUserResult(code, user);
        }
    }

    public interface UserListener {
        void onRequestUserResult(int code, User user);
    }
}

UserActivity.java

public class UserActivity extends Activity implements UserBusiness.UserListener {
    private TextView mNameView;
    private TextView mAgeView;
    private UserBusiness mUserBusiness = UserBusiness.get();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);

        mNameView = (TextView)findViewById(R.id.tv_name);
        mAgeView = (TextView)findViewById(R.id.tv_age);

        // 加載緩存的用戶數(shù)據(jù)并展示
        User user = mUserBusiness.getUser();
        if(user != null) {
            mNameView.setText("昵稱:" + user.name);
            mAgeView.setText("年齡:" + user.age);
        }

        findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 從服務(wù)器拉取最新用戶數(shù)據(jù)并顯示
                mUserBusiness.requestUser();
            }
        });

        mUserBusiness.addListener(this);
    }

    @Override
    protected void onDestroy() {
        mUserBusiness.removeListener(this);
        super.onDestroy();
    }

    @Override
    public void onRequestUserResult(int code, User user) {
        if(code == 0) {
            mNameView.setText("昵稱:" + user.name);
            mAgeView.setText("年齡:" + user.age);
        } else {
            Toast.makeText(UserActivity.this, "刷新失敗", Toast.LENGTH_SHORT).show();
        }
    }
}

重構(gòu)后的代碼有如下優(yōu)點:

  1. Activity的代碼變得簡單和整潔了,Activity現(xiàn)在只需要處理控制邏輯(UI邏輯)盈匾,以及作為V和M通信的橋梁腾务。
  2. 業(yè)務(wù)代碼封裝在UserBusiness中,一是隱藏了數(shù)據(jù)操作(業(yè)務(wù)流程)的具體細(xì)節(jié)削饵,使得UI層在訪問時更簡單了岩瘦;二是可以復(fù)用,任何模塊都可以輕松訪問窿撬,且可以通過在UserBusiness中注冊一個監(jiān)聽器來監(jiān)聽用戶業(yè)務(wù)的相關(guān)事件启昧。

但MVC仍具有以下缺點:

  1. V不可復(fù)用,然而在Android中V復(fù)用沒有意義劈伴,需要復(fù)用的話完全可以封裝可復(fù)用的控件密末,然后V組裝這些控件。
  2. C不可復(fù)用。
  3. V和C之間還存在部分耦合严里,因此除了M外V和C都無法進(jìn)行單元測試新啼。

為了解決以上缺點,便有了MVP田炭。

MVP

MVP由Model师抄、View和Presenter組成,M和V就不再重復(fù)解釋了教硫,P和C一樣叨吮,承擔(dān)著控制層的責(zé)任。MVP相比MVC作了如下改進(jìn)(或者說變化瞬矩,是否改進(jìn)視情況而定):

  1. P可復(fù)用茶鉴,這意味著不能再使用Activity(或...)作為P了,很簡單景用,Activity不能復(fù)用(使用繼承達(dá)到復(fù)用的場景不在這討論范圍之內(nèi))涵叮。由此,Activity從控制層的角色轉(zhuǎn)向了視圖層伞插,即在MVP中V由xml和Activity組成割粮。

    也可以另外抽離一個V,然后將Activity作為創(chuàng)建V和P的管理器媚污,并負(fù)責(zé)將V和P進(jìn)行綁定舀瓢。但不建議采用這種方式,額外增加了代碼耗美,并且也沒帶來多少益處京髓,除非想要復(fù)用V或者界面異常復(fù)雜而拆分了多個V和P。

  2. MVP三者皆可以獨立完成單元測試商架,為了達(dá)到這個目的堰怨,P和V需要做到完全解耦,解耦一般使用接口蛇摸。

我們來看下使用MVP重構(gòu)過的代碼备图,在mvc的基礎(chǔ)上主要是改動了UserActivity.java,然后增加了幾個類赶袄。

PresenterContext.java

public interface PresenterContext {
    Activity getActivity();
}

UserPresenter.java

public interface UserPresenter {
    void onRefresh();
    void onInited();
    void onDestroyed();
}

UserPresenterImpl.java

public class UserPresenterImpl implements UserPresenter, UserBusiness.UserListener {
    private PresenterContext mContext;
    private UserView mView;
    private UserBusiness mUserBusiness = UserBusiness.get();

    public UserPresenterImpl(PresenterContext context, UserView view) {
        mContext = context;
        mView = view;
    }

    @Override
    public void onRefresh() {
        mUserBusiness.requestUser();
    }

    @Override
    public void onInited() {
        mUserBusiness.addListener(this);
        User user = mUserBusiness.getUser();
        if(user != null) {
            mView.updateName(user.name);
            mView.updateAge(user.age);
        }

    }

    @Override
    public void onDestroyed() {
        mUserBusiness.removeListener(this);
    }

    @Override
    public void onRequestUserResult(int code, User user) {
        if(code == 0) {
            mView.updateName(user.name);
            mView.updateAge(user.age);
        } else {
            Toast.makeText(mContext.getActivity(), "刷新失敗", Toast.LENGTH_SHORT).show();
        }
    }
}

UserView.java

public interface UserView {
    void updateName(String name);
    void updateAge(int age);
}

UserActivity.java

public class UserActivity extends Activity implements UserView, PresenterContext {
    private TextView mNameView;
    private TextView mAgeView;
    private UserPresenter mPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);

        mNameView = (TextView)findViewById(R.id.tv_name);
        mAgeView = (TextView)findViewById(R.id.tv_age);

        mPresenter = new UserPresenterImpl(this, this);

        findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPresenter.onRefresh();
            }
        });

        mPresenter.onInited();
    }

    @Override
    protected void onDestroy() {
        mPresenter.onDestroyed();
        super.onDestroy();
    }

    @Override
    public void updateName(String name) {
        mNameView.setText("昵稱:" + name);
    }

    @Override
    public void updateAge(int age) {
        mAgeView.setText("年齡:" + age);
    }

    @Override
    public Activity getActivity() {
        return this;
    }
}

我們來講解下:

  1. 由于Presenter不像Activity一樣诬烹,它沒有上下文信息,因此增加了PresenterContext類作為Presenter的上下文信息弃鸦。

    這里PresenterContext只是用來獲取Activity,是因為示例是力求簡單幢痘,實際項目中它可以獲取的信息會更多唬格。

  2. UserActivity的代碼被拆分成了兩部分,一部分仍然在UserActivity中,作為View的代碼购岗,一部分抽離到UserPresenterImpl中汰聋,作為Presenter的代碼。至此喊积,將視圖和控制層的代碼完全分離了烹困。
  3. 新建了UserViewUserPresenter兩個接口用來表示V和P,V的實現(xiàn)方UserActivity持有UserPresenter接口而非具體的實現(xiàn)類乾吻,P的實現(xiàn)方持有UserView接口而非具體的實現(xiàn)類髓梅,從而達(dá)到V和P解耦。

    需要對P進(jìn)行單元測試時绎签,只需要創(chuàng)建一個類簡單地實現(xiàn)UserView的類枯饿,然后和UserPresenterImpl綁定即可;需要對V進(jìn)行單元測試時诡必,只需要創(chuàng)建一個簡單實現(xiàn)UserPresenter的類奢方,然后在UserActivity中將構(gòu)建P的那行代碼修改下即可。

MVP相對MVC具有P復(fù)用及方便做單元測試的優(yōu)點爸舒,然而蟋字,在實際Android項目中,P復(fù)用的場景基本不存在扭勉,且多數(shù)公司并沒有做單元測試鹊奖。因此,多數(shù)情況下剖效,MVC可能比MVP更適合Android項目嫉入,畢竟MVP多引入了不入類和代碼,且?guī)斫怦畹耐瑫r也使得代碼更加“繞”璧尸。

MVVM

不管是MVC還是MVP都存在幾下問題:

  1. 在每個界面都要編寫不少的findViewById咒林、setOnClickListener之類的代碼。
  2. 數(shù)據(jù)更新后爷光,要手動調(diào)用setText之類的代碼刷新視圖垫竞。

以上幾點都不是什么大問題,編寫這些代碼也不會輕易出錯蛀序,但總有達(dá)人追求極致欢瞪,MVVM便由此產(chǎn)生了。Android解決第問題1的方法是在xml中直接嵌入代碼(類似JSX的寫法)徐裸,解決問題2的方法是提供了DataBinding方案綁定視圖和數(shù)據(jù)陨瘩。MVVM真正地將V層完全地體現(xiàn)在xml上,M層還是老樣子(模式怎么變它都不變)埠通,VM(ViewModel)用來代替C和P声怔,以MVC作為改造回懦,VM包括ActivityDataBinding(自動生成)。

本文主要講解幾種架構(gòu)模式的應(yīng)用場景和區(qū)別次企,因此不會過多講解MVVM在Android中如何使用怯晕,沒接觸過MVVM的建議先看下這篇入門文章或查閱官方文檔。

使用DataBinding需要在build.gradle中加入如下代碼:

android {
    dataBinding {
        enabled = true
    }
}

配置了之后build時會下載DataBinding的依賴包以及自動生成部分代碼缸棵,自動生成的代碼后續(xù)遇到時會提到舟茶。

接著我們來看下在MVC的基礎(chǔ)上變更后的MVVM代碼。

activity_user.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="user"
            type="com.sean.mvvm.model.entity.User" />

        <variable
            name="host"
            type="com.sean.mvvm.UserActivity"/>
    </data>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text='@{"昵稱:" + user.name}' />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text='@{"年齡:" + String.valueOf(user.age)}'/>

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_marginTop="20dp"
            android:text="刷新"
            android:onClick="@{host.onRefresh}"/>

    </LinearLayout>

</layout>

User.java

public class User extends BaseObservable {
    @Bindable
    public String name;

    @Bindable
    public int age;

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }
}

UserActivity.java

public class UserActivity extends Activity implements UserBusiness.UserListener {
    private User mUser;
    private UserBusiness mUserBusiness = UserBusiness.get();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ActivityUserBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_user);
        mUser = mUserBusiness.getUser();
        if(mUser == null) {
            mUser = new User();
        }
        binding.setUser(mUser);
        binding.setHost(this);

        mUserBusiness.addListener(this);
    }

    @Override
    protected void onDestroy() {
        mUserBusiness.removeListener(this);
        super.onDestroy();
    }

    @Override
    public void onRequestUserResult(int code, User user) {
        if(code == 0) {
            mUser.setName(user.name);
            mUser.setAge(user.age);
        } else {
            Toast.makeText(UserActivity.this, "刷新失敗", Toast.LENGTH_SHORT).show();
        }
    }

    public void onRefresh(View v) {
        mUserBusiness.requestUser();
    }
}

分析一下:

  1. xml布局的頂部標(biāo)簽變成了layout堵第,data標(biāo)簽存儲非布局相關(guān)代碼吧凉,variable定義變量。
  2. 編譯時會根據(jù)xml名稱和內(nèi)容自動生成binding類型诚,如例子中的ActivityUserBinding客燕,variable定義的變量在binding中都有對應(yīng)的set、get方法狰贯。
  3. 數(shù)據(jù)改變時要做到自動刷新UI也搓,實體類必須繼承BaseObservable,且需要自動觸發(fā)的字段必須以Bindable注解涵紊,并在字段值變化時調(diào)用notifyPropertyChanged方法傍妒。

PS:強(qiáng)烈建議至少熟讀一個自動生成的binding類,綁定的所有原理都在這里摸柄,代碼很容易理解颤练,也不需要去網(wǎng)絡(luò)上尋求答案。

上面的例子實現(xiàn)了數(shù)據(jù)的單向綁定(數(shù)據(jù)更新觸發(fā)UI更新)和事件的綁定驱负,而Android是支持?jǐn)?shù)據(jù)雙向綁定的嗦玖,現(xiàn)在來看下當(dāng)UI更新時如何觸發(fā)數(shù)據(jù)的更新。使用方式其實很簡單跃脊,在xml中小小修改下就行:

android:text='@={user.name}'

注意到@后面多了個=宇挫,同時昵稱:去掉了,=表示數(shù)據(jù)雙向綁定酪术,但當(dāng)=存在時器瘪,右側(cè)的表達(dá)式只能是個變量,因此昵稱:只能去掉了绘雁。當(dāng)xml改成這樣后橡疼,TextView的文本發(fā)生變化了,user.name的值也會隨之更新庐舟。

總結(jié)下MVVM的優(yōu)缺點:

  1. 新型xml更加成熟欣除,可以獨立支撐View層。然而挪略,這可能也是缺點耻涛,畢竟這種xml編程方式和Android傳統(tǒng)的方式差異較大废酷。

    如果不想改變xml的編寫方式,又希望使用MVVM抹缕,那么可以仿照自動生成的binding類自己編寫一個ViewModel,但是有沒有這個必要呢墨辛。卓研。。

  2. 只關(guān)心數(shù)據(jù)變化睹簇,而不需要關(guān)注視圖的刷新奏赘,刷新由自動生成的binding處理了。
  3. 數(shù)據(jù)雙向綁定是個偽命題太惠,實際上并沒有完全做到自動化磨淌,還是需要手動編寫額外的代碼,并且也有條件限制凿渊。
  4. 在非主module(一般為app)中無法編譯新型xml(這個也可能是筆者使用不當(dāng)梁只,有待確認(rèn))。

綜上埃脏,MVVM相比MVC搪锣、MVP并沒有多大優(yōu)勢,但可以通知配置減少一些重復(fù)的邏輯代碼彩掐。使用哪種模式根據(jù)實際情況而定构舟,沒有誰比誰更好。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末堵幽,一起剝皮案震驚了整個濱河市狗超,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌朴下,老刑警劉巖努咐,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異桐猬,居然都是意外死亡麦撵,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門溃肪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來免胃,“玉大人,你說我怎么就攤上這事惫撰「嵘常” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵厨钻,是天一觀的道長扼雏。 經(jīng)常有香客問我坚嗜,道長,這世上最難降的妖魔是什么诗充? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任苍蔬,我火速辦了婚禮,結(jié)果婚禮上蝴蜓,老公的妹妹穿的比我還像新娘碟绑。我一直安慰自己,他們只是感情好茎匠,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布格仲。 她就那樣靜靜地躺著,像睡著了一般诵冒。 火紅的嫁衣襯著肌膚如雪凯肋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天汽馋,我揣著相機(jī)與錄音侮东,去河邊找鬼。 笑死惭蟋,一個胖子當(dāng)著我的面吹牛苗桂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播告组,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼煤伟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了木缝?” 一聲冷哼從身側(cè)響起便锨,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎我碟,沒想到半個月后放案,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡矫俺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年吱殉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厘托。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡友雳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铅匹,到底是詐尸還是另有隱情押赊,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布包斑,位于F島的核電站流礁,受9級特大地震影響涕俗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜神帅,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一再姑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧找御,春花似錦询刹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沐兰。三九已至哆档,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間住闯,已是汗流浹背瓜浸。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留比原,地道東北人插佛。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像量窘,于是被迫代替她去往敵國和親雇寇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

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