Tinker-使用教程与原理分析(上)

前言

前面我们讲解了AndFix的使用,这篇我们来讲解下微信的Tinker热修复,相比AndFix,Tinker的功能更加全面,更主要的是他支持gradle。他不仅做到了热修复更实现了“热更新”。既然他这么强大,下面我们就来了解他是如何使用的。


命令行生成补丁文件

在学习AndFix时由于它不自持Gradle,所以我们在生成补丁文件时是需要命令行去生成的。然而Tinker不仅支持Gradle同时也支持命令行生成补丁文件。不过在实际开发中,我们往往是使用Gradle去生成补丁文件,同时去配置一些需要的参数与属性。不过既然我们想详细了解它那么我们还是讲解下命令行生成补丁文件。

建议:无论学习什么技术,以官方文档为主,教程文章为辅。这样会好一些。

  1. 引入依赖
·
    //注解库 用于生成application类 provided编译不打包
    provided('com.tencent.tinker:tinker-android-anno:1.7.7'){ changing = true }
    //是否将依赖关系标记为正在改变
    //tinker的核心库 compile编译并打包
    compile('com.tencent.tinker:tinker-android-lib:1.7.7'){ changing = true }


命令行相对简单。首先我们要引入两个依赖。

  1. 创建ApplicationLike代理类
    这里我们创建TinkerManager来实现对Tinker的管理。
    TinkerManager:
/**
 * 功能   :Tinker管理类
 */

public class TinkerManager {

    private static boolean isInstalled = false;//是否已经初始化标志位
    private static ApplicationLike mApplicationLike;

    /**
     * 完成Tinker初始化
     *
     * @param applicationLike
     */
    public static void installedTinker(ApplicationLike applicationLike) {
        mApplicationLike = applicationLike;
        if (isInstalled) {
            return;
        }
        TinkerInstaller.install(mApplicationLike);
        isInstalled = true;
    }

    /**
     * 完成patch文件的加载
     *
     * @param path 补丁文件路径
     */
    public static void loadPatch(String path) {
        if (Tinker.isTinkerInstalled()) {//是否已经安装过
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
        }
    }

    /**
     * 利用Tinker代理Application 获取应用全局的上下文
     * @return 全局的上下文
     */
    private static Context getApplicationContext() {
        if (mApplicationLike != null)
            return mApplicationLike.getApplication().getApplicationContext();
        return null;
    }
}


这里我们不是自己创建Application。而是使用Tinker为我们提供的ApplicationLike(也可以继承DefaultApplicationLike),作用已经有注释了。同时必须实现它的构造方法。并且我们重写了onBaseContextAttached这个方法并在里面初始化Tinker。完成这个后我们需要同步,然后就会生成MyTinkerApplication这个类(同步后没有出现这个类可以rebuild项目)。代码如下:

`
/**
 * 功能   :ApplicationLike为Tinker生成Context对象 官方建议 而不是继承我们自己的Application
 * 作用   :使用这个ApplicationLike这个类作为Application的委托代理是因为,Tinker需要监听Application
 * 的生命周期并针对不同的生命周期来做相应的初始化与处理,这样就减少使用者需要自己处理。
 */
@DefaultLifeCycle(application = ".MyTinkerApplication" ,
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)//都是官方要求这么写的
public class CustomTinkerLike extends ApplicationLike {
    public CustomTinkerLike(Application application,
                            int tinkerFlags,
                            boolean tinkerLoadVerifyFlag,
                            long applicationStartElapsedTime,
                            long applicationStartMillisTime,
                            Intent tinkerResultIntent) {
        super(application,
                tinkerFlags,
                tinkerLoadVerifyFlag,
                applicationStartElapsedTime,
                applicationStartMillisTime,
                tinkerResultIntent);
    }

    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);

        TinkerManager.installedTinker(this);
    }
}

3.在Manifest.xml中配置

·
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.ggxiaozhi.tinker">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <application
        android:name=".tinker.MyTinkerApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- 这个标签开判断我们生成的patch的.apk文件中的tinker_id_XXX
        与我们的版本号tinker_id_XXX比较。相同合法,不同则不会进行更新 -->
        <meta-data
            android:name="TINKER_ID"
            android:value="tinker_id_19940208"/>
    </application>

</manifest>

首先我们加上必要的权限。然设置我们的MyTinkerApplication。同时我们还需要配置TINKER_ID这个属性,value值的数字部分一般为我们的versionCode。

  1. 生成差异apk文件
    在完成配置后我们需要生成一个old.apk(也就是需要修复的apk)。代码如下:
    MainActivity.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.ggxiaozhi.tinker.MainActivity">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="loadPatch"
        android:text="修复BUG"/>
</LinearLayout>

MainActivity:

public class MainActivity extends AppCompatActivity {

    private static final String FILE_END = ".apk";//文件后缀
    private String FILEDIR;//文件路径
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //    /storage/emulated/0/Android/data/com.example.ggxiaozhi.tinker/cache/tpatch/
        FILEDIR = getExternalCacheDir().getAbsolutePath() + "/tpatch/";
        //创建路径对应的文件夹
        File file = new File(FILEDIR);
        if (!file.exists())
            file.mkdir();
    }

    public void loadPatch(View view) {
        TinkerManager.loadPatch(getPatchName());
    }

    public String getPatchName() {
        return FILEDIR.concat("tinker").concat(FILE_END);
    }
}


这是old.apk中的代码。布局与代码也非常简单就是创建补丁文件的路径,在点击按钮时加载补丁文件。然后我就开始打包带签名文件的old.apk。这里我就不带大家打包了。打包完成后,我们修改下布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.ggxiaozhi.tinker.MainActivity">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="loadPatch"
        android:text="修复BUG"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="测试按钮"/>
</LinearLayout>

这里我们其他代码不动,只是增加了一个按钮。同时我们在打包一个新的new.apk文件出来。并将两个文件和签名文件。同时copy到命令行工具中。

  1. 命令行生成补丁文件
    首先我们利用Tinker官方为我们提供的命令行工具目录如下:
    图片.png

    将上面我们生成的两个apk文件重命名并将签名文件copy到该目录下。(注意.keystore是eclipse的签名文件.jks是AndroidStudio的签名文件,可以直接修改后缀名,并不影响)然后我们输入一下命令:
java -jar tinker-patch-cli.jar -old old.apk -new new.apk -config tinker_config.xml -out output_path

生成补丁文件。具体如下:

clipboard.png

aass.png

output为我们补丁文件的输出文件夹,不存在会自动创建。输入完命令后output文件夹如下:
图片.png

patch_signed.apk文件就是我们的补丁文件。然后我们安装old.apk并将这个补丁文件通过命令或是拷贝我们之前创建的指定文件下并重命名成我们代码中写的tinker.apk。这样点击按钮就会完成修复。
注意,在点击后会杀到当前进程,需要重新进入后才能看到效果。官方建议我们去监听手机的广播,比如锁屏的广播,点击HOME键等。来去重新启动,这个问题后面我们再去优化


gradle生成补丁文件

文章开始我们就说过在实际中,我们是通常是以gradle生成补丁文件较多。当然网上也有很多配置教程,基本上大同小异。下面我们来在上面代码的基础上修改,来完成gradle生成补丁文件。首先我们先修改下Manifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.ggxiaozhi.tinker">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

        ...
        
        <!-- 这个标签开判断我们生成的patch的.apk文件中的tinker_id_
        与我们的版本号tinker_id_比较。相同合法,不同则不会进行更新 -->
        <!--<meta-data
            android:name="TINKER_ID"
            android:value="tinker_id_19940208"/>-->
</manifest>

这里我们将tinker_id注释掉,因为我们会在gradle中去配置。然后在最外面的gradle中添加插件。


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

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        //加入Tinker的插件 里面包含gradle脚本
        classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

其他不同修改,只需加上插件即可。然后我们就可以配置gradle了。关于gradle配置网上有很多,基本上都懂小异。我把我的gradler配置粘贴出来,供大家参考:

apply plugin: 'com.android.application'

/*================================常量块中的引用常量====================================*/

def javaVersion=JavaVersion.VERSION_1_7

//这个目录是基于项目的目录:Tinker/app/build/bakApk目录下存放oldApk
//buildDir : Tinker/app/build/
def bakPath = file("${buildDir}/bakApk/")//指定基准文件(oldApk)存放位置

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "com.example.ggxiaozhi.tinker"
        minSdkVersion 19
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        multiDexEnabled true

    }
    //排除目录下不需要编译的包
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
    //java版本
    compileOptions {
        sourceCompatibility javaVersion
        targetCompatibility javaVersion
    }
    //建议 recommend Tinker相关配置
    dexOptions {
        //启动矩形模式
        jumboMode = true
    }
    signingConfigs {
        release {
            try {
                storeFile file("release.jks")//目录位置app/release.jks
                storePassword "gg199402"
                keyAlias "gg199402"
                keyPassword "gg199402"
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }
    }
    buildTypes {
        release {
            //是否进行混淆
            minifyEnabled true
            // 混淆文件的位置
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }

<!--    //真正的多渠道脚本支持
    productFlavors {

        googleplayer {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "googleplayer"]
        }

        baidu {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
        }

        productFlavors.all {
            flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
        }
    }-->
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })

    //可选,用于生成application类 provided编译不打包
    provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    //是否将依赖关系标记为正在改变
    //tinker的核心库 compile编译并打包
    compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    compile "com.android.support:multidex:1.0.1"//分包
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
}


ext {
    tinkerEnabled = true  //是否启用Tinker的标志位
    tinkerOldApkPath = "${bakPath}/"//oldApk 文件路径
    tinkerID = "1.0"//与版本号一致
    tinkerApplyMappingPath = "${bakPath}/" //混淆文件路径
    tinkerApplyResourcePath = "${bakPath}/" //资源路径
<!--    tinkerBuildFlavorDirectory = "${bakPath}/"  //多渠道路径-->
}

/*================================方法实现模块====================================*/

def getOldApkPath() {
    return ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return  ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return ext.tinkerID
}

def buildWithTinker() {
    return ext.tinkerEnabled
}

<!--def getTinkerBuildFlavorDirectory(){

    return ext.tinkerBuildFlavorDirectory
}
-->

if (buildWithTinker()) {
    //启用Tinker  引入相关Gradle方法
    apply plugin: 'com.tencent.tinker.patch'

    //所有Tinker相关参数的配置
    tinkerPatch {

        /*================================基本配置====================================*/

        //指定old apk(即上一个版本的Apk) 的文件路径
        oldApk = getOldApkPath()

        //是否忽略Tinker在产生patch文件时的错误警告并中断编译 false 不忽略 这样可以在生成patch文件时查看错误 具体哪些错误类型查考文档
        ignoreWarning = false

        //patch是否需要签名 true为需要 防止恶意串改
        useSign = true

        //是否启用tinker
        tinkerEnable = buildWithTinker()

        /*================================build配置====================================*/

        buildConfig {

            //指定old apk打包时所使用的混淆文件 (因为patch文件也是需要混淆的 所以必须要与Apk的打包混淆文件一致)
            applyMapping = getApplyMappingPath()

            //指定old apk的资源文件 希望new apk与其保持一致(R.txt 文件保持ResId的分配)
            applyResourceMapping = getApplyResourceMappingPath()

            //指定TinkerID patch文件的唯一标识符 要与新旧Apk一致
            tinkerId = getTinkerIdValue()

            //通常为false true会根据dex分包动态编译patch文件
            keepDexApply = false
        }

        /*================================dex相关配置====================================*/
        dex {

            //Tinker提供两种模式jar、raw
            //jar 适配到了api=14以下 而raw只能再14以上
            //jar模式下 Tinker会对dex文件压缩成jar文件 在对jar进行处理
            //raw模式下 Tinker直接对dex进行处理
            //使用jar文件体积相对会小一些 在实际开发中用jar模式较多
            dexMode = "jar"

            //指定dex目录  "assets/secondary-dex-?.jar"为Tinker官方Demo中建议参数
            //在没有分包的情况下 "classes*.dex" 会匹配到应用中的所有dex文件 分包会是classes1,classes2....
            pattern = ["classes*.dex", "assets/secondary-dex-?.jar"]

            //制定patch文件用到类
            loader = ["com.example.ggxiaozhi.tinker.tinker.MyTinkerApplication"]
        }

        /*================================Tinker关于jar与.so文件的替换相关配置====================================*/

        lib {
            pattern = ["libs/*/*.so"]
        }

        /*================================Tinker关于资源文件替换相关配置====================================*/

        res {

            //指定Tinker可以修改的资源文件路径
            // resources.arcs :AndroidReSourCe也就是与Android资源相关的一种文件格式。
            // 具体角色是提供资源ID到资源文件路径的映射关系,
            // 具体来说就是R.layout.activity_main(0x7f030000)到res/layout/activity_main.xml的映射关系
            // 其中R.layout.activity_main就是应用开发过程中所使用的资源ID
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            //在编译时会忽略该文件的新增、删除与修改 即使修改了文件 也不会patch文件生效
            ignoreChange = ["assets/sample_meta.txt"]

            //对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。
            // 默认大小为100kb
            largeModSize = 100
        }

        /*=============附加说明字段 配置 说明本次Patch文件的相关信息 非必须 packageConfig(官方:用于生成补丁包中的'package_meta.txt'文件)=================*/

        packageConfig {
            /*configField("key","value") 键值对 用于说明 当客户端使用patch文件修复成功 可以通过代码获取下面patch相关信息*/
            configField("patchMessage", "fix the version's bugs")
            configField("patchVersion", "1.0")
        }

        //sevenZip ......
        sevenZip {
            /**
             * 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
    }

    /*================================备份脚本 用来将生成的APK的制定文件备份到制定目录====================================*/

    //多渠道相关遍历
    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    //如果是多渠道 则size()>0 为true
    boolean hasFlavors = flavors.size() > 0

    /**
     * bak apk and mapping 备份pak与mapping(配置文件)
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak 备份你想备份的数据 可以是任意类型
         */
        def taskName = variant.name
        def date = new Date().format("MMdd-HH-mm-ss")

        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"

                        //destPath为备份的目录 没有没有多渠道打包那么hasFlavors为false destPath=bakPath bakPath即最上面定义的基础目录
                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                        from variant.outputs.outputFile
                        into destPath
                        //备份.apk文件
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                        }

                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                        into destPath
                        //备份mapping.txt文件
                        rename { String fileName ->
                            fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                        }

                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                        into destPath
                        //备份R.txt文件 即用于映射的资源ID
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }


     /* Tinker多渠道打包文件拼凑脚本*/
  <!--  project.afterEvaluate {
        if (hasFlavors) {
            //正式签名多渠道打包
            task(tinkerPatchAllFlavorRelease) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()//拿到外层文件夹
                for (String flavor : flavors) {//遍历每种渠道
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                    preAssembleTask.doFirst {//文件拼凑
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"
                    }
                }
            }
            //Debug签名多渠道打包基本
            task(tinkerPatchAllFlavorDebug) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
                    }
                }
            }
        }
    }-->
}

这里有关多渠道打包的部分我先注释掉,我们先从简单看起。里面注释应该是比较详细的了,在使用中这些配置也基本满足需求。关于参数与配置也可以参考官方文档。sample中的app/build.gradle以及gradle参数详解

做完这些Tinker的gradle接入就完成了。还是之前的代码我们先打一个包含一个button的带签名的正式包。

图片.png

首先我们点击1.生成基准(oldApk)签名包。2.是用来生成补丁文件的。然后我们修改代码,在加入一个Button,也可以同时给加上点击事件Toast。生成apk文件后目录如下:
图片.png

首先我们在app/build/outputs/apk/app-release.apk生成签名文件apk,并备份到在app/build/bakApk/下,并以时间重命名文件。这三个文件分别是基准包(oldApk)、混淆文件、资源文件。然后我们分别将这个文件名写入到我们的gradle中,如下:

ext {
    tinkerEnabled = true  //是否启用Tinker的标志位
    tinkerOldApkPath = "${bakPath}/app-release-0201-16-15-06.apk"//oldApk 文件路径
    tinkerID = "1.0"//与版本号一致
    tinkerApplyMappingPath = "${bakPath}/app-release-0201-16-15-06-mapping.txt" //混淆文件路径
    tinkerApplyResourcePath = "${bakPath}/app-release-0201-16-15-06-R.txt" //资源路径
    
}

只需要修改ext部分其他不变。然后我们点击2.部分生成补丁文件。补丁文件目录文件如下:

图片.png

目录中的参数作用,可以参考下表:


图片.png

然后我们就将基准包安装到手机中,并将补丁文件copy到我们代码中指定的文件夹下并重命名。就可以完成动态更新。我亲测有效。所以就不发动图了。


结语

现在Tinker的版本已经更新到了1.9.2。相对与本文的1.7.7最主要的改动就是支持加固同时也进行了一些优化,比如支持Android8.0 等。由于最新的版本我要使用所以就没有去以最新版本去分析。大家有需要的可以在学习本系列后具体了解下。下篇我们来讲解Gradle生成补丁文件的扩展和优化以及从源码查看流程分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容