基于 Android 系統(tǒng)方案適配 Night Mode 后,老板要再加一套皮膚贯吓?

背景說(shuō)明

原本已經(jīng)基于系統(tǒng)方案適配了暗黑主題懈凹,實(shí)現(xiàn)了白/黑兩套皮膚,以及跟隨系統(tǒng)悄谐。后來(lái)老板研究學(xué)習(xí)友商時(shí)介评,發(fā)現(xiàn)友商 App 有三套皮膚可選,除了常規(guī)的亮白和暗黑爬舰,還有一套暗藍(lán)色们陆。并且在跟隨系統(tǒng)暗黑模式下,用戶可選暗黑還是暗藍(lán)情屹。這不坪仇,新的需求馬上就來(lái)了。

其實(shí)我們之前兩個(gè) App 的換膚方案都是使用 Android-skin-support 來(lái)做的垃你,在此基礎(chǔ)上再加套皮膚也不是難事椅文。但在新的 App 實(shí)現(xiàn)多皮膚時(shí)喂很,由于前兩個(gè) App 做了這么久都只有兩套皮膚,而且新的 App 需要實(shí)現(xiàn)跟隨系統(tǒng)皆刺,為了更好的體驗(yàn)和較少的代碼實(shí)現(xiàn)少辣,就采用了系統(tǒng)方案進(jìn)行適配暗黑模式。

Android-skin-support 和系統(tǒng)兩種方案適配經(jīng)驗(yàn)來(lái)看羡蛾,系統(tǒng)方案適配改動(dòng)的代碼更少漓帅,所花費(fèi)的時(shí)間當(dāng)然也就更少了。所以在需要新添一套皮膚的時(shí)候痴怨,也不可能再去切方案了忙干。那么在使用系統(tǒng)方案的情況下,如何再加一套皮膚呢浪藻?來(lái)捐迫,先看源碼吧。

源碼分析

以下源碼基于 android-31

首先爱葵,在代碼中獲取資源一般通過(guò) Context 對(duì)象的一些方法弓乙,例如:

// Context.java

@ColorInt
public final int getColor(@ColorRes int id) {
    return getResources().getColor(id, getTheme());
}

@Nullable
public final Drawable getDrawable(@DrawableRes int id) {
    return getResources().getDrawable(id, getTheme());
}

可以看到 Context 是通過(guò) Resources 對(duì)象再去獲取的,繼續(xù)看 Resources

// Resources.java

@ColorInt
public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
    final TypedValue value = obtainTempTypedValue();
    try {
        final ResourcesImpl impl = mResourcesImpl;
        impl.getValue(id, value, true);
        if (value.type >= TypedValue.TYPE_FIRST_INT 
            && value.type <= TypedValue.TYPE_LAST_INT) {
            return value.data;
        } else if (value.type != TypedValue.TYPE_STRING) {
            throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)                                                                             + " type #0x" + Integer.toHexString(value.type) + " is not valid");
        }
        // 這里調(diào)用 ResourcesImpl#loadColorStateList 方法獲取顏色
        final ColorStateList csl = impl.loadColorStateList(this, value, id, theme);
        return csl.getDefaultColor();
    } finally {
        releaseTempTypedValue(value);
    }
}

public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) 
        throws NotFoundException {
    return getDrawableForDensity(id, 0, theme);
}

@Nullable
public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
    final TypedValue value = obtainTempTypedValue();
    try {
        final ResourcesImpl impl = mResourcesImpl;
        impl.getValueForDensity(id, density, value, true);
        // 看到這里
        return loadDrawable(value, id, density, theme);
    } finally {
        releaseTempTypedValue(value);
    }
}

@NonNull
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
        throws NotFoundException {
    // 這里調(diào)用 ResourcesImpl#loadDrawable 方法獲取 drawable 資源
    return mResourcesImpl.loadDrawable(this, value, id, density, theme);
}

到這里我們知道在代碼中獲取資源時(shí)钧惧,是通過(guò) Context -> Resources -> ResourcesImpl 調(diào)用鏈實(shí)現(xiàn)的。

先看 ResourcesImpl.java

/**
 * The implementation of Resource access. This class contains the AssetManager and all caches
 * associated with it.
 *
 * {@link Resources} is just a thing wrapper around this class. When a configuration change
 * occurs, clients can retain the same {@link Resources} reference because the underlying
 * {@link ResourcesImpl} object will be updated or re-created.
 *
 * @hide
 */
public class ResourcesImpl {
    ...
}

雖然是 public 的類勾习,但是被 @hide 標(biāo)記了浓瞪,意味著想通過(guò)繼承后重寫相關(guān)方法這條路行不通了,pass巧婶。

再看 Resources.java乾颁,同樣是 public 類,但沒(méi)被 @hide 標(biāo)記艺栈。我們就可以通過(guò)繼承 Resources 類英岭,然后重寫 Resources#getColorResources#getDrawableForDensity 等方法來(lái)改造獲取資源的邏輯。

先看相關(guān)代碼:

// SkinResources.kt

class SkinResources(context: Context, res: Resources) : Resources(res.assets, res.displayMetrics, res.configuration) {

    val contextRef: WeakReference<Context> = WeakReference(context)

    override fun getDrawableForDensity(id: Int, density: Int, theme: Theme?): Drawable? {
        return super.getDrawableForDensity(resetResIdIfNeed(contextRef.get(), id), density, theme)
    }

    override fun getColor(id: Int, theme: Theme?): Int {
        return super.getColor(resetResIdIfNeed(contextRef.get(), id), theme)
    }

    private fun resetResIdIfNeed(context: Context?, resId: Int): Int {
        // 非暗黑藍(lán)無(wú)需替換資源 ID
        if (context == null || !UIUtil.isNightBlue(context)) return resId

        var newResId = resId
        val res = context.resources
        try {
            val resPkg = res.getResourcePackageName(resId)
            // 非本包資源無(wú)需替換
            if (context.packageName != resPkg) return newResId

            val resName = res.getResourceEntryName(resId)
            val resType = res.getResourceTypeName(resId)
            // 獲取對(duì)應(yīng)暗藍(lán)皮膚的資源 id
            val id = res.getIdentifier("${resName}_blue", resType, resPkg)
            if (id != 0) newResId = id
        } finally {
            return newResId
        }
    }

}

主要原理與邏輯:

  • 所有資源都會(huì)在 R.java 文件中生成對(duì)應(yīng)的資源 id湿右,而我們正是通過(guò)資源 id 來(lái)獲取對(duì)應(yīng)資源的诅妹。
  • Resources 類提供了 getResourcePackageName/getResourceEntryName/getResourceTypeName 方法,可通過(guò)資源 id 獲取對(duì)應(yīng)的資源包名/資源名稱/資源類型毅人。
  • 過(guò)濾掉無(wú)需替換資源的場(chǎng)景吭狡。
  • Resources 還提供了 getIdentifier 方法來(lái)獲取對(duì)應(yīng)資源 id。
  • 需要適配暗藍(lán)皮膚的資源丈莺,統(tǒng)一在原資源名稱的基礎(chǔ)上加上 _blue 后綴划煮。
  • 通過(guò) Resources#getIdentifier 方法獲取對(duì)應(yīng)暗藍(lán)皮膚的資源 id。如果沒(méi)找到缔俄,改方法會(huì)返回 0弛秋。

現(xiàn)在就可以通過(guò) SkinResources 來(lái)獲取適配多皮膚的資源了器躏。但是,之前的代碼都是通過(guò) Context 直接獲取的蟹略,如果全部替換成 SkinResources 來(lái)獲取登失,那代碼改動(dòng)量就大了。

我們回到前面 Context.java 的源碼科乎,可以發(fā)現(xiàn)它獲取資源時(shí)壁畸,都是通過(guò) Context#getResources 方法先得到 Resources 對(duì)象,再通過(guò)其去獲取資源的茅茂。而 Context#getResources 方法也是可以重寫的捏萍,這意味著我們可以維護(hù)一個(gè)自己的 Resources 對(duì)象。ApplicationActivity 也都是繼承自 Context 的空闲,所以我們?cè)谄渥宇愔兄貙?getResources 方法即可:

// BaseActivity.java/BaseApplication.java

private Resources mSkinResources;

@Override
public Resources getResources() {
    if (mSkinResources == null) {
        mSkinResources = new SkinResources(this, super.getResources());
    }
    return mSkinResources;
}

到此令杈,基本邏輯就寫完了,馬上 build 跑起來(lái)碴倾。

咦逗噩,好像有點(diǎn)不太對(duì)勁,有些 colordrawable 沒(méi)有適配成功跌榔。

經(jīng)過(guò)一番對(duì)比异雁,發(fā)現(xiàn) xml 布局中的資源都沒(méi)有替換成功。

那么問(wèn)題在哪呢僧须?還是先從源碼著手纲刀,先來(lái)看看 View 是如何從 xml 中獲取并設(shè)置 background 屬性的:

// View.java

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    this(context);

    // AttributeSet 是 xml 中所有屬性的集合
    // TypeArray 則是經(jīng)過(guò)處理過(guò)的集合,將原始的 xml 屬性值("@color/colorBg")轉(zhuǎn)換為所需的類型担平,并應(yīng)用主題和樣式
    final TypedArray a = context.obtainStyledAttributes(
            attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);

    ...

    Drawable background = null;

    ...

    final int N = a.getIndexCount();
    for (int i = 0; i < N; i++) {
        int attr = a.getIndex(i);
        switch (attr) {
            case com.android.internal.R.styleable.View_background:
                // TypedArray 提供一些直接獲取資源的方法
                background = a.getDrawable(attr);
                break;
            ...
        }
    }

    ...

    if (background != null) {
        setBackground(background);
    }

    ...
}

再接著看 TypedArray 是如何獲取資源的:

// TypedArray.java

@Nullable
public Drawable getDrawable(@StyleableRes int index) {
    return getDrawableForDensity(index, 0);
}

@Nullable
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
    if (mRecycled) {
        throw new RuntimeException("Cannot make calls to a recycled instance!");
    }

    final TypedValue value = mValue;
    if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
        if (value.type == TypedValue.TYPE_ATTRIBUTE) {
            throw new UnsupportedOperationException(
                "Failed to resolve attribute at index " + index + ": " + value);
        }

        if (density > 0) {
            // If the density is overridden, the value in the TypedArray will not reflect this.
            // Do a separate lookup of the resourceId with the density override.
            mResources.getValueForDensity(value.resourceId, density, value, true);
        }
        // 看到這里
        return mResources.loadDrawable(value, value.resourceId, density, mTheme);
    }
    return null;
}

TypedArray 是通過(guò) Resources#loadDrawable 方法來(lái)加載資源的示绊,而我們之前寫 SkinResources 的時(shí)候并沒(méi)有重寫該方法,為什么呢暂论?那是因?yàn)樵摲椒ㄊ潜?@UnsupportedAppUsage 標(biāo)記的面褐。所以,這就是 xml 布局中的資源替換不成功的原因取胎。

這個(gè)問(wèn)題又怎么解決呢展哭?

之前采用 Android-skin-support 方案做換膚時(shí),了解到它的原理扼菠,其會(huì)替換成自己的實(shí)現(xiàn)的 LayoutInflater.Factory2摄杂,并在創(chuàng)建 View 時(shí)替換生成對(duì)應(yīng)適配了換膚功能的 View 對(duì)象。例如:將 View 替換成 SkinView循榆,而 SkinView 初始化時(shí)再重新處理 background 屬性析恢,即可完成換膚。

AppCompat 也是同樣的邏輯秧饮,通過(guò) AppCompatViewInflater 將普通的 View 替換成帶 AppCompat- 前綴的 View。

其實(shí)我們只需能操作生成后的 View,并且知道 xml 中寫了哪些屬性值即可唐断。那么我們完全照搬 AppCompat 這套邏輯即可:

  • 定義類繼承 LayoutInflater.Factory2,并實(shí)現(xiàn) onCreateView 方法帽撑。
  • onCreateView 主要是創(chuàng)建 View 的邏輯,而這部分邏輯完全 copy AppCompatViewInflater 類即可鞍时。
  • onCreateView 中創(chuàng)建 View 之后亏拉,返回 View 之前,實(shí)現(xiàn)我們自己的邏輯逆巍。
  • 通過(guò) LayoutInflaterCompat#setFactory2 方法及塘,設(shè)置我們自己的 Factory2。

相關(guān)代碼片段:

public class SkinViewInflater implements LayoutInflater.Factory2 {
    @Nullable
    @Override
    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        // createView 方法就是 AppCompatViewInflater 中的邏輯
        View view = createView(parent, name, context, attrs, false, false, true, false);
        onViewCreated(context, view, attrs);
        return view;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        return onCreateView(null, name, context, attrs);
    }

    private void onViewCreated(@NonNull Context context, @Nullable View view, @NonNull AttributeSet attrs) {
        if (view == null) return;
        resetViewAttrsIfNeed(context, view, attrs);
    }

    private void resetViewAttrsIfNeed(Context context, View view, AttributeSet attrs) {
        if (!UIUtil.isNightBlue(context)) return;

        String ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android";
        String BACKGROUND = "background";

        // 獲取 background 屬性值的資源 id锐极,未找到時(shí)返回 0
        int backgroundId = attrs.getAttributeResourceValue(ANDROID_NAMESPACE, BACKGROUND, 0);
        if (backgroundId != 0) {
            view.setBackgroundResource(resetResIdIfNeed(context, backgroundId));
        }
    }
}
// BaseActivity.java

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    SkinViewInflater inflater = new SkinViewInflater();
    LayoutInflater layoutInflater = LayoutInflater.from(this);
    // 生成 View 的邏輯替換成我們自己的
    LayoutInflaterCompat.setFactory2(layoutInflater, inflater);
}

至此笙僚,這套方案已經(jīng)可以解決目前的換膚需求了,剩下的就是進(jìn)行細(xì)節(jié)適配了灵再。

其他說(shuō)明

自定義控件與第三方控件適配

上面只對(duì) background 屬性進(jìn)行了處理肋层,其他需要進(jìn)行換膚的屬性也是同樣的處理邏輯。如果是自定義的控件翎迁,可以在初始化時(shí)調(diào)用 TypedArray#getResourceId 方法先獲取資源 id栋猖,再通過(guò) context 去獲取對(duì)應(yīng)資源,而不是使用 TypedArray#getDrawable 類似方法直接獲取資源對(duì)象汪榔,這樣可以確保換膚成功掂铐。而第三方控件也可通過(guò) background 屬性同樣的處理邏輯進(jìn)行適配。

XML <shape> 的處理

<!-- bg.xml -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="8dp" />
    <solid android:color="@color/background" />
</shape>

上面的 bg.xml 文件內(nèi)的 color 并不會(huì)完成資源替換揍异,根據(jù)上面的邏輯,需要新增以下內(nèi)容:

<!-- bg_blue.xml -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="8dp" />
    <solid android:color="@color/background_blue" />
</shape>

如此爆班,資源替換才會(huì)成功衷掷。

設(shè)計(jì)的配合

這次對(duì)第三款皮膚的適配還是蠻輕松的,主要是有以下基礎(chǔ):

  • 在適配暗黑主題的時(shí)候柿菩,設(shè)計(jì)有出設(shè)計(jì)規(guī)范戚嗅,后續(xù)開(kāi)發(fā)按照設(shè)計(jì)規(guī)范來(lái)。
  • 暗黑和暗藍(lán)共用一套圖片資源枢舶,大大減少適配工作量懦胞。
  • 暗黑和暗藍(lán)部份共用顏色值含透明度,同樣減少了工作量凉泄,僅少量顏色需要新增躏尉。

這次適配的主要工作量還是來(lái)自 <shape> 的替換。

暗藍(lán)皮膚資源文件的歸處

我知道很多換膚方案都會(huì)將皮膚資源制作成皮膚包后众,但是這個(gè)方案沒(méi)有這么做胀糜。一是沒(méi)有那么多需要替換的資源颅拦,二是為了減少相應(yīng)的工作量。

我新建了一個(gè)資源文件夾教藻,與 res 同級(jí)距帅,取名 res-blue。并在 gradle 配置文件中配置它括堤。編譯后系統(tǒng)會(huì)自動(dòng)將它們合并碌秸,同時(shí)也能與常規(guī)資源文件隔離開(kāi)來(lái)。

// build.gradle
sourceSets {
    main {
        java {
            srcDir 'src/main/java'
        }
        res.srcDirs += 'src/main/res'
        res.srcDirs += 'src/main/res-blue'
    }
}

有哪些坑悄窃?

WebView 資源缺失導(dǎo)致閃退

版本上線后讥电,發(fā)現(xiàn)有 android.content.res.Resources$NotFoundException 異常上報(bào),具體異常堆棧信息:

android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:321)
android.content.res.Resources.getInteger(Resources.java:1279)
org.chromium.ui.base.DeviceFormFactor.b(chromium-TrichromeWebViewGoogle.apk-stable-447211483:4)
org.chromium.content.browser.selection.SelectionPopupControllerImpl.n(chromium-TrichromeWebViewGoogle.apk-stable-447211483:1)
N7.onCreateActionMode(chromium-TrichromeWebViewGoogle.apk-stable-447211483:8)
Gu.onCreateActionMode(chromium-TrichromeWebViewGoogle.apk-stable-447211483:2)
com.android.internal.policy.DecorView$ActionModeCallback2Wrapper.onCreateActionMode(DecorView.java:3255)
com.android.internal.policy.DecorView.startActionMode(DecorView.java:1159)
com.android.internal.policy.DecorView.startActionModeForChild(DecorView.java:1115)
android.view.ViewGroup.startActionModeForChild(ViewGroup.java:1087)
android.view.ViewGroup.startActionModeForChild(ViewGroup.java:1087)
android.view.ViewGroup.startActionModeForChild(ViewGroup.java:1087)
android.view.ViewGroup.startActionModeForChild(ViewGroup.java:1087)
android.view.ViewGroup.startActionModeForChild(ViewGroup.java:1087)
android.view.ViewGroup.startActionModeForChild(ViewGroup.java:1087)
android.view.View.startActionMode(View.java:7716)
org.chromium.content.browser.selection.SelectionPopupControllerImpl.I(chromium-TrichromeWebViewGoogle.apk-stable-447211483:10)
Vc0.a(chromium-TrichromeWebViewGoogle.apk-stable-447211483:10)
Vf0.i(chromium-TrichromeWebViewGoogle.apk-stable-447211483:4)
A5.run(chromium-TrichromeWebViewGoogle.apk-stable-447211483:3)
android.os.Handler.handleCallback(Handler.java:938)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loopOnce(Looper.java:233)
android.os.Looper.loop(Looper.java:334)
android.app.ActivityThread.main(ActivityThread.java:8333)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:582)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1065)

經(jīng)查才發(fā)現(xiàn)在 WebView 中長(zhǎng)按文本彈出操作菜單時(shí)广匙,就會(huì)引發(fā)該異常導(dǎo)致 App 閃退允趟。

這是其他插件化方案也踩過(guò)的坑,我們只需在創(chuàng)建 SkinResources 之前將外部 WebView 的資源路徑添加進(jìn)來(lái)即可鸦致。

@Override
public Resources getResources() {
    if (mSkinResources == null) {
        WebViewResourceHelper.addChromeResourceIfNeeded(this);
        mSkinResources = new SkinResources(this, super.getResources());
    }
    return mSkinResources;
}

RePlugin/WebViewResourceHelper.java 源碼文件

具體問(wèn)題分析可參考

Fix ResourceNotFoundException in Android 7.0 (or above)

最終效果圖

總結(jié)

這個(gè)方案在原本使用系統(tǒng)方式適配暗黑主題的基礎(chǔ)上潮剪,通過(guò)攔截 Resources 相關(guān)獲取資源的方法,替換換膚后的資源 id分唾,以達(dá)到換膚的效果抗碰。針對(duì) XML 布局換膚不成功的問(wèn)題,復(fù)制 AppCompatViewInflater 創(chuàng)建 View 的代碼邏輯绽乔,并在 View 創(chuàng)建成功后重新設(shè)置需要進(jìn)行換膚的相關(guān) XML 屬性弧蝇。同一皮膚資源使用單獨(dú)的資源文件夾獨(dú)立存放,可以與正常資源進(jìn)行隔離折砸,也避免了制作皮膚包而增加工作量看疗。

目前來(lái)說(shuō)這套方案是改造成本最小,侵入性最小的選擇睦授。選擇適合自身需求的才是最好的两芳。

作者:ONEW
鏈接:https://juejin.cn/post/7187282270360141879

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市去枷,隨后出現(xiàn)的幾起案子怖辆,更是在濱河造成了極大的恐慌,老刑警劉巖删顶,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件竖螃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡逗余,警方通過(guò)查閱死者的電腦和手機(jī)特咆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)录粱,“玉大人坚弱,你說(shuō)我怎么就攤上這事蜀备。” “怎么了荒叶?”我有些...
    開(kāi)封第一講書人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵碾阁,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我些楣,道長(zhǎng)脂凶,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任愁茁,我火速辦了婚禮蚕钦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鹅很。我一直安慰自己嘶居,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布促煮。 她就那樣靜靜地躺著邮屁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪菠齿。 梳的紋絲不亂的頭發(fā)上佑吝,一...
    開(kāi)封第一講書人閱讀 52,713評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音绳匀,去河邊找鬼芋忿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛疾棵,可吹牛的內(nèi)容都是我干的戈钢。 我是一名探鬼主播,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼是尔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼逆趣!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起嗜历,我...
    開(kāi)封第一講書人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎抖所,沒(méi)想到半個(gè)月后梨州,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡田轧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年暴匠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片傻粘。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡每窖,死狀恐怖帮掉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情窒典,我是刑警寧澤蟆炊,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站瀑志,受9級(jí)特大地震影響涩搓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜劈猪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一昧甘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧战得,春花似錦充边、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至刮吧,卻和暖如春湖饱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背杀捻。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工井厌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人致讥。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓仅仆,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親垢袱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子墓拜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

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