App體積縮減(R文件刪除)-自定義transform

說到App的體積縮減,大家首先能想到的是一些比較常規(guī)的方式:

  1. 圖片壓縮
  2. 配置 ndk的abiFilter
  3. 配置resConfigs
  4. 代碼混淆
  5. 資源混淆
  6. xxxx

那么今天各谚,我將和大家一起探索一種新型的體積縮減方案:R文件刪除
R文件的一些特征:
R文件也是會像resource莺债、assets這些資源一樣進(jìn)行合并的滋觉,比如說是A模塊依賴B模塊,那么A在編譯期間生成的R文件中就包含了B模塊中生成的R文件中的值齐邦,所以在app模塊的R文件中椎侠,它涵蓋了項目中所有的R文件的值,因此我們可以將library或者aar中的R文件進(jìn)行刪除措拇,刪除這些R文件還有一個比較好的效果就是它大大減少了應(yīng)用中的字段數(shù)量

拿上述的圖片壓縮來減少包大小體積來說我纪,應(yīng)該怎么實現(xiàn)呢?最簡單粗暴的方式就是手動將項目中用到的圖片全部手動壓一遍丐吓,再不就是利用Android studio自帶的轉(zhuǎn)webp功能將圖片轉(zhuǎn)成webp格式的浅悉,這樣也沒啥問題,但是有些缺點券犁,那就是我引入的一些aar中的圖片术健,應(yīng)該如何做到刪除呢?
解決方案之一就是自定義gradle 插件在編譯期間進(jìn)行圖片壓縮

今天不拿圖片壓縮來舉例子族操,圖片壓縮這個例子應(yīng)該是屬于自定義task苛坚,今天的主角是自定義transform,雖然它也屬于一個task色难,但是代碼層面稍稍有些不一樣

在這里我就不介紹transform是什么東西了泼舱,不熟悉的同學(xué)科學(xué)上網(wǎng)搜搜也能知道個大概啦

常規(guī)的支持增量的自定義transform如下所示:

package com.kunio.plugin;

import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.pipeline.TransformManager;
import com.android.ide.common.internal.WaitableExecutor;
import com.android.utils.FileUtils;
import com.edu.assets.merge.pre.LottieClassVisitor;

import org.apache.commons.io.IOUtils;
import org.gradle.api.Project;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

public class KunioTransform extends Transform {
    private WaitableExecutor executor = WaitableExecutor.useGlobalSharedThreadPool();
    private static final String NAME = "AssetsLottie";
    private final Project project;

    public KunioTransform(Project project) {
        this.project = project;
    }

    /**
     * @return 返回transform 的名稱
     * <p>
     * 最后會有transformClassesWithXxxForDebug、transformClassesWithXxxForRelease等task
     */
    @Override
    public String getName() {
        return NAME;
    }

    /**
     * @return 返回需要處理的輸入類型枷莉,這里我們處理class文件
     */
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    /**
     * @return 返回處理的作用域范圍娇昙,我們這里處理整個項目中的class文件,包括aar中的
     */
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    /**
     * @return 是否支持增量笤妙,需要在transform時二次判斷來決策當(dāng)前是否是增量的
     */
    @Override
    public boolean isIncremental() {
        return true;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws InterruptedException, IOException {
        boolean isIncremental = transformInvocation.isIncremental();
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        if (!isIncremental) {
            outputProvider.deleteAll();
        }
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        for (TransformInput input : inputs) {
            Collection<JarInput> jarInputs = input.getJarInputs();
            for (JarInput jarInput : jarInputs) {
                executor.execute(() -> {
                    processJarInputIncremental(jarInput, outputProvider, isIncremental);
                    return null;
                });
            }

            Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
            for (DirectoryInput directoryInput : directoryInputs) {
                executor.execute(() -> {
                    processDirectoryInputIncremental(directoryInput, outputProvider, isIncremental);
                    return null;
                });
            }
        }
        executor.waitForTasksWithQuickFail(true);
    }

    private void processDirectoryInputIncremental(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException {
        File inputDir = directoryInput.getFile();
        File outputDir = outputProvider.getContentLocation(
                directoryInput.getName(),
                directoryInput.getContentTypes(),
                directoryInput.getScopes(),
                Format.DIRECTORY);
        if (isIncremental) {
            Set<Map.Entry<File, Status>> entries = directoryInput.getChangedFiles().entrySet();
            for (Map.Entry<File, Status> entry : entries) {
                File file = entry.getKey();
                File destFile = new File(file.getAbsolutePath().replace(inputDir.getAbsolutePath(), outputDir.getAbsolutePath()));
                Status status = entry.getValue();
                switch (status) {
                    case ADDED:
                        FileUtils.mkdirs(destFile);
                        FileUtils.copyFile(file, destFile);
                        break;
                    case CHANGED:
                        FileUtils.deleteIfExists(destFile);
                        FileUtils.mkdirs(destFile);
                        FileUtils.copyFile(file, destFile);
                        break;
                    case REMOVED:
                        FileUtils.deleteIfExists(destFile);
                        break;
                    case NOTCHANGED:
                        break;
                }
            }
        } else {
            FileUtils.copyDirectory(directoryInput.getFile(), outputDir);
        }
    }

    private void processJarInputIncremental(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException {
        File dest = outputProvider.getContentLocation(
                jarInput.getFile().getAbsolutePath(),
                jarInput.getContentTypes(),
                jarInput.getScopes(),
                Format.JAR);
        if (isIncremental) {
            //處理增量編譯
            switch (jarInput.getStatus()) {
                case NOTCHANGED:
                    break;
                case ADDED:
                    processJarInput(jarInput, dest);
                    break;
                case CHANGED:
                    //處理有變化的
                    FileUtils.deleteIfExists(dest);
                    processJarInput(jarInput, dest);
                    break;
                case REMOVED:
                    //移除Removed
                    FileUtils.deleteIfExists(dest);
                    break;
            }
        } else {
            //不處理增量編譯
            processJarInput(jarInput, dest);
        }
    }

    private void processJarInput(JarInput jarInput, File dest) throws IOException {
        String name = jarInput.getName();
//        com.airbnb.android:lottie:3.6.1
//        androidx.cardview:cardview:1.0.0
//        androidx.coordinatorlayout:coordinatorlayout:1.1.0
//        androidx.fragment:fragment:1.1.0
//        androidx.constraintlayout:constraintlayout:2.0.4
//        androidx.appcompat:appcompat:1.2.0
//        do something
        realProcessJarInput(jarInput);
        FileUtils.copyFile(jarInput.getFile(), dest);
    }

    private void realProcessJarInput(JarInput jarInput) throws IOException {
        File file = jarInput.getFile();
        File tempJar = new File(file.getParentFile(), file.getName() + ".temp");
        JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempJar));
        JarFile jar = new JarFile(file);
        Enumeration<JarEntry> entries = jar.entries();
        boolean changed = false;
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            InputStream inputStream = jar.getInputStream(jarEntry);
            jarOutputStream.putNextEntry(new ZipEntry(jarEntry.getName()));
//            com/airbnb/lottie/L.class
//            com/airbnb/lottie/L.class
//            com/airbnb/lottie/Lottie.class
//            com/airbnb/lottie/Lottie.class
//            com/airbnb/lottie/LottieAnimationView.class
            if (true) {
                // 如果是需要處理這個class文件
                changed = true;
                byte[] bytes = insertCodeToConstructors(inputStream);
                jarOutputStream.write(bytes);
            } else {
                jarOutputStream.write(IOUtils.toByteArray(inputStream));
            }
            inputStream.close();
            jarOutputStream.closeEntry();
        }
        jar.close();
        jarOutputStream.close();
        if (changed) {
            FileUtils.delete(file);
            tempJar.renameTo(file);
        } else {
            FileUtils.delete(tempJar);
        }
//        FileUtils.delete(tempJar);
    }


    /**
     * 利用一些字節(jié)碼工具來動態(tài)改變類的行為
     */
    private byte[] insertCodeToConstructors(InputStream inputStream) throws IOException {

        //1. 構(gòu)建ClassReader對象
        ClassReader classReader = new ClassReader(inputStream);
        //2. 構(gòu)建ClassVisitor的實現(xiàn)類ClassWriter
        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        KunioClassVisitor visitor = new KunioClassVisitor(Opcodes.ASM6, classWriter);
        classReader.accept(visitor, ClassReader.EXPAND_FRAMES);
        //4. 通過classWriter對象的toByteArray方法拿到完整的字節(jié)流
        return classWriter.toByteArray();
    }
}


public class KunioClassVisitor extends ClassVisitor {
    public KunioClassVisitor(int api) {
        super(api);
    }

    public KunioClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        if ("init".equals(name) && "(Landroid/util/AttributeSet;I)V".equals(descriptor)) {
//            System.out.println("access = " + access);
//            System.out.println("name = " + name);
//            System.out.println("descriptor = " + descriptor);
//            System.out.println("signature = " + signature);
            return new initMethodVisitor(api, methodVisitor,access,name,descriptor);
        } else {
            return methodVisitor;
        }
    }

    static class initMethodVisitor extends AdviceAdapter {
        initMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor);
        }

        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESTATIC, "com/edu/assets/lottie/bitmap/delegate/BitmapDelegate", "setDelegate", "(Lcom/airbnb/lottie/LottieAnimationView;)V", false);
        }
    }
}

在這個例子中冒掌,我們所需要做的不是去動態(tài)更改class文件,而是刪除一些文件蹲盘,這更簡單了:

1.

app模塊的代碼一般是屬于DirectoryInput類型的股毫,一般情況下該類型的我們不做處理,因為這里面的R文件是應(yīng)用中所有的R引用了召衔,保存即可

2.

對于jarInput類型的輸入铃诬,我們需要作出如下的功能和判斷:
· 當(dāng)前不包含R文件,那么我們需要對該類中的R引用進(jìn)行替換
· 包含了R文件,但是該類是配置在了白名單中趣席,那么該類就不作變換
· 包含了R文件兵志,但是該R文件是以app模塊中的包名開頭,也無需作出變換
· 可以做一個開關(guān)宣肚,在打debug包時想罕,不做R文件的刪除,以此來節(jié)省部分編譯時間

差不多就是這么多霉涨,下面開始寫代碼了:
定義如下實體類:

 class UnifyRExtension {
     // app 模塊包名
    public String appPackageName;
    // 類白名單包名按价,處于此包下的類不處理
    public List<String> whitePackage;
    // debug模式下跳過處理
    public boolean skipDebug = true
}

gradle plgin類:

public class KunioPlugin implements Plugin<Project> {
    private static final String CONFIG_NAME = "UnifyRExtension";

    @Override
    public void apply(@NotNull Project project) {
        boolean hasAppPlugin = project.getPlugins().hasPlugin("com.android.application");
        if (!hasAppPlugin) {
            throw new GradleException("this plugin can't use in library module");
        }
        AppExtension android = (AppExtension) project.getExtensions().findByName("android");
        if (android == null) {
            throw new NullPointerException("application module not have \"android\" block!");
        }
        project.getExtensions().create(CONFIG_NAME, UnifyRExtension.class);
        android.registerTransform(new UnifyRTransform(project));
    }
}

transform:

package com.edu.android.plugin;

import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.pipeline.TransformManager;
import com.android.ide.common.internal.WaitableExecutor;
import com.android.utils.FileUtils;

import org.apache.commons.io.IOUtils;
import org.gradle.api.Project;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

public class UnifyRTransform extends Transform {
    private static final List<String> R = Arrays.asList(
            "R$xml",
            "R$transition",
            "R$styleable",
            "R$style",
            "R$string",
            "R$raw",
            "R$plurals",
            "R$mipmap",
            "R$menu",
            "R$layout",
            "R$interpolator",
            "R$integer",
            "R$id",
            "R$fraction",
            "R$font",
            "R$drawable",
            "R$dimen",
            "R$color",
            "R$bool",
            "R$attr",
            "R$array",
            "R$animator",
            "R$anim");
    private WaitableExecutor executor = WaitableExecutor.useGlobalSharedThreadPool();
    private static final String NAME = "UnifyR";
    private final Project project;
    private String appPackagePrefix;
    private List<String> whitePackages;

    public UnifyRTransform(Project project) {
        this.project = project;
    }

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental() {
        return true;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        UnifyRExtension unifyRExtension = project.getExtensions().findByType(UnifyRExtension.class);
        boolean skipDebugUnifyR = transformInvocation.getContext().getVariantName().toLowerCase().contains("debug") && unifyRExtension.skipDebug;
        if (skipDebugUnifyR) {
            copyOnly(transformInvocation.getInputs(), transformInvocation.getOutputProvider());
            return;
        }
        appPackagePrefix = unifyRExtension.packageName.replace('.', '/') + '/';
        List<String> whitePackage = unifyRExtension.whitePackage;
        List<String> whites = new ArrayList<>();
        if (whitePackage != null) {
            for (String s : whitePackage) {
                whites.add(s.replace('.', '/'));
            }
        }
        whitePackages = new ArrayList<>(whites);

        boolean isIncremental = transformInvocation.isIncremental();
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        if (!isIncremental) {
            outputProvider.deleteAll();
        }
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        for (TransformInput input : inputs) {
            Collection<JarInput> jarInputs = input.getJarInputs();
            for (JarInput jarInput : jarInputs) {
                executor.execute(() -> {
                    processJarInputWithIncremental(jarInput, outputProvider, isIncremental);
                    return null;
                });
            }

            Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
            for (DirectoryInput directoryInput : directoryInputs) {
                executor.execute(() -> {
                    processDirectoryInputWithIncremental(directoryInput, outputProvider, isIncremental);
                    return null;
                });
            }
        }
        executor.waitForTasksWithQuickFail(true);
    }

    private void processDirectoryInputWithIncremental(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException {
        File inputDir = directoryInput.getFile();
        File outputDir = outputProvider.getContentLocation(
                directoryInput.getName(),
                directoryInput.getContentTypes(),
                directoryInput.getScopes(),
                Format.DIRECTORY);
        if (isIncremental) {
            Set<Map.Entry<File, Status>> entries = directoryInput.getChangedFiles().entrySet();
            for (Map.Entry<File, Status> entry : entries) {
                File file = entry.getKey();
                File destFile = new File(file.getAbsolutePath().replace(inputDir.getAbsolutePath(), outputDir.getAbsolutePath()));
                Status status = entry.getValue();
                switch (status) {
                    case ADDED:
                        FileUtils.mkdirs(destFile);
                        FileUtils.copyFile(file, destFile);
                        break;
                    case CHANGED:
                        FileUtils.deleteIfExists(destFile);
                        FileUtils.mkdirs(destFile);
                        FileUtils.copyFile(file, destFile);
                        break;
                    case REMOVED:
                        FileUtils.deleteIfExists(destFile);
                        break;
                    case NOTCHANGED:
                        break;
                }
            }
        } else {
            FileUtils.copyDirectory(directoryInput.getFile(), outputDir);
        }
    }

    private void processJarInputWithIncremental(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException {
        File dest = outputProvider.getContentLocation(
                jarInput.getFile().getAbsolutePath(),
                jarInput.getContentTypes(),
                jarInput.getScopes(),
                Format.JAR);
        if (isIncremental) {
            //處理增量編譯
            switch (jarInput.getStatus()) {
                case NOTCHANGED:
                    break;
                case ADDED:
                    processJarInput(jarInput, dest);
                    break;
                case CHANGED:
                    //處理有變化的
                    FileUtils.deleteIfExists(dest);
                    processJarInput(jarInput, dest);
                    break;
                case REMOVED:
                    //移除Removed
                    if (dest.exists()) {
                        FileUtils.delete(dest);
                    }
                    break;
            }
        } else {
            //不處理增量編譯
            processJarInput(jarInput, dest);
        }
    }

    private void processJarInput(JarInput jarInput, File dest) throws IOException {
        processClass(jarInput);
        FileUtils.copyFile(jarInput.getFile(), dest);
    }

    private void processClass(JarInput jarInput) throws IOException {
        File file = jarInput.getFile();
        File tempJar = new File(file.getParentFile(), file.getName() + ".temp");
        JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempJar));
        JarFile jar = new JarFile(file);
        Enumeration<JarEntry> entries = jar.entries();
        while (entries.hasMoreElements()) {
            byte[] destBytes = null;
            JarEntry jarEntry = entries.nextElement();
            InputStream inputStream = jar.getInputStream(jarEntry);
            String name = jarEntry.getName();
            if (name.endsWith(".class")) {
                boolean keep = false;
                for (String s : whitePackages) {
                    if (name.contains(s)) {
                        keep = true;
                        break;
                    }
                }
                if (keep) {
                    destBytes = IOUtils.toByteArray(inputStream);
                } else {
                    if (!hasR(name)) {
                        destBytes = unifyR(name, inputStream);
                    } else if (name.startsWith(appPackagePrefix)) {
                        destBytes = IOUtils.toByteArray(inputStream);
                    }
                }
            } else {
                destBytes = IOUtils.toByteArray(inputStream);
            }
            if (destBytes != null) {
                jarOutputStream.putNextEntry(new ZipEntry(jarEntry.getName()));
                jarOutputStream.write(destBytes);
                jarOutputStream.closeEntry();
            }
            inputStream.close();
        }
        jar.close();
        jarOutputStream.close();
        FileUtils.delete(file);
        tempJar.renameTo(file);
    }

    private byte[] unifyR(String entryName, InputStream inputStream) throws IOException {
        ClassReader cr = new ClassReader(inputStream);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM6, cw) {

            @Override
            public MethodVisitor visitMethod(int access, String name, String desc,
                                             String signature, String[] exceptions) {
                MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
                return new MethodVisitor(Opcodes.ASM6, mv) {
                    @Override
                    public void visitFieldInsn(int opcode, String owner, String fName, String fDesc) {
                        if (hasR(owner) && !owner.contains(appPackagePrefix)) {
                            super.visitFieldInsn(opcode, appPackagePrefix + "R$" + owner.substring(owner.indexOf("R$") + 2), fName, fDesc);
                        } else {
                            super.visitFieldInsn(opcode, owner, fName, fDesc);
                        }
                    }
                };
            }

        };
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();
    }

    private static void copyOnly(Collection<TransformInput> inputs, TransformOutputProvider outputProvider) throws IOException {
        for (TransformInput input : inputs) {
            Collection<JarInput> jarInputs = input.getJarInputs();
            for (JarInput jarInput : jarInputs) {
                File dest = outputProvider.getContentLocation(
                        jarInput.getName(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR);
                FileUtils.copyFile(jarInput.getFile(), dest);
            }

            Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
            for (DirectoryInput directoryInput : directoryInputs) {
                File dest = outputProvider.getContentLocation(
                        directoryInput.getName(),
                        directoryInput.getContentTypes(),
                        directoryInput.getScopes(),
                        Format.DIRECTORY);
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
            }
        }
    }

    /**
     * 判斷這個字符串里面有沒有R文件的標(biāo)識
     *
     * @param check 待檢測的字符串
     * @return 有標(biāo)識的話返回true
     */
    private static boolean hasR(String check) {
        for (String s : R) {
            if (check.contains(s)) {
                return true;
            }
        }
        return false;
    }
}

經(jīng)過上述Transform + ASM處理,可以對aar及l(fā)ibrary中打包生成的R文件刪除嵌纲,達(dá)到減少字段及包大小的目的俘枫。

最后

1. whitePackage需要將androidx.constraintlayout作為白名單填入
2. packageName需要填寫成為app模塊的包名即可
2. 上述示例只是給出了transform與plugin代碼,如何發(fā)布以及應(yīng)用plugin需要自己查詢相關(guān)資料
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逮走,一起剝皮案震驚了整個濱河市鸠蚪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌师溅,老刑警劉巖茅信,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異墓臭,居然都是意外死亡蘸鲸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門窿锉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酌摇,“玉大人,你說我怎么就攤上這事嗡载∫ざ啵” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵洼滚,是天一觀的道長埂息。 經(jīng)常有香客問我,道長遥巴,這世上最難降的妖魔是什么千康? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮铲掐,結(jié)果婚禮上拾弃,老公的妹妹穿的比我還像新娘。我一直安慰自己摆霉,他們只是感情好砸彬,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布颠毙。 她就那樣靜靜地躺著,像睡著了一般砂碉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上刻两,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天增蹭,我揣著相機與錄音,去河邊找鬼磅摹。 笑死滋迈,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的户誓。 我是一名探鬼主播饼灿,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼帝美!你這毒婦竟也來了碍彭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤悼潭,失蹤者是張志新(化名)和其女友劉穎庇忌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舰褪,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡皆疹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了占拍。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片略就。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖晃酒,靈堂內(nèi)的尸體忽然破棺而出表牢,到底是詐尸還是另有隱情,我是刑警寧澤掖疮,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布初茶,位于F島的核電站,受9級特大地震影響浊闪,放射性物質(zhì)發(fā)生泄漏恼布。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一搁宾、第九天 我趴在偏房一處隱蔽的房頂上張望折汞。 院中可真熱鬧,春花似錦盖腿、人聲如沸爽待。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸟款。三九已至膏燃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間何什,已是汗流浹背组哩。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留处渣,地道東北人伶贰。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像罐栈,于是被迫代替她去往敵國和親黍衙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345