16.优化 - apk 分析

  在 matrix 代码中有一个 matrix-apk-canary 的 library ,可以查看 apk 的一些详细信息,如大小、方法数、资源使用等等一些情况。
包含功能如下:

UnzipTask(config, params)   解压 apk 文件
ManifestAnalyzeTask(config, params) 解析 Manifest、arsc文件
ShowFileSizeTask(config, params)    统计超过阈值的文件
MethodCountTask(config, params)     统计方法数   
ResProguardCheckTask(config, params)    判断 apk 是否资源混淆(使用资源混淆来进一步减小 apk 的大小)
FindNonAlphaPngTask(config, params)     检测 png 文件是否有透明度(对于不含 alpha 通道的 png 文件,可以转成 jpg 格式来减少文件的大小)
MultiLibCheckTask(config, params)   判断是不是存在多 CPU 架构的 so
UncompressedFileTask(config, params)    检测原文件是否压缩(未压缩可以考虑是否需要压缩)
CountRTask(config, params)  统计 R 文件数量(编译之后,代码中对资源的引用都会优化成 int 常量,除了 R.styleable 之外,其他的 R 类其实都可以删除)
DuplicateFileTask(config, params)   判断是否存在一样的文件,通过计算文件的 md5
MultiSTLCheckTask(config, params)   判断是否依赖 std 标准模板库(如果有多个动态库都依赖了 STL ,应该采用动态链接的方式而非多个动态库都去静态链接 STL)
UnusedResourcesTask(config, params) 检测代码、资源文件中未引用的资源
UnusedAssetsTask(config, params);   检测未使用的 assets 资源
UnStrippedSoCheckTask(config, params)   检测为裁剪的 so(去掉调试信息)
CountClassTask(config, params)  统计类数量

使用说明如下

  • 1.可以将matrix-apk-canary library 编译成 jar ,运行 java jar -apkjar params1 params2 ... 即可。
  • 2.直接在工程 run 也可。

其中参数说明:在matrix-apk-canary 工程下有一个apk-checker-config.json文件,

  • --apk替换成自己的apk路径
  • --output替换为自己的结果输出路径,会输出一个html和一个json文件
  • --toolnm替换为自己sdk下的nm命令行工具路径
  • --rTxt替换为自己的build目录下的R.txt路径
    之后可以将apk-checker-config.json文件的路径作为运行的参数,最终可以在--output目录下找到输出的 apk 分析结果有一个 html 的和一个 json 的结果。

流程原理如下:

apk-process.png

matrix-apk-canary 的整体工作流程如上所示。

    1. UnzipTask
      首先 apk 首先会经过 UnzipTask 处理,解压到--output目录,还会在这里读取 mapping.txt (反混淆类名)、读取 resMapping.txt (反混淆资源)、统计文件数量及大小等。并将这些数据写入到JobConfig这个类中,之后的 task 只需要读取这个JobConfig这个类中的数据就可以了。
    1. ManifestAnalyzeTask
      ManifestAnalyzeTask 用于读取 AndroidManifest.xml中的信息,如:packageName、verisonCode、clientVersion 等。
      利用 ApkTool 中的 ApkResourceDecoder.createAXmlParser() 来解析二进制的 AndroidManifest.xml 和 arscFile 文件,不断读取 xml 的数据,遇到 handleStartElement( "<" 进,记录 xml) 与 handleEndElement( ">" 出,丢弃记录的 xml 数据) ,最终只会记录packageName、verisonCode、clientVersion等数据。
      manifest.png
    1. ShowFileSizeTask
      根据apk-checker-config.json文件中的规则,如"--min":"10", "--order":"desc","--suffix":"png, jpg, jpeg, gif, arsc",过滤超过最小文件大小以及文件后缀名文件,并按照升序或降序排列结果。
      利用 UnzipTask 中统计的文件的entryList来作为输入,根据上面的规则来输出结果。
            if (!entrySizeMap.isEmpty()) {                                                          //take advantage of the result of UnzipTask.
                for (Map.Entry<String, Pair<Long, Long>> entry : entrySizeMap.entrySet()) {
                    final String suffix = getSuffix(entry.getKey());
                    Pair<Long, Long> size = entry.getValue();
                    if (size.getFirst() >= downLimit * ApkConstants.K1024) { // > 10240
                        if (filterSuffix.isEmpty() || filterSuffix.contains(suffix)) {
                            // 记录 size > 10240 ,且是 png, jpg, jpeg, gif, arsc 结尾的文件
                            entryList.add(Pair.of(entry.getKey(), size.getFirst())); // png, jpg, jpeg, gif, arsc
                        } else {
                            Log.d(TAG, "file: %s, filter by suffix.", entry.getKey());
                        }
                    } else {
                        Log.d(TAG, "file:%s, size:%d B, downlimit:%d KB", entry.getKey(), size.getFirst(), downLimit);
                    }
                }
            }

            Collections.sort(entryList,...);
file_size.png
  • 4.MethodCountTask
    统计出各个 Dex 中的方法数,并按照类名或者包名来分组输出结果。
    遍历 UnzipTask 中解压后是 dex 结尾的文件,利用 dexdeps 类库来读取 dex 文件,统计方法数。
for (int i = 0; i < dexFileList.size(); i++) {
                RandomAccessFile dexFile = dexFileList.get(i);
                // 统计 dex 中的方法数并将其存在 classInternalMethod 和 classExternalMethod 中
                countDex(dexFile); 
                dexFile.close();
                int totalInternalMethods = sumOfValue(classInternalMethod);
                int totalExternalMethods = sumOfValue(classExternalMethod);
                JsonObject jsonObject = new JsonObject();
                jsonObject.addProperty("dex-file", dexFileNameList.get(i));

                if (JobConstants.GROUP_CLASS.equals(group)) {
                    ...
                } else if (JobConstants.GROUP_PACKAGE.equals(group)) {
                    String packageName;
                    // 聚合 package 下的方法数
                    for (Map.Entry<String, Integer> entry : classInternalMethod.entrySet()) {
                        // 读出包名,用最后一个 . 来分割
                        packageName = ApkUtil.getPackageName(entry.getKey()); // android.accounts.Account->android.accounts
                        if (!Util.isNullOrNil(packageName)) {
                            // 原来没有就是 1 ,有的话就加上之前的数量
                            if (!pkgInternalRefMethod.containsKey(packageName)) {
                                pkgInternalRefMethod.put(packageName, entry.getValue()); // pair{android.accounts,1}
                            } else {
                                pkgInternalRefMethod.put(packageName, pkgInternalRefMethod.get(packageName) + entry.getValue());
                            }
                        }
                    }
                    // 以方法包名的数量排序
                    List<String> sortList = sortKeyByValue(pkgInternalRefMethod);
                    JsonArray packages = new JsonArray();
                    for (String pkgName : sortList) {
                        JsonObject pkgObj = new JsonObject();
                        pkgObj.addProperty("name", pkgName);
                        pkgObj.addProperty("methods", pkgInternalRefMethod.get(pkgName));
                        packages.add(pkgObj);
                    }
                    // 记录
                    jsonObject.add("internal-packages", packages);
                }
                // 记录
                jsonObject.addProperty("total-internal-classes", classInternalMethod.size());
                jsonObject.addProperty("total-internal-methods", totalInternalMethods);

                if (JobConstants.GROUP_CLASS.equals(group)) {
                    ...
                } else if (JobConstants.GROUP_PACKAGE.equals(group)) {
                    String packageName = "";
                    for (Map.Entry<String, Integer> entry : classExternalMethod.entrySet()) {
                        packageName = ApkUtil.getPackageName(entry.getKey()); // android.accounts.Account->android.accounts
                        if (!Util.isNullOrNil(packageName)) {
                            if (!pkgExternalMethod.containsKey(packageName)) {
                                pkgExternalMethod.put(packageName, entry.getValue());
                            } else {
                                pkgExternalMethod.put(packageName, pkgExternalMethod.get(packageName) + entry.getValue());
                            }
                        }
                    }
                    List<String> sortList = sortKeyByValue(pkgExternalMethod);
                    JsonArray packages = new JsonArray();
                    for (String pkgName : sortList) {
                        JsonObject pkgObj = new JsonObject();
                        pkgObj.addProperty("name", pkgName);
                        pkgObj.addProperty("methods", pkgExternalMethod.get(pkgName));
                        packages.add(pkgObj);
                    }
                    jsonObject.add("external-packages", packages);

                }
                jsonObject.addProperty("total-external-classes", classExternalMethod.size());
                jsonObject.addProperty("total-external-methods", totalExternalMethods);
                jsonArray.add(jsonObject);
            }
method_size.png
    1. ResProguardCheckTask
      判断 apk 是否经过了资源混淆
      资源混淆之后的 res 文件夹会重命名成 r ,直接判断是否存在文件夹 r 即可判断是否经过了资源混淆。
            if (resDir.exists() && resDir.isDirectory()) {
                Log.i(TAG, "find resource directory " + resDir.getAbsolutePath());
                ((TaskJsonResult) taskResult).add("hasResProguard", true);
            } else {
                resDir = new File(inputFile, ApkConstants.RESOURCE_DIR_NAME);
                if (resDir.exists() && resDir.isDirectory()) {
                    File[] dirs = resDir.listFiles();
                    boolean hasProguard = true;
                    for (File dir : dirs) {
                        if (dir.isDirectory() && !fileNamePattern.matcher(dir.getName()).matches()) {
                            hasProguard = false;
                            Log.i(TAG, "directory " + dir.getName() + " has a non-proguard name!");
                            break;
                        }
                    }
                    ((TaskJsonResult) taskResult).add("hasResProguard", hasProguard);
                } else {
                    throw new TaskExecuteException(TAG + "---No resource directory found!");
                }
            }
resguard.png
  • 6.FindNonAlphaPngTask
    检测出 apk 中非透明的是 png 结尾的图片且不是 .9.png 的图片
    通过 java.awt.BufferedImage 类读取png文件并判断是否有 alpha 通道。
    private void findNonAlphaPng(File file) throws IOException {
        if (file != null) {
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                for (File tempFile : files) {
                    findNonAlphaPng(tempFile);
                }
            // 是 png 结尾的图片并且不是 .9.png 的图片
            } else if (file.isFile() && file.getName().endsWith(ApkConstants.PNG_FILE_SUFFIX) && !file.getName().endsWith(ApkConstants.NINE_PNG)) {
                BufferedImage bufferedImage = ImageIO.read(file);
                // 颜色模式没有透明度
                if (bufferedImage != null && bufferedImage.getColorModel() != null && !bufferedImage.getColorModel().hasAlpha()) {
                    String filename = file.getAbsolutePath().substring(inputFile.getAbsolutePath().length() + 1);
                    if (entryNameMap.containsKey(filename)) {
                        filename = entryNameMap.get(filename);
                    }
                    long size = file.length();
                    if (entrySizeMap.containsKey(filename)) {
                        size = entrySizeMap.get(filename).getFirst();
                    }
                    if (size >= downLimitSize * ApkConstants.K1024) { // 10 * 1024
                        nonAlphaPngList.add(Pair.of(filename, file.length()));
                    }
                }
            }
        }
    }
png_alpha.png

-7.MultiLibCheckTask
判断 app 的 so 是不是支持多 CPU 的
遍历 lib 文件夹下是否包含多个目录。

    @Override
    public TaskResult call() throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(getType(), TASK_RESULT_TYPE_JSON, config);
            if (taskResult == null) {
                return null;
            }
            long startTime = System.currentTimeMillis();
            JsonArray jsonArray = new JsonArray();
            if (libDir.exists() && libDir.isDirectory()) {
                File[] dirs = libDir.listFiles();
                for (File dir : dirs) {
                    if (dir.isDirectory()) {
                        jsonArray.add(dir.getName());
                    }
                }
            }
            ((TaskJsonResult) taskResult).add("lib-dirs", jsonArray);
            if (jsonArray.size() > 1) {
                ((TaskJsonResult) taskResult).add("multi-lib", true);
            } else {
                ((TaskJsonResult) taskResult).add("multi-lib", false);
            }
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw new TaskExecuteException(e.getMessage(), e);
        }
    }
mutil_so.png
  • 8.UncompressedFileTask
    检测出未经压缩的png, jpg, jpeg, gif, arsc文件类型
    实现方法:直接利用 UnzipTask 中统计的各个文件的压缩前和压缩后的大小,判断压缩前和压缩后大小是否相等。
    @Override
    public TaskResult call() throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config);
            if (taskResult == null) {
                return null;
            }
            long startTime = System.currentTimeMillis();
            JsonArray jsonArray = new JsonArray();
            // Map<文件名,Pair<解压大小,压缩大小>> 在 UnzipTask 中生成
            Map<String, Pair<Long, Long>> entrySizeMap = config.getEntrySizeMap();
            if (!entrySizeMap.isEmpty()) {                                                          //take advantage of the result of UnzipTask.
                for (Map.Entry<String, Pair<Long, Long>> entry : entrySizeMap.entrySet()) {
                    final String suffix = getSuffix(entry.getKey());
                    Pair<Long, Long> size = entry.getValue();
                    if (filterSuffix.isEmpty() || filterSuffix.contains(suffix)) { // png, jpg, jpeg, gif, arsc
                        if (!uncompressSizeMap.containsKey(suffix)) {
                            uncompressSizeMap.put(suffix, size.getFirst());
                        } else {
                            uncompressSizeMap.put(suffix, uncompressSizeMap.get(suffix) + size.getFirst());
                        }
                        if (!compressSizeMap.containsKey(suffix)) {
                            compressSizeMap.put(suffix, size.getSecond());
                        } else {
                            compressSizeMap.put(suffix, compressSizeMap.get(suffix) + size.getSecond());
                        }
                    } else {
                        Log.d(TAG, "file: %s, filter by suffix.", entry.getKey());
                    }
                }
            }

            for (String suffix : uncompressSizeMap.keySet()) {
                if (uncompressSizeMap.get(suffix).equals(compressSizeMap.get(suffix))) {
                    JsonObject fileItem = new JsonObject();
                    fileItem.addProperty("suffix", suffix);
                    fileItem.addProperty("total-size", uncompressSizeMap.get(suffix));
                    jsonArray.add(fileItem);
                }
            }
            ((TaskJsonResult) taskResult).add("files", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw new TaskExecuteException(e.getMessage(), e);
        }
    }
  • 9.CountRTask
    统计 R 类以及 R 类的中的 field 数目
    通过 dexdeps 类库来遍历 dex 文件,找出 R 类以及 field 数目。
    @Override
    public TaskResult call() throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config);
            long startTime = System.currentTimeMillis();
            Map<String, String> classProguardMap = config.getProguardClassMap();
            for (RandomAccessFile dexFile : dexFileList) {
                DexData dexData = new DexData(dexFile);
                dexData.load();
                dexFile.close();
                ClassRef[] defClassRefs = dexData.getInternalReferences();
                for (ClassRef classRef : defClassRefs) {
                    String className = ApkUtil.getNormalClassName(classRef.getName());
                    if (classProguardMap.containsKey(className)) {
                        className = classProguardMap.get(className);
                    }
                    String pureClassName = getOuterClassName(className);// java.lang.String->java.lang.String  com.zhejiang.a.A$Z->com.zhejiang.a.A
                    if (pureClassName.endsWith(".R") || "R".equals(pureClassName)) {
                        if (!classesMap.containsKey(pureClassName)) {
                            classesMap.put(pureClassName, classRef.getFieldArray().length);
                        } else {
                            classesMap.put(pureClassName, classesMap.get(pureClassName) + classRef.getFieldArray().length);
                        }
                    }
                }
            }

            JsonArray jsonArray = new JsonArray();
            long totalSize = 0;
            // null
            Map<String, String> proguardClassMap = config.getProguardClassMap();
            for (Map.Entry<String, Integer> entry : classesMap.entrySet()) {
                JsonObject jsonObject = new JsonObject();
                if (proguardClassMap.containsKey(entry.getKey())) {
                    jsonObject.addProperty("name", proguardClassMap.get(entry.getKey()));
                } else {
                    jsonObject.addProperty("name", entry.getKey());
                }
                jsonObject.addProperty("field-count", entry.getValue());
                totalSize += entry.getValue();
                jsonArray.add(jsonObject);
            }
            ((TaskJsonResult) taskResult).add("R-count", jsonArray.size());
            ((TaskJsonResult) taskResult).add("Field-counts", totalSize);

            ((TaskJsonResult) taskResult).add("R-classes", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw new TaskExecuteException(e.getMessage(), e);
        }
    }
R.png
  • 10.DuplicatedFileTask
    检测出冗余的文件
    通过比较文件的 MD5 是否相等来判断文件内容是否相同。
    // 计算每个文件的 md5,存到 md5Map 中,key = md5,value = array,如果有相同 md5 的就加入到 array 中。
    // 添加每个文件到 fileSizeList 中,T = pair{md5,file_size}
    private void computeMD5(File file) throws NoSuchAlgorithmException, IOException {
        if (file != null) {
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                for (File resFile : files) {
                    computeMD5(resFile);
                }
            } else {
                MessageDigest msgDigest = MessageDigest.getInstance("MD5");
                BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file));
                byte[] buffer = new byte[512];
                int readSize = 0;
                long totalRead = 0;
                while ((readSize = inputStream.read(buffer)) > 0) {
                    msgDigest.update(buffer, 0, readSize);
                    totalRead += readSize;
                }
                inputStream.close();
                if (totalRead > 0) {
                    final String md5 = Util.byteArrayToHex(msgDigest.digest());
                    String filename = file.getAbsolutePath().substring(inputFile.getAbsolutePath().length() + 1); // 从 un—zip 目录开始
                    if (entryNameMap.containsKey(filename)) {
                        filename = entryNameMap.get(filename);
                    }
                    if (!md5Map.containsKey(md5)) {
                        md5Map.put(md5, new ArrayList<String>());
                        if (entrySizeMap.containsKey(filename)) {
                            fileSizeList.add(Pair.of(md5, entrySizeMap.get(filename).getFirst()));
                        } else {
                            fileSizeList.add(Pair.of(md5, totalRead));
                        }
                    }
                    md5Map.get(md5).add(filename);
                }
            }
        }
    }
md5.png
  • 11.MultiSTLCheckTask
    检测 apk 中的 so 是否静态链接 STL
    通过 nm 工具来读取 so 的符号表,如果出现 std:: 即表示 so 静态链接了 STL 。
    // libFile so文件
    private boolean isStlLinked(File libFile) throws IOException, InterruptedException {
        ProcessBuilder processBuilder = new ProcessBuilder(toolnmPath, "-D", "-C", libFile.getAbsolutePath());
        Process process = processBuilder.start();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
![unuse_resource.png](https://upload-images.jianshu.io/upload_images/16590719-a131d5e46c8503a2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
        String line = reader.readLine();
        while (line != null) {
            String[] columns = line.split(" ");
            Log.d(TAG, "%s", line);
            if (columns.length >= 3 && columns[1].equals("T") && columns[2].startsWith("std::")) {
                return true;
            }
            line = reader.readLine();
        }
        reader.close();
        process.waitFor();
        return false;
    }
stl.png
  • 12.UnusedResourceTask
    检测出 apk 中未使用的资源,对于 getIdentifier 获取的资源可以加入白名单
    原理(copy):
  • 读取 R.txt 获取 apk 中声明的所有资源得到 declareResourceSet ;
  • 读取 smali 文件中引用资源的指令(包括通过 reference 和直接通过资源 id 引用资源)得出 class 中引用的资源 classRefResourceSet ;
  • 通过 ApkTool 解析 res 目录下的 xml 文件、AndroidManifest.xml 以及 resource.arsc 得出资源之间的引用关系;
  • 根据上述几步得到的中间数据即可确定出 apk 中未使用到的资源。
  • 13.UnusedAssetsTask
    检测出 apk 中未使用的 assets 资源
    public TaskResult call() throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config);
            long startTime = System.currentTimeMillis();
            File assetDir = new File(inputFile, ApkConstants.ASSETS_DIR_NAME);
            // 找到 assets 下的所有文件,存在 assetsPathSet 中
            findAssetsFile(assetDir);
            // 将符合过滤规则 ".so" 的 assetsPathSet 中的文件,存在了 assetRefSet
            // assetsPathSet 中的文件去掉了前缀
            generateAssetsSet(assetDir.getAbsolutePath());
            Log.i(TAG, "find all assets count: %d", assetsPathSet.size());
            // 将代码中使用到的 assets 资源加入到了 assetRefSet 里面
            decodeCode();
            Log.i(TAG, "find reference assets count: %d", assetRefSet.size());
            assetsPathSet.removeAll(assetRefSet);
            JsonArray jsonArray = new JsonArray();
            for (String name : assetsPathSet) {
                jsonArray.add(name);
            }
            ((TaskJsonResult) taskResult).add("unused-assets", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw new TaskExecuteException(e.getMessage(), e);
        }
    }
unuse_asserts.png
  • 14.UnStrippedSoCheckTask
    检测 so 文件是否裁剪(去除了调试信息)
    通过 nm 工具来读取 so 的符号表,如果出现 no symbols(错误输出) 表明已经去除了调试信息 。
    private boolean isSoStripped(File libFile) throws IOException, InterruptedException {
        ProcessBuilder processBuilder = new ProcessBuilder(toolnmPath, libFile.getAbsolutePath());
        Process process = processBuilder.start();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
        String line = reader.readLine();
        boolean result = false;
        if (!Util.isNullOrNil(line)) {
            Log.d(TAG, "%s", line);
            String[] columns = line.split(":");
            if (columns.length == 3 && columns[2].trim().equalsIgnoreCase("no symbols")) {
                result = true;
            }
        }
        reader.close();
        process.waitFor();
        return result;
    }
so_strip.png

写在最后
是不是可以自己编译成 jar,配合上线前的测试检测,分析 apk 的数据之后可以根据此结果再次的优化包体积、统计每次发布包的大小、文件占比及连续二次发布包的比较等等呢

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

推荐阅读更多精彩内容