一個(gè)高性能、功能豐富、可自定義的 Android 相機(jī)庫 iCamera 的設(shè)計(jì)和開發(fā)過程

1、背景介紹

去年年初的時(shí)候?qū)戇^一篇文章 《CameraX:Android 相機(jī)庫開發(fā)實(shí)踐》,那時(shí)我想自己寫一個(gè) Android 相機(jī)庫,但是因?yàn)槊趾凸雀桕P(guān)放的 CameraX 沖突了,所以現(xiàn)在我將自己的項(xiàng)目改名為 iCamera.

之前的文章中也交代過一些 Android 相機(jī)庫的背景,本身集成相機(jī)功能到自己的項(xiàng)目中并不復(fù)雜,但是如果設(shè)計(jì)一個(gè)功能全面的 Android 相機(jī)庫就沒那么簡單了——你要滿足更多用戶的需求,基本的縮放、閃光燈等這些在日常開發(fā)中不會(huì)涉及的功能都要支持;此外,你還要處理相機(jī)的各種支持尺寸和寬高比的計(jì)算問題,滿足用戶自定義的需求等等。

在 iCamera 之前,為了集成相機(jī)功能,我也找過一些開源的相機(jī)庫。比如 CameraView,雖然是掛名谷歌,但是并不算谷歌官方的庫,而且因?yàn)榇a設(shè)計(jì)的問題,本身性能并不好。再者 CameraFragment,雖然代碼結(jié)構(gòu)清晰得多,但是對(duì)很多功能的支持不夠完善。還有 CameraX,這個(gè)庫我沒有仔細(xì)研究它的代碼,但是它只支持 Camera2. Camera2 雖然 API 21 上就可以使用,但實(shí)際上很多手機(jī)對(duì) Camera2 的支持并不好,就比如在我的手機(jī)(OnePlus6, API 29)上 Camera1 的啟動(dòng)速率明顯高于 Camera2.

綜上,我決定自己寫一個(gè)性能更好、功能全面并且支持用戶自定義的相機(jī)庫。這個(gè)項(xiàng)目去年就開始寫了,但是因?yàn)楣ぷ鞯膯栴}一直沒時(shí)間完善。最近有了些自己的時(shí)間,于是我解決了之前遺留下來的各種問題并做了系統(tǒng)的測試。現(xiàn)在,第一個(gè)版本已經(jīng)正式發(fā)布可用了。

項(xiàng)目地址:iCamera

在這篇文章中,我重點(diǎn)介紹下是如何設(shè)計(jì)和實(shí)現(xiàn)這樣一款 Android 相機(jī)庫的,希望這能夠?qū)δ愕南到y(tǒng)設(shè)計(jì)有所啟發(fā),并且希望這能夠增進(jìn)你對(duì) iCamera 的了解。

2、整體的設(shè)計(jì)與實(shí)現(xiàn)

下面是這個(gè)項(xiàng)目整體的設(shè)計(jì)圖,相比于第一個(gè)版本的設(shè)計(jì),在下面的這個(gè)版本中我又新增了一些方法并對(duì)部分設(shè)計(jì)細(xì)節(jié)做了調(diào)整:

design.png

鏈接地址:https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049

了解了整體的設(shè)計(jì),我再來具體介紹下我是如何通過多種設(shè)計(jì)模式的綜合運(yùn)用來滿足用戶的自定義等需求的。

2.1 單例的應(yīng)用:緩存、自定義和預(yù)加載

最初我在設(shè)計(jì)的時(shí)候希望通過靜態(tài)字段緩存一些相機(jī)的信息,這樣一來可以避免多次從相機(jī)屬性中讀取和計(jì)算各種參數(shù),二來可以通過預(yù)加載操作來把讀取相機(jī)參數(shù)的操作提前到相機(jī)啟動(dòng)之前來達(dá)到加快相機(jī)啟動(dòng)速度的目的。不過后來我發(fā)現(xiàn)使用靜態(tài)單例一樣可以滿足這個(gè)需求并且應(yīng)該說更加合理一些。于是,我在項(xiàng)目中使用了單例的 ConfigurationProvider 來緩存計(jì)算結(jié)果并且提供方法提前讀取相機(jī)信息。該類如下:

public final class ConfigurationProvider {
    /** The singleton */
    private static volatile ConfigurationProvider configurationProvider;

    /**
     * The sizes map from a int value, which was calculated from:
     * hash = {@link CameraFace} | {@link CameraSizeFor} | {@link CameraType} */
    private SparseArray<List<Size>> sizeMap;

    /**
     * The room ratios map from a int value, which was calculated from:
     * hash = {@link CameraFace} | {@link CameraType} */
    private SparseArray<List<Float>> ratioMap;

    private ConfigurationProvider() {
        if (configurationProvider != null) {
            throw new UnsupportedOperationException("U can't initialize me!");
        }
        initWithDefaultValues();
    }

    private void initWithDefaultValues() {
        // initialize all kinds of parameters
    }

    public static ConfigurationProvider get() {
        if (configurationProvider == null) {
            synchronized (ConfigurationProvider.class) {
                if (configurationProvider == null) {
                    configurationProvider = new ConfigurationProvider();
                }
            }
        }
        return configurationProvider;
    }
}

對(duì)于 Camera2,因?yàn)楸旧硭恍枰蜷_相機(jī)就能從系統(tǒng)服務(wù)中讀取相機(jī)信息,所以我們可以方便地實(shí)現(xiàn)預(yù)加載的需求。按照下面這樣,我們只需要在打開相機(jī)之前調(diào)用 prepareCamera2() 就可以提前將相機(jī)的信息讀取并緩存起來,這樣就可以減少相機(jī)啟動(dòng)過程中的時(shí)間。我測試了下,這樣大概可以減少幾十毫秒的時(shí)間:

public final class ConfigurationProvider {

    // ....

    private int numberOfCameras;
    private AtomicBoolean camera2Prepared = new AtomicBoolean();
    private SparseArray<String> cameraIdCamera2 = new SparseArray<>();
    private SparseArray<CameraCharacteristics> cameraCharacteristics = new SparseArray<>();
    private SparseIntArray cameraOrientations = new SparseIntArray();
    private SparseArray<StreamConfigurationMap> streamConfigurationMaps = new SparseArray<>();

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public ConfigurationProvider prepareCamera2(Context context) {
        if (!camera2Prepared.get()) {
            CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
            try {
                assert cameraManager != null;
                final String[] ids = cameraManager.getCameraIdList();
                numberOfCameras = ids.length;
                for (String id : ids) {
                    final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
                    final Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
                    if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
                        cameraIdCamera2.put(CameraFace.FACE_FRONT, id);
                        Integer iFrontCameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
                        cameraOrientations.put(CameraFace.FACE_FRONT, iFrontCameraOrientation == null ? 0 : iFrontCameraOrientation);
                        cameraCharacteristics.put(CameraFace.FACE_FRONT, characteristics);
                        streamConfigurationMaps.put(CameraFace.FACE_FRONT, characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP));
                    } else if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK){
                        cameraIdCamera2.put(CameraFace.FACE_REAR, id);
                        Integer iRearCameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
                        cameraOrientations.put(CameraFace.FACE_REAR, iRearCameraOrientation == null ? 0 : iRearCameraOrientation);
                        cameraCharacteristics.put(CameraFace.FACE_REAR, characteristics);
                        streamConfigurationMaps.put(CameraFace.FACE_REAR, characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP));
                    }
                }
                camera2Prepared.set(true);
            } catch (Exception e) {
                XLog.e(TAG, "initCameraInfo error " + e);
            }
        }
        return this;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public StreamConfigurationMap getStreamConfigurationMap(Context context, @CameraFace int cameraFace) {
        return prepareCamera2(context).streamConfigurationMaps.get(cameraFace);
    }

    // ...
}

此外,后面會(huì)提到我們滿足用戶自定義需求的設(shè)計(jì)邏輯——策略接口,所以,我們又可以通過 ConfigurationProvider 暴露的方法設(shè)置全局的計(jì)算策略,比如具體使用 Camera1 還是 Camera2,使用 TextureView 還是 SurfaceView,各種寬高比、支持的尺寸如何進(jìn)行計(jì)算等:

public final class ConfigurationProvider {

    /** The creator for {@link me.shouheng.icamera.manager.CameraManager}. */
    private CameraManagerCreator cameraManagerCreator;

    /** The creator for {@link me.shouheng.icamera.preview.CameraPreview}. */
    private CameraPreviewCreator cameraPreviewCreator;

    /** The calculator for camera size. */
    private CameraSizeCalculator cameraSizeCalculator;

    // ... setters & getters
}

用戶只需要實(shí)現(xiàn)這些接口并通過 ConfigurationProvider 暴露的方法在相機(jī)啟動(dòng)之前將其賦值給這個(gè)單例就可以實(shí)現(xiàn)相機(jī)默認(rèn)算法的替換。

2.2 策略模式應(yīng)用:滿足用戶自定義需求

之前我們也說過自己開發(fā)一個(gè)相機(jī)庫一個(gè)難點(diǎn)就是處理各種尺寸的計(jì)算問題。因?yàn)椋粋€(gè)相機(jī)支持的寬高比從 2:1 到 1:1 不等,每個(gè)比例下有多個(gè)支持的尺寸。另外,相機(jī)的尺寸又分為預(yù)覽的支持尺寸、照片的支持尺寸和視頻的支持尺寸。所以,要處理的數(shù)據(jù)比較多,問題是你如何整合這些需求。另外還有 Camera1 還是 Camera2,使用 TextureView 還是 SurfaceView 的問題。對(duì)于一般的相機(jī)開發(fā),可能默認(rèn)就是 4:3 了,所以我覺得這才是開發(fā)一個(gè)庫和一個(gè)應(yīng)用相比復(fù)雜的地方。

這里我的設(shè)計(jì)是使用策略接口提供接口給用戶進(jìn)行算法自定義。

1. CameraManager:相機(jī)的實(shí)現(xiàn)邏輯

對(duì)于 Camera1 和 Camera2 的邏輯,我們提供了 CameraManager 這個(gè)接口以及兩者分別的實(shí)現(xiàn) Camera1Manager 和 Camera2Manager,而兩者的實(shí)現(xiàn)又共同繼承自一個(gè)父類 BaseCameraManager. 因?yàn)槲覀兊膸煺w上還是將 CameraView 以一個(gè)控件的形式交付給用戶,所以 CameraView 中引用了 CameraManager. 可以說 CameraView 的每個(gè)方法都是直接或者間接調(diào)用了 CameraManager 的方法,因此 CameraManager 的方法非常多,我想一般人應(yīng)該不會(huì)想自己實(shí)現(xiàn)一個(gè) CameraManager 接口。所以,這個(gè)接口我不多介紹了。

2. CameraPreview:相機(jī)展示控件的邏輯

除了 Camera1 和 Camera2 的選擇,相機(jī)預(yù)覽控件也要在 TextureView 和 SurfaceView 之間進(jìn)行選擇。跟 CameraManager 一樣,我們提供了 CameraPreview 接口來滿足用戶的自定義需求。

在 CameraManager 和 CameraPreview 之上我們又定義了兩個(gè)工廠接口:CameraManagerCreator 和 CameraPreviewCreator,用戶可以通過實(shí)現(xiàn)這兩個(gè)接口并將其設(shè)置到 ConfigurationProvider 來實(shí)現(xiàn)定義自己的策略。

public interface CameraManagerCreator {

    /**
     * Method used to create {@link CameraManager}.
     *
     * @param context the context
     * @param cameraPreview the {@link CameraPreview}
     * @return CameraManager object.
     */
    CameraManager create(Context context, CameraPreview cameraPreview);
}

public interface CameraPreviewCreator {

    /**
     * Method used to create {@link CameraPreview}.
     *
     * @param context the context to create the preview view.
     * @param parent the parent view of the preview.
     * @return CameraPreview object.
     */
    CameraPreview create(Context context, ViewGroup parent);
}

比如,我們默認(rèn)提供的 CameraManagerCreator 的實(shí)現(xiàn)是:只要系統(tǒng) API≥21 并且支持 Camera2,我們就使用 Camera2Manager 作為自己的相機(jī)邏輯實(shí)現(xiàn)。如果你測試發(fā)現(xiàn)這無法滿足自己的需求,可以使用 ConfigurationProvider 的方法,將 Camera1OnlyCreator 作為自己的策略,這樣無論什么場景都使用 Camera1. 當(dāng)然,你也可以添加自定義的策略,只要實(shí)現(xiàn)自己的接口即可并塞到 ConfigurationProvider 中即可。

3. CameraSizeCalculator:綜合處理各種尺寸和寬高比的問題

與 CameraManager 類似,CameraSizeCalculator 是提供給用戶來自定義各種尺寸計(jì)算規(guī)則的接口。這個(gè)接口是我最近改動(dòng)比較大的接口,經(jīng)過調(diào)整之后接口內(nèi)的各個(gè)方法簡單清晰得多了:

public interface CameraSizeCalculator {

    void init(@NonNull AspectRatio expectAspectRatio,
              @Nullable Size expectSize,
              @MediaQuality int mediaQuality,
              @NonNull List<Size> previewSizes,
              @NonNull List<Size> pictureSizes,
              @NonNull List<Size> videoSizes);

    void changeExpectAspectRatio(@NonNull AspectRatio expectAspectRatio);
    void changeExpectSize(@Nullable Size expectSize);
    void changeMediaQuality(@MediaQuality int mediaQuality);

    Size getPictureSize(@CameraType int cameraType);
    Size getPicturePreviewSize(@CameraType int cameraType);
    Size getVideoSize(@CameraType int cameraType);
    Size getVideoPreviewSize(@CameraType int cameraType);
}

用戶可以通過這里的方法含義如下:

  • 我們通過 init() 方法將當(dāng)前系統(tǒng)相機(jī)的各種尺寸信息告知你
  • 當(dāng)用戶改變了相機(jī)的參數(shù)的時(shí)候,我們通過這里三個(gè)以 change 開頭的方法進(jìn)行通知
  • 這里的四個(gè)以 get 開頭的方法是需要你實(shí)現(xiàn)計(jì)算規(guī)則并返回計(jì)算結(jié)果的方法

這里我們要處理的參數(shù)包括:

  • 系統(tǒng)支持的所有預(yù)覽尺寸列表:previewSizes
  • 系統(tǒng)支持的所有照片尺寸列表:pictureSizes
  • 系統(tǒng)支持的所有視頻尺寸列表:videoSizes
  • 用戶期望得到的視頻或者照片的尺寸:expectSize
  • 用戶期望得到的視頻或者照片的寬高比:expectAspectRatio
  • 用戶期望得到的視頻或者照片的質(zhì)量:mediaQuality

該接口的一個(gè)默認(rèn)實(shí)現(xiàn)在 CameraSizeCalculatorImpl 中。在新版本中,我做了三處調(diào)整:

  • 增加了尺寸的緩存信息,用戶期望的尺寸、寬高比和輸出質(zhì)量不變的時(shí)候只需要計(jì)算一次,以后可以復(fù)用,來實(shí)現(xiàn)相機(jī)啟動(dòng)速率的提升

  • 實(shí)現(xiàn)了緩存的尺寸的隔離,之前我沒有將緩存的尺寸信息隔離開,導(dǎo)致在拍攝視頻和照片之間切換的時(shí)候出現(xiàn)畫面扭曲的問題,現(xiàn)在解決了這個(gè)問題

  • 增加了新的計(jì)算算法,目前該庫中包含兩種尺寸計(jì)算算法:

    • 第一種適用于輸出的圖片或者視頻的尺寸,我們會(huì)整合期望的輸出的尺寸、期望的輸出的寬高比和期望的輸出的圖片質(zhì)量來選擇一個(gè)最符合要求的輸出尺寸:首先尋找最接近期望的寬高比,然后尋找最接近的尺寸,如果沒有指定期望的尺寸會(huì)根據(jù)期望的輸出的質(zhì)量將所有支持尺寸劃分為不同的品質(zhì)之后選擇符合要求的尺寸。

    • 第二種算法適用于預(yù)覽的尺寸。相比于輸出的圖片和視頻的尺寸,預(yù)覽的尺寸可能沒那么重要。我們只需要尋找一個(gè)符合接近于輸出的圖片或者視頻的尺寸的尺寸即可。因此,這里的算法是,首先匹配寬高比,其次匹配尺寸。

具體實(shí)現(xiàn)可以參考源碼,這里就不再一一說明了。

3、細(xì)節(jié)的優(yōu)化與設(shè)計(jì)

3.1 枚舉在 iCamera 中的應(yīng)用

以相機(jī)的尺寸為例,它分為預(yù)覽的尺寸、輸出視頻的尺寸和輸出照片的尺寸,當(dāng)我們要對(duì)外暴露一個(gè)獲取尺寸的方法的時(shí)候,按照一般的思路勢必要提供三個(gè)方法。但是,我們可以通過整數(shù)+注解來取代枚舉,從而實(shí)現(xiàn)將三個(gè)方法合并未一個(gè)的目標(biāo)。比如,

    public Size getSize(@CameraSizeFor int sizeFor) {
        return cameraManager.getSize(sizeFor);
    }

    public SizeMap getSizes(@CameraSizeFor int sizeFor) {
        return cameraManager.getSizes(sizeFor);
    }

此外,我們?cè)趹?yīng)用中還定義了其他的枚舉,比如表示前置和后置相機(jī)的 CameraFace,表示 Camera1 還是 Camera2 的 CameraType 等,所以,在某些場合我們只需要進(jìn)行按位取或就可以實(shí)現(xiàn) hash 的鍵的區(qū)分,這樣設(shè)計(jì)使我們緩存隔離的時(shí)候的邏輯更加簡潔明了:

    public List<Size> getSizes(android.hardware.Camera camera,
                               @CameraFace int cameraFace,
                               @CameraSizeFor int sizeFor) {
        // calculate hash of map
        int hash = cameraFace | sizeFor | CameraType.TYPE_CAMERA1;
        XLog.d(TAG, "getSizes hash : " + Integer.toHexString(hash));
        // try to get sizes from cache first.
        if (useCacheValues) {
            List<Size> sizes = sizeMap.get(hash);
            if (sizes != null) {
                return sizes;
            }
        }
        // get sizes from parameters
        android.hardware.Camera.Parameters parameters = camera.getParameters();
        List<Size> sizes;
        switch (sizeFor) {
            case CameraSizeFor.SIZE_FOR_PICTURE:
                sizes = Size.fromList(parameters.getSupportedPictureSizes());
                break;
            case CameraSizeFor.SIZE_FOR_PREVIEW:
                sizes = Size.fromList(parameters.getSupportedPreviewSizes());
                break;
            case CameraSizeFor.SIZE_FOR_VIDEO:
                sizes = Size.fromList(parameters.getSupportedVideoSizes());
                break;
            default:
                throw new IllegalArgumentException("Unsupported size for " + sizeFor);
        }
        // cache the sizes in memory
        if (useCacheValues) {
            sizeMap.put(hash, sizes);
        }
        return sizes;
    }

3.2 應(yīng)用內(nèi)部的耗時(shí)分析

在進(jìn)行相機(jī)開發(fā)的時(shí)候我們?cè)趹?yīng)用內(nèi)部進(jìn)行了許多的耗時(shí)分析,比如之前用 TraceView 進(jìn)行方法調(diào)用的分析,以及使用各種 Log 輸出日志的方式來統(tǒng)計(jì)應(yīng)用耗時(shí)的邏輯。上也提到過我們使用預(yù)加載和緩存等來實(shí)現(xiàn)系統(tǒng)的啟動(dòng)速率的提升等。

Android 相機(jī) TraceView 分析

4、總結(jié)

在相機(jī)庫開發(fā)過程中還遇到一些其他的問題,比如橫豎屏切換和快門聲音處理等,這些都已經(jīng)反應(yīng)到了 iCamera 的源碼中。本來也想寫一下如何使用 Android 提供的 API 實(shí)現(xiàn)相機(jī)開發(fā)的,但是我覺得最好的教程就是源碼。既然項(xiàng)目已經(jīng)開源,直接讀源碼好了,并且我自認(rèn)為這個(gè)項(xiàng)目的代碼還是我比較滿意的。本來嘛,我們把別人的東西分析得頭頭是道但是卻做不成自己的東西,那分析了這么多又有什么用呢?其實(shí)比學(xué)習(xí)能力更高級(jí)的應(yīng)該是創(chuàng)造力。有人問我說,你寫這些開源項(xiàng)目和博客的目的是什么,我只能說,這是一種表達(dá),而生命本身就是一種表達(dá)。我們只不過是通過做這些事情來告訴這個(gè)世界,什么是對(duì)的,什么是美好的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,566評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,683評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,899評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,135評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評(píng)論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,528評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,844評(píng)論 2 372