【Gradle深入淺出】——Gralde配置(二)

系列目錄

1.【Gradle深入淺出】——初識Gradle
2.【Gradle深入淺出】——Gradle基礎概念
3.【Gradle深入淺出】——Android Gradle Plugin 基礎概念
4.【Gradle深入淺出】——Gradle配置(一)
5.【Gradle深入淺出】——Gralde配置(二)

一、前言

前一篇博客分析了Gralde配置的三個文件local.properties、gradle.properties、setting.gradle,本篇博客開始分析gradle的工程配置,這些配置參數都是影響我們平時實際工程打包的參數,所以還是很有必要了解的。首先我們要知道gradle打包的核心配置在build.gradle文件,而關于build.gradle父工程和子工程的差異這里就不再說了,前面的博客已有介紹,所以有了前面博客的介紹,我們知道我們工程的打包首先要看父項目的打包配置的build.gralde.

二、Root工程的Build.gradle配置

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        google()
        jcenter()
        maven {
            url('http://xxxxxx')
        }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

我們初始化創建的工程的build.gradle還是比較清晰簡單的,首先看到的是buildscript,看到內部的內容我們一開始可能會很困惑,這里為什么有repositoriesdependencies的定義,我們每個子工程的build.gradle中也有關于repositoriesdependencies的定義,這兩個有什么區別呢?
這里buildScript正如名字的含義那樣,表示的是gradle腳本編譯過程中需要的資源,比如我們在gradle打包時用到了一些外部依賴項或者第三方插件,那么這時我們就需要在buildScript中聲明相關的repo倉庫地址,并且在dependencies中聲明相應的依賴項。而在其他工程中的build.gradle中聲明的repo和denpencies表示的是打包的項目所依賴的資源,也就是我們工程用到的依賴的資源。
repositories表示的是dependencies的repo地址,我們看下配置的源碼,我們一般會配置google(),jcenter(),maven這幾種,簡單看下源碼。

   /**
     * Adds a repository which looks in Google's Maven repository for dependencies.
     * <p>
     * The URL used to access this repository is {@literal "https://dl.google.com/dl/android/maven2/"}.
     * <p>
     * Examples:
     * <pre autoTested="">
     * repositories {
     *     google()
     * }
     * </pre>
     *
     * @return the added resolver
     * @since 4.0
     */
    @Incubating
    MavenArtifactRepository google();

    /**
     * Adds a repository which looks in Bintray's JCenter repository for dependencies.
     * <p>
     * The URL used to access this repository is {@literal "https://jcenter.bintray.com/"}.
     * The behavior of this repository is otherwise the same as those added by {@link #maven(org.gradle.api.Action)}.
     * <p>
     * Examples:
     * <pre autoTested="">
     * repositories {
     *     jcenter()
     * }
     * </pre>
     *
     * @return the added resolver
     * @see #jcenter(Action)
     */
    MavenArtifactRepository jcenter();

    /**
     * Adds a repository which looks in the local Maven cache for dependencies. The name of the repository is
     * {@value org.gradle.api.artifacts.ArtifactRepositoryContainer#DEFAULT_MAVEN_LOCAL_REPO_NAME}.
     *
     * <p>Examples:</p>
     * <pre autoTested="">
     * repositories {
     *     mavenLocal()
     * }
     * </pre>
     * <p>
     * The location for the repository is determined as follows (in order of precedence):
     * </p>
     * <ol>
     * <li>The value of system property 'maven.repo.local' if set;</li>
     * <li>The value of element &lt;localRepository&gt; of <code>~/.m2/settings.xml</code> if this file exists and element is set;</li>
     * <li>The value of element &lt;localRepository&gt; of <code>$M2_HOME/conf/settings.xml</code> (where <code>$M2_HOME</code> is the value of the environment variable with that name) if this file exists and element is set;</li>
     * <li>The path <code>~/.m2/repository</code>.</li>
     * </ol>
     *
     * @return the added resolver
     */
    MavenArtifactRepository mavenLocal();

所以可以看到google(),jcenter(),mavenLocal()就表示我們引入了三個地址的倉庫,下載denpendencies庫的時候就會按照順序從這幾個url中找是否有對應的庫,如果有則會下載下來。
那么如何指定本地路徑作為repo地址呢?我們在開發gradle插件的時候可能會遇到這個問題。

   maven{
            url 'file://E:/libs/localMaven/'
        }

接著往下看,會看到脫離了buildScript的代碼塊,也就是說明現在的配置不是用于gradle編譯使用了,而且真正打包使用的配置了。

allprojects {
    repositories {
        google()
        jcenter()
    }
}

正如名字allprojects一樣,這個就是我們統一為所有的子project配置的地方,具體為什么語法糖是這樣的,后面會專門開一篇博客講一下關于這里的語法糖,這里如果有困惑的我們其實可以把代碼改成這樣。

allprojects {
    println "this.name:" + name
    repositories {
        google()
        jcenter()
    }
}

這樣我們就會發現這其實是一個for循環,會將我們依賴的子project都打印出來,所以這里配置的所有配置都會在全局的project生效,而如果子project在build.gralde的配置會覆蓋這里的配置,所以我們如果做全局配置,就可以在這里進行統一配置。
接下來就是一個統一的配置,可以看到定義了一個task,關于task的定義前面已經講到過,這里可以看到定義了一個clean的task,可以看到clean這個task繼承了系統的delete的task,所以我們可以在task內部用到delete的功能,而clean的作用也很簡單,就是把/projectDir/build目錄給清理了,所以我們有時候在編譯打包時有時候會發現自己的一個改動沒有生效,很有可能就是緩存的問題,先clean一下再build就好了。
這樣根目錄的build.gradle就分析完了,總體來看下根目錄的build.gradle會做哪些事情:

  • 構建腳本的配置,例如gradle插件的版本,構建依賴的repo,注意這里都是和構建有關的
  • 定義全局配置,可以定義所有子project的配置
  • 定義了一個clean的task,用于清理緩存目錄
    接下來來看下主project的build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.xuan.studydemo"
        minSdkVersion 15
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    def lifecycle_version = "2.2.0"
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.core:core-ktx:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
}

首先看到的是apply plugin,這個就是gradle中的插件,關于插件的自定義我們后面再詳細講解,但對于插件的定義我們要在這里先了解下,不然后面對于流程會有些不理解。前面一篇博客有講解關于插件的基礎概念,這里再結合這里講一下。

gradle的本質是一個流程控制框架,提供了基礎的task和方便的流程控制的語法糖,而AGP是Google以Gradle為基礎,并且結合Android的打包流程,專門開發的一個插件。

所以簡單的理解,就是Google將Android的打包流程,例如編譯、資源合并、混淆、簽名等一系列的打包流程開發成一個個Gradle中的Task,而AGP再將這些Task整合成流程,利用Gradle方便的流程控制和配置打包,整合成一個AGP插件。
所以可以看到我們經常會看到apply plugin: 'com.android.application',apply plugin: 'com.android.library',這其實就是AGP中定義的插件,因為依賴了這些插件,所以有了下面的android的配置,我們可以試下將apply plugin: 'com.android.application'注釋掉,這時候再build就會發現這樣的錯誤的信息。

Could not find method android() for arguments [build_kmftzroc9bwua5m7q41tme6p$_run_closure1@47c045f] on project ':app' of type org.gradle.api.Project.

這也就是說明android()這個配置其實不是gradle自帶的,而是AGP中新增的。接下來來看看android的配置。
compileSdkVersion
表示的Gradle用哪個版本的AndroidSDK編譯項目,所以當我們使用新版本的API時就需要使用對應版本的AndroidSDK,而compileSdkVersion只影響編譯時行為,不影響運行時的行為,如果我們想要使用最新版本的的API,就可以將compileSdkVersion升級到對應的版本,并且如果有老的API過期,在編譯時就會警告或者報錯。
buildToolsVersion
表示的編譯工具集的版本號,例如在打包時使用的aapt,dx等工具版本,都是根據這里的版本制定的版本來尋找的。也就是我們構建項目需要的工具集的版本號,一般是在sdk/build-tools/目錄中,如果說剛才說到的compileSdkVersion表示的是我們使用的AndroidSDK版本,那么buildToolsVersion就是我們的編譯工具的版本,然后使用這個編譯工具配合AndroidSDK來編譯工程項目,所以一般buildToolsVersion的版本會和compileSdkVersion大版本相同。
minSdkVersion
表示的是我們的應用程序運行所需要的最低版本號,如果手機系統版本大于這個版本,那么Android系統會阻止應用安裝應用,而如果我們沒有顯示的聲明minSdkVersion,那么默認值就是1,也就是可以運行在所有的Android機器上。
targetSdkVersion
正如字面意思,表示的是我們應用的目標版本,所以也是最重要的和我們應用適配有關的字段,在targetSdkVersion設置的版本表示我們已經對當前版本及以下的版本充分適配測試,所以能夠兼容小于等于targetSdkVersion所有的手機,所以這個也是Android系統向前兼容的判斷依據。比如我們設置的targetSdkVersion版本是26,而如果我們運行項目在一個28版本系統的手機上,Android系統判定我們應用的targetSdkVerison是26,就會向前兼容,使用API時就會使用26及26版本以下的API,如果某個API在28上有變更,我們的應用就不會體現出新特性。

所以綜上所述,我們應用的版本關系應該是:

minSdkVersion<=targetSdkVersion<=compileSdkVersion(buildToolsVersion)

defaultConfig

接下來來看下defaultConfig的配置,首先看下這個是用來配置什么東西的,當然最便捷的就是先看AGP的源碼。

/**
     * Specifies defaults for variant properties that the Android plugin applies to all build
     * variants.
     *
     * <p>You can override any <code>defaultConfig</code> property when <a
     * >
     * configuring product flavors</a>.
     *
     * <p>For more information about the properties you can configure in this block, see {@link
     * ProductFlavor}.
     */
    public void defaultConfig(Action<DefaultConfig> action) {
        checkWritability();
        action.execute(defaultConfig);
    }

這里我們先忽略下為什么這里是一個方法,這個其實涉及到Gradle的一個特性,后續講Gradle源碼的時候會專門分析,這里先看注釋來看下是做什么作用的,在這個系列的第三篇博客我們有提到過關于AGP中buildType,Flavor,defaultConfig的區別和概念,這里再來看應該就清晰很多,其實defaultConfig就是對于不同的產物類型的默認配置,而如果我們在buildType或者Flavor中配置,那么就會覆蓋defaultConfig里的配置。具體的順序之前也有提到,這里在說明一下:

按優先級從高到低: buildType->Flavor->defaultConfig
首先來看下defaultConfig的配置方式,defaultConfig和Gradle之前的配置一樣,支持語法糖和方法調用兩種形式

defaultConfig {
  minSdkVersion 15
}

//等價于
defaultConfig {
  minSdkVersion(15)
}

接下來來看下核心屬性,這里只列舉幾個我認為比較常見,其他的這里有可以查看Google的官方文檔,或者這里有一篇博客講的也比較詳細。

  • applicationId
    應用的id,也就是我們常說的包名,Android中的應用包名是唯一的,所以我們是用包名來區分一個唯一的應用的。
  • comsumerProguardFiles\consumerProguardFile\proguardFiles
defaultConfig {
    consumerProguardFiles 'consumer-rules.pro'
}

// 因為該屬性是一個 List<File> 類型,如果需要多個文件配置,則如下所示
defaultConfig {
    consumerProguardFiles 'consumer-rules.pro','consumer-test-rules.pro'
}

proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

首先說下comsumerProguardFiles,很多博客有說到一個概念

這個屬性只作用于我們創建的library中,也就是不是在我們的主項目中配置使用的,它是負責配置library被編譯打包時使用的混淆規則的。

但其實我感覺這里這樣描述是有錯誤或者說有歧義的,接下來我們來分析下這個問題。
首先看下proguardFiles這個屬性,這個是用于配置混淆屬性的,也就是我們平時打包時當開啟了minifyEnabled屬性時,這時候Android打包的過程中,在混淆時就會讀取我們通過proguardFiles配置的文件列表,來進行相應的混淆配置和防混淆配置。這個屬性適用于App也同樣適用于library,也就是我們如果打一個aar包時,也可以通過配置proguardFiles屬性來進行混淆。哪可能有人要問來,哪comsumerProguardFiles是用于干什么的呢?
我們在使用一些三方開源庫,可能會發現一種情景,庫的Uage要求不僅僅需要通過maven依賴,還需要手動寫入一些keep文件,這時就會發現很麻煩,為什么三方庫的混淆配置需要我們手動加到我們自己工程里的,不能在三方庫內部配置好嗎?
comsumerProguardFiles就是用于這種場景的,我們在通過comsumerProguardFiles配置了混淆配置后,我們打出的release的aar,我們解壓后就會發現里面包含一個proguard.txt文件,而這個文件內容其實就是我們的comsumerProguardFiles配置,也就是我們的混淆配置會被打入aar文件中,而proguardFiles就不會有這樣的效果。
然后在App引入aar后,Android在進行apk打包的時候,就會對proguard.txt進行合并,然后最終在apk打包混淆時對全部文件生效。
這時候我們就可以得出幾個結論:

comsumerProguardFiles用于用于當三方庫被App依賴時,不僅僅會對三方庫生效,也會對全局的Java代碼生效
proguardFiles僅用于當次編譯混淆時使用,并不會被持久化到aar中,所以我們在aar配置的proguardFiles只會當前aar打包的混淆時有效,并不會被寫入proguard.txt文件中,也就是aar中的proguardFiles配置不會影響接入的Apk的混淆。

這里大家可以測試一下,如果在一個aar的comsumerProguardFiles配置,增加一個-dontobfuscate表示不進行混淆,然后打出一個aar包,用App項目依賴這個aar,就會發現我們在app中就算打開了混淆,也會失效,就是因為我們在aar的comsumerProguardFiles配置了-dontobfuscate這個屬性,最終被合并到apk打包的proguard.txt文件中,對全部的Java文件進行生效了,所以就導致全局的混淆都被關閉,這樣看這個配置還是要慎重的。

  • javaCompileOptions
    配置編譯時 java 的一些參數,例如我們使用 annotationProcessor 時所需要的參數。
defaultConfig {
    javaCompileOptions {
        annotationProcessorOptions{
            arguments = []
            classNames ''
            ....
        }
    }
    ......省略其他配置
}
  • ndk
    用于配置abi過濾,配置我們打出來的apk支持什么架構類型的CPU
defaultConfig {
    // ndk中,目前只有 abiFilter 一個屬性,所以 ndk 目前來說只用于 abi 的過濾
    ndk {
        abiFilter 'armeabi-v7a'
    }
    ...
}
  • versionCode/versionName
    這里兩個的區別網上其實挺多講解的,但其實可以簡單理解就是:versionName就是一個名字,是一個字符串,沒任何作用,僅給用戶展示區分使用,versionCode是最重要的參數,是一個int值,應用市場的更新/判斷新舊包都是用這個,對開發者透明,不對用戶透露。可以出現一個versionName對應多個versionCode,但是最好不要這樣使用,因為這樣很容易導致版本亂了,后續的運營數據分析,線上問題修復,版本升級,渠道管理都可能會有問題,所以最好的就是一對一的關系。
    最后介紹一個defaultConfig的兩個特性,用于動態生成變量使用。
    首先是我們在defaultConfig配置的屬性,在構建時都會在對應的產物變體目錄下生成對應的BuildConfig.java文件,此文件會講我們之前配置的屬性變成對應的Java常量,這樣我們就可以在Java代碼中進行使用。
public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.xuan.studydemo";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
}

而如果我們想在編譯時自定義一些相應的屬性變量,這時候就可以用buildConfigField(type,name,value)方法,用于向構建時生成BuildConfig.java類中新增屬性。例如:

defaultConfig {
    ...
    // 是否是Monkey包
    isMonkey = project.hasProperty('isMonkey') ? isMonkey : 'false'
    buildConfigField "boolean", "IS_MONKEY", isMonkey
    
    // 添加模塊對應的模塊名
    buildConfigField "String", "MODULE_NAME", "\"${project.name}\""
}

對應我們如果編譯后,就會在產物目錄中發現BuildConfig.java類中就有我們相應的變量定義。

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.xuan.studydemo";
  public static final String BUILD_TYPE = "debug";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
  // Fields from default config.
  public static final boolean IS_MONKEY = true;
  public static final String MODULE_NAME = "app";
}

這樣我們就可以在我們的代碼邏輯中加入相關的判斷,例如Monkey包可以不顯示某些入口,直接登錄等等。
除了buildConfigField(type,name,value)方法外,還有一個方法也是用于在編譯時新增資源的。resValue(String type, String name, String value),這個方法相當于在res/values中新增一個資源。例如:

buildTypes {
        debug {
            resValue("String", "app_name_monkey", "Monkey for App")
        }
    }

這樣我們在編譯后,就會生成一個資源文件。
而我們如果要在代碼中使用這個資源,就可以通過下面的方式來獲取。

getResources().getString(R.string.app_name_monkey);

最后是關于buildTypes的介紹,其實關于buildTypesproductFlavors的區別,前面已經有一篇博客比較詳細的介紹,這里再簡單總結下,buildType就是我們的構建類型,比如debug包,release包,monkey包,不同的構建類型有不同的構建方式,比如debug包一般不會混淆,monkey包一般會加入一些apm的操作和庫,release一般會開啟混淆和簽名。而productFlavors就是指的產物類型,比如我們打的不同的渠道包,比如內部包,GooglePlay包,國內市場包,而buildTypesproductFlavors會以排列的方式進行匯總,所以對應的就是n*m的關系,比如GooglePlay版的Debug包/release包,等等。對應的這個最終的產物就是我們的BuildVariant,所以對應到一個公示:

BuildVariant = ProductFlavor x BuildType

就很好理解這些了,而這些對應的配置優先級順序就是:

按優先級從高到低: buildType->Flavor->defaultConfig
接下來來看下里面常用的一些屬性。

  • shrinkResources
    shrinkResources是google官方提供的優化無用資源的配置,shrinkResources和minifyEnabled必須同時開啟才有效,這里具體shrinkResources的實現細節就不展開了,后續有時間可以專門寫篇博客學習分析下,但是這里說幾個這個屬性特殊的地方。

shrinkResources中被移除的資源是真正被刪除了嗎?

這里先說下結論,開啟后無用的資源和圖片并沒有被真正的移除掉,而是用了一個同名的占位符號,具體我們可以自己試下。

shrinkResources會將所有無用的資源都移除嗎?

這個也是不會的,shrinkResources在嚴格模式下,其實是會檢測我們代碼中靜態聲明的一些字符串,如果圖片中有命中我們定義的字符串,就算資源沒有被使用,但是也不會被移除,具體這里有篇博客比較詳細的講解了。

  • minifyEnabled
    當設置為true的時候,就會開啟代碼混淆,壓縮apk,還會對資源進行壓縮,對無用的代碼和多余的代碼在編譯打包的時候就會被移除掉,而具體的混淆規則配置前面也有提到,就是我們關于proguard.pro的配置。

如何方式資源被混淆移除

我們可以在res/raw/keep.xml(避免被誤刪除)配置。例如:

<?xml version="1.0" encoding="utf-8"?>  
<resources xmlns:tools="http://schemas.android.com/tools"  
  tools:keep="@layout/activity_main,@drawable/comfirm_bg"/>  

代碼混淆的結果和細節如何追溯?

代碼混淆生成apk之后,項目下面會多出來一個proguard文件夾,proguard文件夾中四個文件的作用。
dump.txt : 描述了apk中所有類文件中內部的結構體。

mapping.txt : 列出了原始的類、方法和名稱與混淆代碼間的映射。

seeds.txt : 列出了沒有混淆的類和方法。

usage.txt : 列出congapk中刪除的代碼。

  • signingConfig
    從字面知道有 “簽署配置” 的意思。該配置的作用,就是為編譯出來的apk簽上我們的“名字”,這樣才能將apk發布安裝到用戶的設備上。設備(手機、TV等)對 apk 的唯一認定,并不只是包名,而是 包名和簽名,其中一項不同,都會認為這個 apk 包是不同的。

包名的不同,表現為多個應用。簽名的不同,在應用升級時表現為無法安裝,如果是第一次安裝,則不會有問題。

具體可以看下這篇博客

  • resConfigs
    作用是指定打包時編譯的語言包類型,未指定的其他語言包,將不會打包到apk文件中,從而減少apk體積的大小。

最后介紹下關于依賴的內容,這里但從使用層面介紹下幾種常用依賴的關鍵字的區別:
首先介紹下gralde不同版本下的幾個關鍵字的區別:
compile依賴關系已被棄用,被implementation和api替代;provided被compileOnly替代;

  • implementation
    與compile對應,會添加依賴到編譯路徑,并且會將依賴打包到輸出(aar或apk),但是在編譯時不會將依賴的實現暴露給其他module,也就是只有在運行時其他module才能訪問這個依賴中的實現。使用這個配置,可以顯著提升構建時間,因為它可以減少重新編譯的module的數量。建議,盡量使用這個依賴配置。
  • api
    與compile對應,功能完全一樣,會添加依賴到編譯路徑,并且會將依賴打包到輸出(aar或apk),與implementation不同,這個依賴可以傳遞,其他module無論在編譯時和運行時都可以訪問這個依賴的實現,也就是會泄漏一些不應該不使用的實現。舉個例子,A依賴B,B依賴C,如果都是使用api配置的話,A可以直接使用C中的類(編譯時和運行時),而如果是使用implementation配置的話,在編譯時,A是無法訪問C中的類的。
  • compileOnly
    與provided對應,Gradle把依賴加到編譯路徑,編譯時使用,不會打包到輸出(aar或apk)。這可以減少輸出的體積,在只在編譯時需要,在運行時可選的情況,很有用。
  • annotationProcessor
    與compile對應,用于注解處理器的依賴配置。

三、總結

本篇博客介紹了gradle的常用配置,至此關于Gradle的基礎知識準備告于段落了,雖然沒有很全面的把Gradle基礎配置都包含進來,但5篇博客下來基本上對于Gradle的基礎概念有了一個大體的認識和思路,所以接下來準備從源碼角度來學習Gradle,為什么Gradle支持這樣的語法糖,AGP的打包流程是什么樣的,依賴關系是如何打入APK中的,等等問題都需要我們從Gradle和AGP的源碼角度來學習,所以接下來的博客就結合源碼來繼續學習Gradle。

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

推薦閱讀更多精彩內容