安卓下快速搜索文件实现历程{NDK}

我在公司有一个文件浏览器的开发项目,需要很快的去遍历某一路劲下的所有的"图片文件""视频文件""音频文件""文档文件";在一接到这个需求的时候,我首先翻了一下我司前人的杰作——旧版的文件管理器。他们是使用java实现的,用的深度优先递归写法,加上后缀的判定方式是直接比较字符串,就是string的equals方法。想想,多可怕。完了以后呢,他们又吧所有的文件,用了一个中文转英文的库,把文件名翻译成拼音,然后再排序,用的Collections的sort方法,总共这一系列神操作下来呢,一个1w文件的目录耗时15s左右。
我的老大也受够了这么慢的"文件浏览器",所以他想让我优化优化……

首先,我分析了一下,这个文件浏览器遍历搜索花了5s左右,然后排序10s+.
然后我询问了老大的意见,他说排序太废时间了,直接砍了吧。嗯,那就是没有排序,就文件遍历,然后耗时5s。

这时候有机制的小伙伴就发现了,安卓下的文件不是可以通过ContentProvider去做查询吗。需要这么费力吗?是的,小伙子有在认真思考哦,诚然,安卓内置的或者挂载了很久的存储器确实可以这么做的。但是,你没想到的时,我压根不是给手机开发文件管理器。而是给一个安卓大屏开发文件管理器。

我最开始的思路,在java层,使用深度优先递归方式去遍历文件,然后,我在文件过滤器里使用equals去获取文件类型,然后根据类型把他加入一个map,所有的"图片文件""视频文件""音频文件""文档文件"都放入一个map里,然后我后面再去map里拿。似乎没少毛病吧?

一、更加糟糕的代码和复杂度

这是递归获取文件的方式:

/**
     * 获得某一路径下的所有的文件
     */
    public static void getAllFiles(File directory, FileFilter filter) {
        if (isDirectory(directory)) {
            File[] children = directory.listFiles(filter);
            if (children != null) {
                for (File child : children)
                    getAllFiles(child, filter);
            }
        }
    }

现在回过头来看,我的过滤器咋写的吧:{ps,似乎也没毛病}

/**
     * 数据分类
     */
    private static Map<Integer, List<File>> classiferData(String rootPath) {
        Map<Integer, List<File>> map = new HashMap<>();
        getAllFiles(new File(rootPath), new FileFilter() {
            @Override
            public boolean accept(File file) {
                int fileType = getFileType(file);
                int fileParentType = getFileParentType(fileType);
                if (fileParentType == MainConstants.FileType.TYPE_IMAGE
                    || fileParentType == MainConstants.FileType.TYPE_VIDEO_OR_AUDIO
                    || fileParentType == MainConstants.FileType.TYPE_DOC) {
                    if (map.get(fileParentType) != null) {
                        map.get(fileParentType).add(file);
                    } else {
                        map.put(fileParentType, new ArrayList<>());
                    }
                }
                return true;
            }
        });
        return map;
    }

这是我的文件类型获取方式:{这里怕不是就已经有很高的复杂度了吧}

/**
     * 获取文件类型
     */
    public static @FileTypeAnnotation int getFileType(File file) {
        if (file.isHidden()) {
            return MainConstants.FileType.TYPE_HIDDEN;
        }
        if (isDirectory(file)) {
            return MainConstants.FileType.TYPE_FOLDER;
        }
        String suffix = getSuffix(file);
        if (memberOf(suffix, IMAGE_SUFFIXES)) {
            return MainConstants.FileType.TYPE_IMAGE;
        } else if (memberOf(suffix, AUDIO_SUFFIXES)) {
            return MainConstants.FileType.TYPE_AUDIO;
        } else if (memberOf(suffix, VIDEO_SUFFIXES)) {
            return MainConstants.FileType.TYPE_VIDEO;
        } else if (memberOf(suffix, PDF_SUFFIXES)) {
            return MainConstants.FileType.TYPE_DOC_PDF;
        } else if (memberOf(suffix, PPT_SUFFIXES)) {
            return MainConstants.FileType.TYPE_DOC_PPT;
        } else if (memberOf(suffix, EXCEL_SUFFIXES)) {
            return MainConstants.FileType.TYPE_DOC_EXCEL;
        } else if (memberOf(suffix, WORD_SUFFIXES)) {
            return MainConstants.FileType.TYPE_DOC_WORD;
        } else if (memberOf(suffix, TXT_SUFFIXES)) {
            return MainConstants.FileType.TYPE_DOC_TXT;
        } else if (memberOf(suffix, COMPRESS_SUFFIXES)) {
            return MainConstants.FileType.TYPE_COMPRESS;
        } else if (memberOf(suffix, EXECUTE_SUFFIXES)) {
            return MainConstants.FileType.TYPE_EXECUTE;
        } else {
            return MainConstants.FileType.TYPE_UNKNOWN;
        }
    }

然而,你没想到的是——我还有更高的复杂度:

 /**
     * 是否是数组中的一个成员
     */
    public static boolean memberOf(String one, String[] members) {
        if (members != null) {
            for (String member : members) {
                if (member.equalsIgnoreCase(one)) {
                    return true;
                }
            }
        }
        return false;
    }

这样,我的代码很容易遍历所有文件的时候就是一次性遍历三种分类的文件,即图片类型文件,音视频类型文件,文档型文件。然后这个操蛋的获取类型的方式。我成功的把分类时间干到了15s以上。我可真牛逼。我还在为我搞出了一个很好的常量表而沾沾自喜呢。
大概是这样的:

 interface GroupTypes {
        String[] IMAGE_SUFFIXES=new String[]{"png","gif", "jpg", "jpeg", "bmp"};
        String[] AUDIO_SUFFIXES=new String[]{"wav", "ogg", "mid", "mp2", "mp3", "aac", "m4a"};
        String[] VIDEO_SUFFIXES=new String[]{"mp4", "mkv", "mpg", "mpeg", "mpeg", "swf", "3gp", "avi", "flv", "wmv", "webm"};
        String[] PDF_SUFFIXES=new String[]{"pdf"};
        String[] PPT_SUFFIXES=new String[]{"ppt","pptx"};
        String[] EXCEL_SUFFIXES=new String[]{"xls","xlsx"};
        String[] WORD_SUFFIXES=new String[]{"doc","docx"};
        String[] TXT_SUFFIXES=new String[]{"txt","log","rtf","xml"};
        String[] COMPRESS_SUFFIXES=new String[]{"zip","rar"};
        String[] EXECUTE_SUFFIXES=new String[]{"apk"};
    }

    /**
     * 文件类型采用组合类型方式
     * 对于int的低16位都是类型
     * 对于低16位中的高8位,是父类型,共有256种取值
     * 对于低16位中的低8位,是子类型,共有256种取值
     * 从子类型转到父类新子类型&0xff00 == 父类型
     */
    interface FileType {
        /**
         * 未知类型
         */
        int TYPE_UNKNOWN = 0x0000;

        /**
         * 文件类型
         */
        int TYPE_FOLDER = 0x0100;

        /**
         * 图片资源类
         */
        int TYPE_IMAGE = 0x0200;

        /**
         * 视频大类
         */
        int TYPE_VIDEO_OR_AUDIO = 0x0300;//父类型

        //子类型
        int TYPE_VIDEO = 0x0301;
        int TYPE_AUDIO = 0x0302;

        /**
         * 文档类型
         */
        int TYPE_DOC = 0x0400;//父类型

        //子类型
        int TYPE_DOC_PDF = 0x0401;
        int TYPE_DOC_PPT = 0x0402;
        int TYPE_DOC_EXCEL = 0x0403;
        int TYPE_DOC_WORD = 0x0404;
        int TYPE_DOC_TXT = 0x0405;

        /**
         * 压缩文件类型
         */
        int TYPE_COMPRESS=0x0500;

        /**
         * 安卓可执行文件
         */
        int TYPE_EXECUTE=0x0600;

        /**
         * 隐藏文件
         */
        int TYPE_HIDDEN=0x0700;

        /**
         * 子类转父类的掩码
         */
        int PARENT_MASK=0xff00;
    }

二、似乎渐入佳境,然而也只是掩耳盗铃

直接比较后缀这种方式确实傻,一次性加载所有类型的文件到map,后面获取其他类型的文件从map中去读不可取,“牺牲你第一次的时间,换来你后面更快的速度。”是行不通的。所以这一次,我做了一点点改变。我不再直接比较后缀了,也不会说上来就把所有的文件先加载好,后面你用的时候在从map中拿。我是用了正则去匹配文件的后缀,并且加入了LRUCache。
使用正则分类,但是,还是一连串的if

/**
   * 获取文件类型
   */
  public static @FileTypeAnnotation int getFileType(File file) {
    if (file.isHidden()) {
      return MainConstants.FileType.TYPE_HIDDEN;
    }
    if (file.isDirectory()) {
      return MainConstants.FileType.TYPE_FOLDER;
    }
    String fileName=file.getName().toLowerCase();
    if(match(fileName,IMAGE_PATTERN))
      return MainConstants.FileType.TYPE_IMAGE;
    if(match(fileName,AUDIO_PATTERN))
      return MainConstants.FileType.TYPE_AUDIO;
    if(match(fileName,VIDEO_PATTERN))
      return MainConstants.FileType.TYPE_VIDEO;
    if(match(fileName,DOC_PATTERN)){
      if(match(fileName,PDF_PATTERN))
        return MainConstants.FileType.TYPE_DOC_PDF;
      if(match(fileName,PPT_PATTERN))
        return MainConstants.FileType.TYPE_DOC_PPT;
      if(match(fileName,EXCEL_PATTERN))
        return MainConstants.FileType.TYPE_DOC_EXCEL;
      if(match(fileName,WORD_PATTERN))
        return MainConstants.FileType.TYPE_DOC_WORD;
      if(match(fileName,TXT_PATTERN))
        return MainConstants.FileType.TYPE_DOC_TXT;
    }
    if(match(fileName,COMPRESS_PATTERN))
      return MainConstants.FileType.TYPE_COMPRESS;
    if (match(fileName,EXCUTE_PATTERN))
      return MainConstants.FileType.TYPE_EXECUTE;
    return MainConstants.FileType.TYPE_UNKNOWN;
  }

不再使用文件过滤器去拿文件,而是用正则去匹配文件的后缀


  private static List<File> getAllFiles(File dir, Pattern regex) {
    List<File> result = new ArrayList<>();
    if (dir.isDirectory()) {
      File[] files = dir.listFiles();
      for (File file : files) {
        result.addAll(getAllFiles(file, regex));
      }
    } else {
      if (regex != null) {
        if (regex.matcher(dir.getName()).matches()) {
         if(!dir.isHidden())
           result.add(dir);
        }
      }
    }
    return result;
  }

  private static List<File> getAllAudioAndVideo(File dir) {
    return getAllFiles(dir, MainConstants.Patterns.AV_PATTERN);
  }

  private static List<File> getAllDoc(File dir) {
    return getAllFiles(dir, MainConstants.Patterns.DOC_PATTERN);
  }

  private static List<File> getAllImages(File dir) {
    return getAllFiles(dir, MainConstants.Patterns.IMAGE_PATTERN);
  }

正则:

 interface Regexs{
        String IMAGE_REGEX="^.+\\.(?i)(png|gif|jp(e)?g|bmp)$";
        String AUDIO_REGEX="^.+\\.(?i)(aac|wav|ogg|m(id|p(2|3)))$";
        String VIDEO_REGEX="^.+\\.(?i)(m(kv|p(4|(e)?g))|swf|3gp|avi|flv|w(mv|ebm))$";
        String AV_REGEX="^.+\\.(?i)(m(id|kv|p(2|3|4|(e)?g))|swf|3gp|a(vi|ac)|flv|w((a|m)v|ebm)|ogg)$";
        String DOC_REGEX="^.+\\.(?i)(p(df|pt(x)?)|((xls|doc)(x)?)|txt|log|rtf|xml)$";
        String PDF_REGEX="^.+\\.(?i)(pdf)$";
        String PPT_REGEX="^.+\\.(?i)(ppt(x)?)$";
        String EXCEL_REGEX="^.+\\.(?i)(xls(x)?)$";
        String WORD_REGEX="^.+\\.(?i)(doc(x)?)$";
        String TXT_REGEX="^.+\\.(?i)(txt|log|rtf|xml)$";
        String COMPRESS_REGEX="^.+\\.(?i)(zip|rar)$";
        String EXCUTE_REGEX="^.+\\.(?i)(apk)$";
    }

    interface Patterns{
        Pattern IMAGE_PATTERN=Pattern.compile(Regexs.IMAGE_REGEX);
        Pattern AUDIO_PATTERN=Pattern.compile(Regexs.AUDIO_REGEX);
        Pattern VIDEO_PATTERN=Pattern.compile(Regexs.VIDEO_REGEX);
        Pattern DOC_PATTERN=Pattern.compile(Regexs.DOC_REGEX);
        Pattern PDF_PATTERN=Pattern.compile(Regexs.PDF_REGEX);
        Pattern PPT_PATTERN=Pattern.compile(Regexs.PPT_REGEX);
        Pattern EXCEL_PATTERN=Pattern.compile(Regexs.EXCEL_REGEX);
        Pattern WORD_PATTERN=Pattern.compile(Regexs.WORD_REGEX);
        Pattern TXT_PATTERN=Pattern.compile(Regexs.TXT_REGEX);
        Pattern COMPRESS_PATTERN=Pattern.compile(Regexs.COMPRESS_REGEX);
        Pattern EXCUTE_PATTERN=Pattern.compile(Regexs.EXCUTE_REGEX);
        Pattern AV_PATTERN=Pattern.compile(Regexs.AV_REGEX);
    }

这一次,我第一次分类的时候去拿1w文件所耗费的时间大概是4-5s左右

三、更进一步,使用非递归的广度优先搜索

使用非递归的广度优先遍历,少一层入栈(队),节约了一丢丢时间,并且,可以拿到一部分数据就返回一点数据了

/**
   * 文件遍历非递归方式
   * @param dir
   * @param regex
   * @return
   */
  private static List<File> getAllFilesNR(File dir, @NonNull Pattern regex) {
    List<File> result = new LinkedList<>();
    Queue<File> queue = new LinkedList<>();
    queue.add(dir);
    while (!queue.isEmpty()) {
      File temp = queue.remove();
      if (temp.isDirectory()) {
        File[] files = temp.listFiles();
        if (files != null)
          for (File file : files)
            if (file.isDirectory()) //文件夹
              queue.add(file);
            else //文件
                if(regex.matcher(file.getName()).matches())
                    result.add(file);
      }
    }
    return result;
  }

不同的文件分类使用不同的正则,减少了一些语句

  public static @FileTypeAnnotation int getDocType(File file){
    String fileName = file.getName().toLowerCase();
    if (match(fileName, PDF_PATTERN)) {
      return MainConstants.FileType.TYPE_DOC_PDF;
    }
    if (match(fileName, PPT_PATTERN)) {
      return MainConstants.FileType.TYPE_DOC_PPT;
    }
    if (match(fileName, EXCEL_PATTERN)) {
      return MainConstants.FileType.TYPE_DOC_EXCEL;
    }
    if (match(fileName, WORD_PATTERN)) {
      return MainConstants.FileType.TYPE_DOC_WORD;
    }
    if (match(fileName, TXT_PATTERN)) {
      return MainConstants.FileType.TYPE_DOC_TXT;
    }
    return MainConstants.FileType.TYPE_UNKNOWN;
  }

  public static @FileTypeAnnotation int getAVType(File file){
    String fileName = file.getName().toLowerCase();
    if (match(fileName, AUDIO_PATTERN)) {
      return MainConstants.FileType.TYPE_AUDIO;
    }
    if (match(fileName, VIDEO_PATTERN)) {
      return MainConstants.FileType.TYPE_VIDEO;
    }
    return MainConstants.FileType.TYPE_UNKNOWN;
  }

这一次,我第一次分类的时候去拿1w文件所耗费的时间大概是3-4s左右

四、给人们一种假象,善意的谎言

无需等全部文件都分类完,才去显示内容,而是只要有数据了,就去显示,虽然拿完所有的数据耗时还是原来的时间,但给人的感觉却是变快了。

private static void getAllFilesNR(String rootPathNav,File dir, @NonNull Pattern regex,@NonNull FileInfoCallback callback) {
    List<FileInfo> result = new LinkedList<>();
    Queue<File> queue = new LinkedList<>();
    queue.add(dir);
    while (!queue.isEmpty()) {
      File temp = queue.remove();
      if (temp.isDirectory()) {
        File[] files = temp.listFiles();
        if (files != null) {
          for (File file : files)
            if (file.isDirectory()) //文件夹
            {
              queue.add(file);
            } else //文件
              if (regex.matcher(file.getName()).matches()) {
                int fileType = getFileType(file);
                result.add(new FileInfo(file,fileType));
              }
        }
      }
      callback.update(result);
    }
    if (result.size() > cache.maxSize()) {
      cache.resize((int) (cache.maxSize() * 2));
    }
    cache.put(rootPathNav, result);
  }

五、山穷水尽疑无路,柳岸花明又一村

在java层,大概这也就是比较快的速度了吧,除了获取文件类型很慢,还有优化的空间。遍历文件的方式似乎再也没有更好的解决方案了。然而,我觉得遍历文件主要还是太慢了。要是我能提高这个速度,就好了。我也没想着能用c++做到主工程中去,只是觉得这个思路不错。然后我在后面花了点时间去写了个小demo去验证了一下,我使用jni去分类,而这一次,我虽然使用的是字符串比较这种笨办法去分类,但是却用了500ms。

int getFilesNR(char *path) {
    queue<string> mQueue;
    mQueue.push(path);
    while (!mQueue.empty()) {
        string currentPath = mQueue.front();
        mQueue.pop();
        DIR *dir = opendir(currentPath.c_str());
        if (dir == NULL) {
            perror("open dir error");
            continue;
        }
        Dirent ptr;
        while ((ptr = readdir(dir)) != NULL) {
            char *cur;//如果路劲过长加上文件名过长可能导致bug
            if (strcmp(ptr->d_name, ".") == 0 ||
                strcmp(ptr->d_name, "..") == 0)//current dir or parent dir
                continue;
            else if (ptr->d_type == 8 || ptr->d_type == 4) {//file or linked file
                //得到当前文件路径
                //为什么是2? 一个'/'加上一个'\0'
                int strLen = strlen(currentPath.c_str()) + strlen(ptr->d_name) + 2;
                cur = new char[strLen];
                memset(cur, '\0', sizeof(cur));
                sprintf(cur, "%s/%s", currentPath.c_str(), ptr->d_name);
                if (ptr->d_type == 8) {
                    LOGD("path:%s",ptr->d_name);//1.可以在这里对文件获取类型和分类处理
                } else {
                    mQueue.push(cur);
                }
            }
            delete cur;
        }
        closedir(dir);
    }
    return 1;
}

上述代码注释1处我首先是使用笨办法对文件进行分类,写法如下:

bool isavs(char *str) {
    char **audioAndVideo = new char *[]{"mp3", "mp4", "ogg", "wav", "3gp", "avi"}
    char *suffix = getSuffix(str);
    bool flag = false;
    for (int i = 0; i < sizeof(audioAndVideo); ++i) {
        if (strcmp(suffix, audioAndVideo[i]) == 0) {
            flag = true;
            break;
        }
    }
    delete suffix;
    return flag;
}

后面我优化了根据后缀获取类型的方法,使用了tire树,然后这个速度就达到了现在的最优时间:100ms内,实测60ms左右。{虽然如此,但还是有优化的空间,那就是把变量放入native中,本地分页去取,可能能在50ms内,但是内存回收不好做,万一没有回收,就是内存泄露了。所以目前不打算这么做。}

/**
 * 获取路劲的后缀
 * @param path 路劲
 * @return 后缀
 */
char *getSuffix(char *path) {
    char *ret = strrchr(path, '.');
    if (ret) {
        return ret + 1;
    }
    return "";
}

int getFileType(SuffixTire *suffixTire, char *name) {
    char *suffix = getSuffix(name);
    int suffixLen = strlen(suffix);
    if (suffixLen == 0)
        return TYPE_UNKNOWN;
    int type = suffixTire->search(suffix);
    return type;
}

bool isImage(SuffixTire *suffixTire, char *name) {
    return getFileType(suffixTire, name) == TYPE_IMAGE;
}


bool isDocs(SuffixTire *suffixTire, char *name) {
    return (getFileType(suffixTire, name) & PARENT_MASK) == TYPE_DOC;
}

bool isAvs(SuffixTire *suffixTire, char *name) {
    return (getFileType(suffixTire, name) & PARENT_MASK) == TYPE_VIDEO_OR_AUDIO;
}

SuffixNode:

class SuffixNode {
public:
    char ch;
    int type;
    bool isLeaf;
    vector<SuffixNode> children;
public:
    SuffixNode() : ch(NULL), type(0), isLeaf(false),children(vector<SuffixNode>()) {
    }

    ~SuffixNode(){

    }
};

SuffixTire

class SuffixTire {
private:
    SuffixNode *root;
public:
    SuffixTire() {
        root = new SuffixNode();
    }

    /**
     * 构造字典树
     * @param suffix 后缀
     * @param type 后缀类型
     */
    void insert(char *suffix, int type);

    /**
     * 查询字典
     * @param suffix 后缀
     * @return  后缀类型
     */
    int search(char* suffix);

    ~SuffixTire(){
        LOGD("NDK:SuffixTire析构");
        delete root;
    }
};

void SuffixTire::insert(char *suffix, int type) {
    LOGD(">>>SuffixTire insert, suffixes: %s,type:%d", suffix, type);
    assert(suffix != NULL);
    int suffixLen = strlen(suffix);
    assert(suffixLen != 0);
    SuffixNode *node = root;
    for (int i = 0; i < suffixLen; ++i) {
        int index = 0;
        for (; index < node->children.size(); ++index) {
            if (node->children[index].ch == suffix[i])
                break;
        }

        if (index < node->children.size()) {//找到了
            node = &(node->children[index]);
        } else if (index == node->children.size()) {//未找到节点
            SuffixNode temp;
            temp.ch = suffix[i];
            node->children.push_back(temp);
            node = &(node->children.back());
        }
    }
    node->isLeaf = true;
    node->type = type;
    LOGD("<<<SuffixTire insert");
}

int SuffixTire::search(char *suffix) {
    assert(suffix != nullptr);
    int suffixLen = strlen(suffix);
    assert(suffixLen != 0);
    SuffixNode *node = root;
    for (int i = 0; i < suffixLen; ++i) {
        int index = 0;
        for (; index < node->children.size(); ++index) {
            if (node->children[index].ch == suffix[i])
                break;
        }
        if (index == node->children.size()) {//未找到
            return 0;
        }
        node = &(node->children[index]);
    }
    if (node->isLeaf)
        return node->type;
    else
        return 0;
}

最后,java也是用字典树,ndk也是用字典树,所有文件相同,在我们的产品上的耗时差距比较。


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

推荐阅读更多精彩内容

  • 夜莺2517阅读 127,712评论 1 9
  • 版本:ios 1.2.1 亮点: 1.app角标可以实时更新天气温度或选择空气质量,建议处女座就不要选了,不然老想...
    我就是沉沉阅读 6,878评论 1 6
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,523评论 28 53
  • 兔子虽然是枚小硕 但学校的硕士四人寝不够 就被分到了博士楼里 两人一间 在学校的最西边 靠山 兔子的室友身体不好 ...
    待业的兔子阅读 2,586评论 2 9