iOS Link Map File计算探索

1、Link Map File 是啥

我们编写的OC代码需要经过预编译->编译->汇编->链接(静态链接),最终生成一个可执行文件。汇编阶段完成后,每个类都会生成一个对应的.o文件(可重定位的目标文件),在链接阶段(静态),会把所有的.o文件链接到一起,生成一个可执行文件。Link Map File就是这样一个记录链接相关信息的纯文本文件,里面记录了可执行文件的路径、CPU架构、目标文件、符号等信息。

2、探索 Link Map File 的意义

探索 Link Map File 可以帮助我们更好的理解链接过程、理解内存分段及分区、分析可执行文件中哪个类或库占用比较大,进行安装包瘦身

苹果对上传到App Store上的app大小也有严格的规定 Apple 规定最大构建版本文件大小

1599047151702-c23d1104-6c40-487d-a031-a20241d1cb94.png

翻译:
执行文件大小是指执行文件的__TEXT部分

  • 当iOS最低版本小于7.0,最多为80MB;
  • 想iOS系统版本位于 7.0~8.0之间时,每个分区是60MB(并不是指32位+64位最多为120MB,当32位分区占用50MB,64位分区占用61MB,总111MB也不行,因为64位分区超出了);
  • 当iOS大于等于9.0,总限制500MB;

对App来说,可以用Link Map File 来分析各个文件占用大小,有针对性进行优化。
对SDK来说,也可以用Link Map File 来计算自身大小,控制好大小这个度。一般大App对集成二方和三方这样的SDK大小是有很严格的限制。

3、如何生成 Link Map File

Xcode 默认情况下是不会去生成 Link Map File,需要开发者自己手动去打开生成配置开关,如下图,Target -> Build Setting -> Linking -> Write Link Map File 为 YES

1599045570555-e26f5c29-099f-49eb-959e-151df97fa7fa.png

存放 Link Map File 文件路径如下图

1599045684267-815539f6-c00f-4b44-9839-c14f08948452.png

默认存放路径是

$(TARGET_TEMP_DIR)/$(PRODUCT_NAME)-LinkMap-$(CURRENT_VARIANT)-$(CURRENT_ARCH).txt

最终翻译成实体文件路径如下

/Users/chao/Library/Developer/Xcode/DerivedData/Demo-fvacayrprqdluqdcpzegtlzjjoqp/Build/Intermediates.noindex/Demo.build/Debug-iphonesimulator/Demo.build/Demo-LinkMap-normal-x86_64.txt

我们也可更改 Link Map File 文件存放位置,这个要看自己需要了

4、Link Map File 组成

1. Path:生成可执行文件的路径

# Path: /Users/chao/Library/Developer/Xcode/DerivedData/Demo-fvacayrprqdluqdcpzegtlzjjoqp/Build/Products/Debug-iphonesimulator/Demo.app/Demo

2. Arch:架构类型

# Arch: x86_64

3. Object files:列举了可执行文件里所有的obj以及tbd。

# Object files:
[  0] linker synthesized
[  1] /Users/chao/Library/Developer/Xcode/DerivedData/Demo-fvacayrprqdluqdcpzegtlzjjoqp/Build/Intermediates.noindex/Demo.build/Debug-iphonesimulator/Demo.build/Demo.app-Simulated.xcent
[  2] /Users/chao/Library/Developer/Xcode/DerivedData/Demo-fvacayrprqdluqdcpzegtlzjjoqp/Build/Intermediates.noindex/Demo.build/Debug-iphonesimulator/Demo.build/Objects-normal/x86_64/ViewController.o
[  3] /Users/chao/Library/Developer/Xcode/DerivedData/Demo-fvacayrprqdluqdcpzegtlzjjoqp/Build/Intermediates.noindex/Demo.build/Debug-iphonesimulator/Demo.build/Objects-normal/x86_64/AFSecurityPolicy.o
[  4] /Users/chao/Library/Developer/Xcode/DerivedData/Demo-fvacayrprqdluqdcpzegtlzjjoqp/Build/Intermediates.noindex/Demo.build/Debug-iphonesimulator/Demo.build/Objects-normal/x86_64/AppDelegate.o
...

4. Sections:主要用展示代码和数据在内存中的分布,涉及到Mach-O可执行文件内存分布相关,这里不做过多解释

# Sections:
# Address   Size        Segment Section
0x100004350 0x0002308D  __TEXT  __text
0x1000273DE 0x000002A0  __TEXT  __stubs
0x100027680 0x00000470  __TEXT  __stub_helper
......
0x100030000 0x00000008  __DATA  __nl_symbol_ptr
0x100030008 0x000000F0  __DATA  __got
0x1000300F8 0x00000380  __DATA  __la_symbol_ptr
......

5. Symbols:记录符号相关信息,也是我们统计的关键

# Symbols:
# Address   Size        File  Name
0x100004350 0x00000040  [  2] -[ViewController viewDidLoad]
0x100004390 0x000000E0  [  2] -[ViewController touchesBegan:withEvent:]
0x100004470 0x00000050  [  2] ___41-[ViewController touchesBegan:withEvent:]_block_invoke
0x1000044C0 0x00000080  [  2] ___41-[ViewController touchesBegan:withEvent:]_block_invoke_2
0x100004540 0x00000074  [  2] ___41-[ViewController touchesBegan:withEvent:]_block_invoke_3
0x1000045C0 0x00000330  [  3] +[AFSecurityPolicy certificatesInBundle:]
0x1000048F0 0x00000080  [  3] +[AFSecurityPolicy defaultPolicy]

根据Sections的起始地址,可以将Symbols分为Sections个数的组
Symbols包含的信息有:

  • Address:符号起始地址
  • Size:所占内存大小(16进制)。
  • File:该Name所在的文件编号,也就是Object files部分的中括号的数字,例如-[ViewController viewDidLoad]对应的文件编号为[ 2],根据Object files部分可以看到所属的文件为:ViewController.o。这样可以计算某个.o文件所占内存的大小。只需要把Symbols中编号为.o对应编号符号累加统计即可。
  • Name:符号的名称。

6. Dead Stripped Symbols:链接器认为无用的符号,统计大小的时候不统计它下面的符号

# Dead Stripped Symbols:
#           Size        File  Name
<<dead>>    0x00000018  [  2] CIE
<<dead>>    0x00000015  [  3] literal string: supportsSecureCoding
<<dead>>    0x0000000F  [  3] literal string: SSLPinningMode
<<dead>>    0x00000011  [  3] literal string: pinnedPublicKeys

5、Link Map File 信息统计

接下里正式进入本文主题,使用 Link Map 文件统计各个文件的大小。

说到统计,真的是心酸的不行,开始写了一个shell的统计脚本,结果发现计算速度太慢(主要是没有面向对象的思想,也没有我想要的数据结构,例如map)。为了加快统计的速度,我用OC写了个linkmap计算程序,速度是快了,但是想到运行OC毕竟要装Xcode才能运行,哎!还能怎样,接着探索呗。这个时候我想到一门语言,Python! 面向对象写脚本,正是我需要的,于是扒资料扒教程得去学习Python,学完出山,写了一个linkmap.sh程序,计算速度果然大幅度增加,比较满意。这样就满足了吗?不!生命不息,折腾不止!这让我想起了之前无意间看到C++之父说过的一句话,大概的意思是“我为全球变暖做出的贡献就是让C ++运行更高效”,这句话激励了我去重新去认识C ++这门语言,于是我买了李明杰的C++课程,一番学习下来收获颇丰,于是用C ++重写了计算程序。

5.1 人狠话不多,先上耗时对比

首先我用工程编译出一个 Link Map File,有 1.7w 多行符号,也就是说脚本要解析计算1.7w多行文本信息

程序 耗时
shell脚本 548.183s
Python脚本 0.231s
C++程序 0.107s

5.2 慢速的shell

初期写了个shell统计脚本,但是shell脚本没有map这样的数据结构,这让人很痛苦,因为一个文件可能对应很多符号,解析到某一个符号之后,首先要找出文件,再找出文件之前统计的大小,加上该符号大小,以此类推。设计上我用了两个数组,一个装文件编号,一个装文件对应累加大小,两个数组通过位置对应起来。解析某个文件对应的符号需要遍历两个数组,所以慢的要死。脚本代码如下

#!/bin/sh

if [[ $# < 1 ]]; then
  echo "脚本正确使用方式:./linkmap.sh <link-map-file-pat> <keyword>"
  echo "示例:./linkmap.sh ./linkmap.txt"
  echo "示例:./linkmap.sh ./linkmap.txt ATAuthSDK"
  exit 0
fi

declare -a file_number_arr
declare -a file_name_arr
declare -a file_size_arr

reach_files=0
reach_sections=0
reach_symbols=0

number=`cat $1 | wc -l`
progress_view=''
current_number=0
progress=0

while read line
do
  #进度条相关
  ((current_number++))
  if [[ $((current_number * 100 / number)) -ge $((progress + 1)) ]]; then
    progress_view+='#'
    progress=$((current_number * 100 / number))
  fi
  printf "[%-100s] %d%% \r" "$progress_view" "$progress";


  if [[ -n `echo $line | grep '^# Object files:'` ]]; then
    reach_files=1
  elif [[ -n `echo $line | grep '^# Sections:'` ]]; then
    reach_sections=1
  elif [[ -n `echo $line | grep '^# Symbols:'` ]]; then
    reach_symbols=1
  elif [[ -n `echo $line | grep '^# Dead Stripped Symbols:'` ]]; then
    break;
  fi

  if [[ $reach_files -ne    0 && $reach_sections -eq 0 && $reach_symbols -eq 0 ]]; then
    if [[ -n `echo $line | grep ']'` ]]; then
      count=${#file_number_arr[@]}
      file_number_arr[count]=`echo $line | egrep -o '\[([0-9 ]*)\]'`
      file_name_arr[count]=${line#*] }
      file_size_arr[count]=0
    fi
  elif [[ $reach_files -ne  0 && $reach_sections -ne 0 && $reach_symbols -ne 0 ]]; then
    file_number=`echo $line | egrep -o '\[([0-9 ]*)\]'`
    file_size=`echo ${line%]*} | cut -d ' ' -f 2`
    echo "第 $current_number 行 大小为:$file_size" >> log.txt
    if [[ -n "${file_number}" && -n "${file_size}" ]]; then
      idx=-1
      for (( i = 0; i < ${#file_number_arr[@]}; i++ )); do
        if [[ "${file_number_arr[$i]}" = "${file_number}" ]]; then
          idx=$i
        fi
      done

      if [[ $idx -ge 0 ]]; then
        file_size_arr[$idx]=$(( ${file_size_arr[$idx]} + ((file_size)) ))
      fi
    fi
  fi

  ((tag++))
done < "${1}"

total_size=0

printf "\n%-40s \t\t %-10s %-20s\n" "文件名" "文件编号" "文件大小"
for (( i = 0; i < ${#file_name_arr[@]}; i++ )); do
  if [[ -n "${2}" ]]; then
    if [[ -n `echo "${file_name_arr[i]}" | grep "${2}"` ]]; then
      printf "%-40s \t %-10s %-20s\n" "${file_name_arr[$i]##*/}" "${file_number_arr[$i]}" "${file_size_arr[$i]} byte"
      ((total_size+=${file_size_arr[$i]}))
    fi
  else
    printf "%-40s \t %-10s %-20s\n" "${file_name_arr[$i]##*/}" "${file_number_arr[$i]}" "${file_size_arr[$i]} byte"
    ((total_size+=${file_size_arr[$i]}))
  fi
done

echo "\n总大小 ${total_size} byte ≈ $((total_size / 1024)) kb"

5.3 高铁速度的Python

对比上面的shell脚本,Python脚本快了不止一倍两倍,上面有数据统计。因为Python是面向对象的编程语言,所以代码上更清晰明朗,看起来也很舒服,同时还增加了排序功能,可以根据需要,选择文件名排序、文件大小升序排序、文件大小降序排序中的一种,只需要传入不同的参数即可实现。

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import sys,os
from enum import Enum, unique

@unique
class LMPStep(Enum):
    Initial     = 0
    Object      = 1
    Section     = 2
    Symbols     = 3
    Finish      = 4

class LMPOrder(Enum):
    FileName    = 0 #按文件名输出显示
    SizeAsc     = 1 #按文件大小升序排序
    SizeDesc    = 2 #按文件大小降序排序


class LMPModel(object):
    "符号信息记录类"
    def __init__(self, number, path, name, size=0):
        self.number = number
        self.path = path
        self.name = name
        self.size = size
    def add(self, size):
        self.size += size
    def __str__(self):
        return 'number=%s,name=%s,size=%d' \
        % (self.number, self.name, self.size)

def build_symbol_model(line):
    "根据 # Object files: 中的行构建 LCSymbolModel 对象"
    if '[' not in line or ']' not in line:
        return None
    snumber = line[line.index('['):line.index(']') + 1]
    spath = line[line.index(']') + 1:]
    sname = spath.split('/')[-1].strip()
    model = LMPModel(snumber, spath, sname)
    return model

def parse_linkmap_file(filepath):
    if os.path.exists(filepath) == False:
        print '请检查 %s 文件是否存在' % filepath
        return None
    try:
        linkmap_file = open(filepath, mode='r')
    except Exception as e:
        print '%s 文件读取失败' % filepath
        return None
    dict = {}
    step = LMPStep.Initial
    line = linkmap_file.readline()
    while line:
        line = line.strip()
        if len(line) > 0:
            if line.startswith('#'):
                if line.startswith('# Object files:'):
                    step = LMPStep.Object
                elif line.startswith('# Sections:'):
                    step = LMPStep.Section
                elif line.startswith('# Symbols:'):
                    step = LMPStep.Symbols
                elif line.startswith('# Dead Stripped Symbols:'):
                    step = LMPStep.Finish
            else:
                if step == LMPStep.Object:
                    model = build_symbol_model(line)
                    if model and len(model.number) > 0:
                        dict[model.number] = model
                    else:
                        print 'Object 解析异常:%s' % line
                elif step == LMPStep.Symbols:
                    snumber = line[line.index('['):line.index(']') + 1]
                    array = line.split('\t')
                    if len(snumber) <= 0 or snumber not in dict or len(array) < 3:
                        print 'Symbols 解析异常:%s' % line
                    else:
                        size = int(array[1], 16)
                        dict[snumber].add(size)
                    pass
                elif step == LMPStep.Finish:
                    break
        line = linkmap_file.readline()
    linkmap_file.close()
    return dict

def show(dict, keyword = None, order = LMPOrder.FileName):
    total_size = 0
    if dict:
        if order == LMPOrder.SizeAsc.value:
            models = sorted(dict.values(), key = lambda m: m.size)
        elif order == LMPOrder.SizeDesc.value:
            models = sorted(dict.values(), key = lambda m: m.size, reverse=True)
        else:
            models = sorted(dict.values(), key = lambda m: m.name)
        for model in models:
            if keyword:
                if keyword in model.name:
                    print '%-40s %-6d byte' % (model.name, model.size)
                    total_size += model.size
            else:
                print '%-40s %-6d byte' % (model.name, model.size)
                total_size += model.size
    print '总大小 %d byte ≈ %.2f kb' % (total_size, total_size / 1024.0)

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print '脚本正确使用方式:./linkmap.py <link-map-file-pat> <keyword> <order(0:文件名 1:文件大小升序 2:文件大小降序)>'
        print '示例:./linkmap.py ./linkmap.txt'
        print '示例:./linkmap.py ./linkmap.txt ATAuthSDK'
        print '示例:./linkmap.py ./linkmap.txt ATAuthSDK 0'
        sys.exit(0)
    filepath = sys.argv[1]
    keyword = None
    order = LMPOrder.FileName
    if len(sys.argv) > 2:
        keyword = sys.argv[2]
    if len(sys.argv) > 3:
        order = int(sys.argv[3])
    dict = parse_linkmap_file(filepath)
    if dict:
        show(dict, keyword, order)

5.4 火箭速度的C++

#include <string>
#include <unordered_map>
#include <fstream>
#include <vector>
#include <iomanip>
using namespace std;

class Model {
    string number;
    string path;
    string name;
    int size;
public:
    Model();
    Model(string number, string path);
    void add(int size);
    string getNumber();
    string getPath();
    string getName();
    int getSize();
    friend ostream& operator<<(ostream &, const Model&);
};

void parse_linkmap_file(string&, unordered_map<string, Model>&);
void show(unordered_map<string, Model>&, string&, int);

int main(int argc, const char * argv[]) {
    //1. 参数检查
    if (argc < 2) {
        cout << "命令正确使用方式:./linkmap <link-map-file-path> <keyword> <order(0:文件名 1:文件大小升序 2:文件大小降序)>" << endl;
        cout << "示例:./linkmap ./linkmap.txt" << endl;
        cout << "示例:./linkmap ./linkmap.txt ATAuthSDK" << endl;
        cout << "示例:./linkmap ./linkmap.txt ATAuthSDK 0" << endl;
        return 0;
    }
    string filepath = argv[1];
    string keyword{};
    if (argc > 2) {
        keyword = argv[2];
    }
    int order = 0;
    if (argc > 3) {
        order = *(argv[3]) - '0';
    }
    
    //2. 解析
    unordered_map<string, Model> map = {};
    parse_linkmap_file(filepath, map);
    
    //3. 展示
    show(map, keyword, order);
    return 0;
}


Model::Model() {}
Model::Model(string number, string path) : number(number), path(path), size(0) {
    string::size_type idx = path.rfind('/');
    if (idx == string::npos) {
        name = path;
    } else {
        name = path.substr(idx + 1);
    }
}
string Model::getNumber() { return number; }
string Model::getPath() { return path; }
string Model::getName() { return name; }
int Model::getSize() { return size; }

void Model::add(int size) {
    this->size += size;
}

bool starts_with(const string& s1, const string& s2) {
    return s1.size() >= s2.size() && s1.compare(0, s2.size(), s2) == 0;
}
vector<string> split(const string& str, const string& delim) {
    vector<string> tokens;
    size_t prev = 0, pos = 0;
    do {
        pos = str.find(delim, prev);
        if (pos == string::npos) {
            pos = str.size();
        }
        string token = str.substr(prev, pos - prev);
        if (!token.empty()) {
            tokens.push_back(token);
        }
        prev = pos + delim.size();
    } while (pos < str.size() && prev < str.size());
    return tokens;
}

string find_number(const string& line) {
    string::size_type ns = line.find('[');
    string::size_type ne = line.find(']');
    if (ns == string::npos || ne == string::npos || ns > ne) {
        return string();
    }
    return line.substr(ns, ne - ns + 1);
}
Model build_model(const string& line) {
    string snumber = find_number(line);
    if (snumber.size() == 0) {
        return Model();
    }
    string spath = line.substr(line.find(']')+2);
    return Model(snumber, spath);
}

void parse_linkmap_file(string& filepath, unordered_map<string, Model>& map) {
    ifstream fin(filepath);
    if (!fin) {
        cout << "请检查 " << filepath << " 是否存在" << endl;
        return;
    }
    enum { Initial, Object, Section, Symbols, Finish } step;
    step = Initial;
    string line;
    while (getline(fin, line)) {
        if (line.size() == 0) continue;
        if (starts_with(line, "#")) {
            if (starts_with(line, "# Object files:")) {
                step = Object;
            }
            else if (starts_with(line, "# Sections:")) {
                step = Section;
            }
            else if (starts_with(line, "# Symbols:")) {
                step = Symbols;
            }
            else if (starts_with(line, "# Dead Stripped Symbols:")) {
                step = Finish;
            }
        }
        else {
            if (step == Object) {
                Model m = build_model(line);
                if (m.getNumber().size() > 0) {
                    map[m.getNumber()] = m;
                } else {
                    cout << "Object 解析异常:" << line << endl;
                }
            }
            else if (step == Symbols) {
                string snumber = find_number(line);
                vector<string> array = split(line, "\t");
                if (snumber.size() == 0 || map.count(snumber) == 0 || array.size() < 3) {
                    cout << "Symbols 解析异常:" << line << endl;
                }
                else {
                    int size = (int)strtol(array[1].c_str(), nullptr, 16);
                    unordered_map<string, Model>::iterator it = map.find(snumber);
                    it->second.add(size);
                }
            }
            else if (step == Finish) {
                break;
            }
        }
    }
    fin.close();
}

void show(unordered_map<string, Model>& map, string& keyword, int order) {
    typedef pair<string, Model> Pair;

    int total_size = 0;
    vector<Pair> pairs(map.begin(), map.end());
    sort(pairs.begin(), pairs.end(), [=](Pair& lhs, Pair& rhs) {
        if (order == 1) {
            return lhs.second.getSize() < rhs.second.getSize();
        }
        else if (order == 2) {
            return lhs.second.getSize() > rhs.second.getSize();
        }
        else {
            return lhs.second.getName() < rhs.second.getName();
        }
    });
    
    for (int i = 0; i != pairs.size(); ++i) {
        Model model = pairs[i].second;
        if (keyword.size() > 0) {
            if (model.getName().find(keyword) != string::npos) {
                cout << model << endl;
                total_size += model.getSize();
            }
        }
        else {
            cout << model << endl;
            total_size += model.getSize();
        }
    }
    
    cout << "总大小 " << total_size << " byte ≈ " << total_size / 1024.0 << " kb" << endl;
}

ostream& operator<<(ostream& os, const Model& m) {
    os << left << setw(8) <<  m.number << left << setw(40) << m.name << m.size;
    return os;
}

C++ 代码写出来了,接下来就是编译成可执行文件了

5.4.1 Mac OS 系统下

当然是使用Xcode自带的Clang编译器了,虽然底层用的还是GCC,编译指令如下

clang++ -std=c++11 -stdlib=libc++ linkmap.cpp -o linkmap
  • -std指定编译的标准,我这里选择的是C++11这个标准,因为我的代码是基于该标准开发的
  • -stdlib指定C++标准库
  • -o后面指定编译成可执行文件的名字,不指定会默认编译成a.out(ps:其实这里的a.out是Unix系统很早版本的可执行文件格式后缀,感兴趣可以查下相关资料)

这两个编译配置在Xcode里面也能找到

1599055244953-b809d6a6-7d97-4c73-8eab-b2c092249e21.png

现在已经编译出来了可执行文件了(名字为 linkmap),让我们一起开心的使用,我的linkmap 可执行文件放在 Documents 目录下

# 1. cd 到 linkmap可执行文件目录下
cd ~/Documents

# 2. 使用 linkmap 计算程序
./linkmap LinkMap.txt

NO!这不是我们想要的,每次还要cd到可执行文件对应的目录下,才能使用该指令,我在其他目录下就不能使用了吗?我想要随心所欲地用,任何地方打开终端就可以用。这个时候我让我想到了环境变量PATH,想到咱就来,没有一丝犹豫

首先回顾下Mac系统默认配置文件加载顺序:/etc/profile /etc/paths ~/.bash_profile
~/.bash_login ~/.profile ~/.bashrc,其中/etc/profile/etc/paths是系统级别的,系统启动就会加载,后面三个是当前用户级的环境变量。如果~/.bash_profile文件存在,则后面的两个文件就会被忽略不读了,如果~/.bash_profile文件不存在,才会以此类推读取后面的文件。~/.bashrc没有上述规则,它是bash shell打开的时候载入的。

如果让我来选择配置环境变量,肯定是放在~/.bash_profile文件里面喽,但是因为我的电脑安装了zsh,导致~/.bash_profile不会被执行,而是执行~/.zshrc,所以这里我会放到~/.zshrc里面去配置环境变量,当然如果你安装了zsh,同时又在 ~/.zshrc 执行了~/.bash_profilesource ~/.bash_profile),那也可以配置在~/.bash_profile里面

接下来正式开始环境变量配置工作:
1、首先我们在用户目录下创建一个 mybin 目录专门用来存放我们自己写的程序指令

mkdir ~/mybin

2、将我们编译好的 linkmap 可执行文件放到新建好的~/mybin目录下
3、配置环境变量
将如下代码加入到对应的配置文件(ps:我的电脑因安装了zsh,所以要在~/.zshrc文件里面配置,如果没有安装zsh,需要在~/.bash_profile文件里面配置)

PATH=$PATH:~/mybin
export PATH

最后执行source ~/.zshrc或重启终端即可,这样我就可以在地方打开终端,随用所欲使用linkmap指令啦。

看下效果,我cd到不同目录下去调用linkmap指令,都能识别到。效果如下,舒服了!舒服了!

20211021163506.jpg

用起来方便、快捷、高效!满意!!!

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

推荐阅读更多精彩内容