iOS-性能优化实战

优化思路:
1、先了解工程结构。第三方库管理方式使用情况、本地资源文件使用情况
2、使用infer进行静态代码扫描
3、使用command+shift+b进行静态内存扫描
4、使用LSUnusedResources进行无用图片扫描,扫描后由开发检测这些图片,判断图片在项目中是否实际有用到
5、使用FindSelectorsUnrefs.py脚本检测无用类与方法
6、使用fdupes扫描重复文件
7、使用pmd扫描重复代码
8、使用智图工具压缩工程图片
9、检测工程+(void)load使用情况(搜索(void)load)
10、检测工程启动时间,查看工程启动流程代码
11、工程配置优化

一、使用infer进行静态代码扫描

静态扫描的目的是进行代码规范的执行检测,保障项目代码规范的一致性,增加代码的可阅读性、维护性。减少低级或隐藏错误

1、安装命令brew install infer

如遇报错:No such file or directory @ rb_sysopen - /Users/heyujia/Library/Caches/Homebrew/downloads/3f2311ffb10a2774d49c39dfca3e6fc69d0a951be4299c7c84383c5774481d62--sqlite-3.37.2.monterey.bottle.tar.gz
brew install sqlite-3.37.2.monterey.bottle.tar.gz
解决办法:brew update (更新homebrew 源,本人homebrew版本3.6.3)

如遇其他报错请检查HomeBrew的版本、安装路径、源路径等等。

2、使用以下命令进行扫描

# 第1步:编译工程生成编译日志xcodebuild.log文件
xcodebuild -workspace xxx.xcworkspace -scheme xxx -configuration Debug -sdk iphoneos COMPILER_INDEX_STORE_ENABLE=NO | tee xcodebuild.log
# 第2步:根据编译日志生成编译数据compile_commands.json文件
xcpretty -r json-compilation-database -o compile_commands.json < xcodebuild.log > /dev/null
# 第3步:基于编译数据compile_commands.json文件进行静态分析
infer run --skip-analysis-in-path Pods --no-xcpretty --keep-going --compilation-database-escaped compile_commands.json

3、查看report.txt文件,根据文件内容进行修改
扫描结果会产出在命令指定的工程目录下,根据扫描结果去进行修改。修改时根据项目和团队情况选择性修改,不必所有内容都修改。因为扫描规则是infer设置的,如果需要调整扫描规则需要修改infer。

二、command + shift + b 静态内存扫描

扫描的目的是从静态代码分析工程代码中有哪些明确的代码使用错误,减少内存占用,从而提升App性能。 一般只需要修复自身编写代码的错误,第三方库的不建议修改。

静态扫描结果

三、使用LSUnusedResources进行无用图片扫描

扫描的目的是减少无用资源,从而减少App体积以及启动时资源加载的耗时

1、下载LSUnusedResources
在官网下载工程后,用MyMac模拟器运行工程就会打开工具

2、开始扫描并删除无用图片

LSUnusedResources

四、使用FindSelectorsUnrefs.py脚本检测无用类与方法

通过减少工程中无用类与未使用的方法的方式,可以减少App包的体积,也可以增加App启动时连接加载的耗时,从而提升App启动速度。
因为FindSelectorsUnrefs.py是python脚本,因此需要有python环境。因为本人使用的是python3,所以下面脚本是针对python3的,如果是其他版本的python,语法会有所差异,差异的部分需要根据语法自行调整。

1、FindSelectorsUnrefs.py脚本内容

# coding:utf-8

import os
import re
import sys
import getopt

reserved_prefixs = ["-[", "+["]

# 获取入参参数
def input_parameter():
    opts, args = getopt.getopt(sys.argv[1:], '-a:-p:-w:-b:',
                               ['app_path=', 'project_path=', 'black_list_Str', 'white_list_str'])

    black_list_str = ''
    white_list_str = ''
    white_list = []
    black_list = []
    # 入参判断
    for opt_name, opt_value in opts:
        if opt_name in ('-a', '--app_path'):
            # .app文件路径
            app_path = opt_value
        if opt_name in ('-p', '--project_path'):
            # 项目文件路径
            project_path = opt_value
        if opt_name in ('-b', '--black_list_Str'):
            # 检测黑名单前缀,不检测谁
            black_list_Str = opt_value
        if opt_name in ('-w', '--white_list_str'):
            # 检测白名单前缀,只检测谁
            white_list_str = opt_value

    if len(black_list_str) > 0:
        black_list = black_list_str.split(",")

    if len(white_list_str) > 0:
        white_list = white_list_str.split(",")

    if len(white_list) > 0 and len(black_list) > 0:
        print("\033[0;31;40m白名单【-w】和黑名单【-b】不能同时存在\033[0m")
        exit(1)

    # 判断文件路径存不存在
    if not os.path.exists(project_path):
        print("\033[0;31;40m输入的项目文件路径【-p】不存在\033[0m")
        exit(1)

    app_path = verified_app_path(app_path)
    if not app_path:
        exit('输入的app路径不存在,停止运行')

    return app_path, project_path, black_list, white_list


def verified_app_path(path):
    if path.endswith('.app'):
        appname = path.split('/')[-1].split('.')[0]
        path = os.path.join(path, appname)
        if appname.endswith('-iPad'):
            path = path.replace(appname, appname[:-5])
    if not os.path.isfile(path):
        return None
    if not os.popen('file -b ' + path).read().startswith('Mach-O'):
        return None
    return path


# 获取protocol中所有的方法
def header_protocol_selectors(file_path):
    # 删除路径前后的空格
    file_path = file_path.strip()
    if not os.path.isfile(file_path):
        return None
    protocol_sels = set()
    file = open(file_path, 'r',encoding='utf-8',errors='ignore')
    is_protocol_area = False
    # 开始遍历文件内容
    for line in file.readlines():
        # 删除注释信息
        # delete description
        line = re.sub('\".*\"', '', line)
        # delete annotation
        line = re.sub('//.*', '', line)
        # 检测是否是 @protocol
        # match @protocol
        if re.compile('\s*@protocol\s*\w+').findall(line):
            is_protocol_area = True
        # match @end
        if re.compile('\s*@end').findall(line):
            is_protocol_area = False
        # match sel
        if is_protocol_area and re.compile('\s*[-|+]\s*\(').findall(line):
            sel_content_match_result = None
            # - (CGPoint)convertPoint:(CGPoint)point toCoordinateSpace:(id <UICoordinateSpace>)coordinateSpace
            if ':' in line:
                # match sel with parameters
                # 【"convertPoint:","toCoordinateSpace:"]
                sel_content_match_result = re.compile('\w+\s*:').findall(line)
            else:
                # - (void)invalidate;
                # match sel without parameters
                # invalidate;
                sel_content_match_result = re.compile('\w+\s*;').findall(line)

            if sel_content_match_result:
                # 方法参数拼接
                # convertPoint:toCoordinateSpace:
                funcList = ''.join(sel_content_match_result).replace(';', '')
                protocol_sels.add(funcList)
    file.close()

    return protocol_sels


# 获取所有protocol定义的方法
def protocol_selectors(path, project_path):
    print('获取所有的protocol中的方法...')
    header_files = set()
    protocol_sels = set()
    # 获取当前引用的系统库中的方法列表
    system_base_dir = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'
    # get system librareis
    lines = os.popen('otool -L ' + path).readlines()
    for line in lines:
        # 去除首尾空格
        line = line.strip()

        # /System/Library/Frameworks/MediaPlayer.framework/MediaPlayer (compatibility version 1.0.0, current version 1.0.0)
        # /System/Library/Frameworks/MediaPlayer.framework/MediaPlayer
        # delete description,
        line = re.sub('\(.*\)', '', line).strip()

        if line.startswith('/System/Library/'):

            # [0:-1],获取数组的左起第一个,到倒数最后一个,不包含最后一个,[1,-1)左闭右开
            library_dir = system_base_dir + '/'.join(line.split('/')[0:-1])
            if os.path.isdir(library_dir):
                # 获取当前系统架构中所有的类
                # 获取合集
                header_files = header_files.union(os.popen('find %s -name \"*.h\"' % library_dir).readlines())

    if not os.path.isdir(project_path):
        exit('Error: project path error')
    # 获取当前路径下面所有的.h文件路径
    header_files = header_files.union(os.popen('find %s -name \"*.h\"' % project_path).readlines())

    for header_path in header_files:
        # 获取所有查找到的文件下面的protocol方法,这些方法,不能用来统计
        header_protocol_sels = header_protocol_selectors(header_path)
        if header_protocol_sels:
            protocol_sels = protocol_sels.union(header_protocol_sels)
    return protocol_sels


def imp_selectors(path):
    print('获取所有的方法,除了setter and getter方法...')
    # return struct: {'setupHeaderShadowView':['-[TTBaseViewController setupHeaderShadowView]']}
    #  imp     0x100001260 -[AppDelegate setWindow:] ==>> -[AppDelegate setWindow:],setWindow:
    re_sel_imp = re.compile('\s*imp\s*0x\w+ ([+|-]\[.+\s(.+)\])')
    re_properties_start = re.compile('\s*baseProperties 0x\w{9}')
    re_properties_end = re.compile('\w{16} 0x\w{9} _OBJC_CLASS_\$_(.+)')
    re_property = re.compile('\s*name\s*0x\w+ (.+)')
    imp_sels = {}
    is_properties_area = False

    # “otool - ov”将输出Objective - C类结构及其定义的方法。
    for line in os.popen('/usr/bin/otool -oV %s' % path).readlines():
        results = re_sel_imp.findall(line)
        if results:
            #  imp     0x100001260 -[AppDelegate setWindow:] ==>> [-[AppDelegate setWindow:],setWindow:]
            (class_sel, sel) = results[0]
            if sel in imp_sels:
                imp_sels[sel].add(class_sel)
            else:
                imp_sels[sel] = set([class_sel])
        else:
            # delete setter and getter methods as ivar assignment will not trigger them
            # 删除相关的set方法
            if re_properties_start.findall(line):
                is_properties_area = True
            if re_properties_end.findall(line):
                is_properties_area = False
            if is_properties_area:
                property_result = re_property.findall(line)
                if property_result:
                    property_name = property_result[0]
                    if property_name and property_name in imp_sels:
                        # properties layout in mach-o is after func imp
                        imp_sels.pop(property_name)
                        # 拼接set方法
                        setter = 'set' + property_name[0].upper() + property_name[1:] + ':'
                        # 干掉set方法
                        if setter in imp_sels:
                            imp_sels.pop(setter)
    return imp_sels


def ref_selectors(path):
    print('获取所有被调用的方法...')
    re_selrefs = re.compile('__TEXT:__objc_methname:(.+)')
    ref_sels = set()
    lines = os.popen('/usr/bin/otool -v -s __DATA __objc_selrefs %s' % path).readlines()
    for line in lines:
        results = re_selrefs.findall(line)
        if results:
            ref_sels.add(results[0])
    return ref_sels


def ignore_selectors(sel):
    if sel == '.cxx_destruct':
        return True
    if sel == 'load':
        return True
    return False


def filter_selectors(sels):
    filter_sels = set()
    for sel in sels:
        for prefix in reserved_prefixs:
            if sel.startswith(prefix):
                filter_sels.add(sel)
    return filter_sels


def unref_selectors(path, project_path):
    # 获取所有类的protocol的方法集合
    protocol_sels = protocol_selectors(path, project_path)

    # 获取项目所有的引用方法
    ref_sels = ref_selectors(path)
    if len(ref_sels) == 0:
        exit('获取项目所有的引用方法为空....')
    # 获取所有的方法,除了set方法
    imp_sels = imp_selectors(path)

    print("\n")
    if len(imp_sels) == 0:
        exit('Error: imp selectors count null')
    unref_sels = set()
    for sel in imp_sels:
        # 所有的方法,忽略白名单
        if ignore_selectors(sel):
            continue
        # 如果当前的方法不在protocol中,也不再引用的方法中,那么认为这个方法没有被用到
        # protocol sels will not apppear in selrefs section
        if sel not in ref_sels and sel not in protocol_sels:
            unref_sels = unref_sels.union(filter_selectors(imp_sels[sel]))
    return unref_sels


# 黑白名单过滤
def filtration_list(unref_sels, black_list, white_list):
    # 黑名单过滤
    temp_unref_sels = list(unref_sels)
    if len(black_list) > 0:
        # 如果黑名单存在,那么将在黑名单中的前缀都过滤掉
        for unref_sel in temp_unref_sels:
            for black_prefix in black_list:
                class_method = "+[%s" % black_prefix
                instance_method = "-[%s" % black_prefix
                if (unref_sel.startswith(class_method) or unref_sel.startswith(
                        instance_method)) and unref_sel in unref_sels:
                    unref_sels.remove(unref_sel)
                    break

    # 白名单过滤
    temp_array = []
    if len(white_list) > 0:
        # 如果白名单存在,只留下白名单中的部分
        for unref_sel in unref_sels:
            for white_prefix in white_list:
                class_method = "+[%s" % white_prefix
                instance_method = "-[%s" % white_prefix
                if unref_sel.startswith(class_method) or unref_sel.startswith(instance_method):
                    temp_array.append(unref_sel)
                    break
        unref_sels = temp_array

    return unref_sels


# 整理结果,写入文件
def write_to_file(unref_sels):
    file_name = 'selector_unrefs.txt'
    f = open(os.path.join(sys.path[0].strip(), file_name), 'w')
    unref_sels_num_str = '查找到未被使用的方法: %d个\n' % len(unref_sels)
    print(unref_sels_num_str)
    f.write(unref_sels_num_str)
    num = 1
    for unref_sel in unref_sels:
        unref_sels_str = '%d : %s' % (num, unref_sel)
        print(unref_sels_str)
        f.write(unref_sels_str + '\n')
        num = num + 1
    f.close()
    print('\n项目中未使用方法检测完毕,相关结果存储到当前目录 %s 中' % file_name)
    print('请在项目中进行二次确认后处理')


if __name__ == '__main__':
    # 获取入参
    app_path, project_path, black_list, white_list = input_parameter()

    # 获取未使用方法
    unref_sels = unref_selectors(app_path, project_path)

    # 黑白名单过滤
    unref_sels = filtration_list(unref_sels, black_list, white_list)

    # 打印写入文件
    write_to_file(unref_sels)

2、执行脚本开始扫描

扫描时需要cd到FindSelectorsUnrefs.py所在目录,扫描是基于工程编译产物.app进行的,因此需要保障工程可以正常的编译通过。然后找到工程的.app文件路径。扫描命令里第一个路径是填写.app文件路径。第二个路径是工程的根目录。其原理是通过编译产物的二进制文件里的符号表信息与工程文件做比较,分析哪些方法没有使用过。

//脚本命令说明
//-a Xcode运行之后的,项目Product路径
//-p 项目的地址
//-w 结果白名单处理,检测结果,只想要以什么开头的类的方法,多个用逗号隔开,比如JD,BD,AL
//-b 结果黑名单处理,检测结果,不想要以什么开头的类的方法,多个用逗号隔开,比如Pod,AF,SD
//-w 和 -b 不能共存,共存会报错

python FindSelectorsUnrefs.py -a /Users/heyujia/Library/Developer/Xcode/DerivedData/mPaasBestPracticeDemo-dwqqyiincpwuaocnzhmmhnqxnrmd/Build/Products/Debug-iphonesimulator/mPaasBestPracticeDemo.app -p /Users/heyujia/Desktop/MPaaS/mPaasBestPracticeDemo -b UI,AU,MP,AP,DT,SD,AF,mPaaS,MA,Ali

// /Users/heyujia/Library/Developer/Xcode/DerivedData/mPaasBestPracticeDemo-dwqqyiincpwuaocnzhmmhnqxnrmd/Build/Products/Debug-iphonesimulator/mPaasBestPracticeDemo.app
//这个地址是你工程编译产物.app地址

// /Users/heyujia/Desktop/MPaaS/mPaasBestPracticeDemo
//这个地址是你工程根目录地址

//UI,AU,MP,AP,DT,SD,AF,mPaaS,MA,Ali
//这个是你自己开发的类的前缀,用于白名单处理,让脚本只扫描你自己的类,避免扫描无法修改的类

如遇报错:zsh: command not found: python 可执行下面命令解决

echo "alias python=/usr/bin/python3" >> ~/.zshrc
source ~/.zshrc
echo "alias python=/usr/bin/python3" >> ~/.bash_profile
source ~/.bash_profile

3、检测扫描结果

扫描结果的内容一般会有很多,特别是一些第三方库,但第三方库的类一般都是有固定前缀,可以通过在执行命名后 “加参数 -b AU,MP,AP,DT,SD,AF,mPaaS,MA,Ali” 来忽略。最好的方式是自己工程的类,有固定的前缀,通过添加-w 白名单,只扫描自己的类。
检测扫描结果selector_unrefs.txt文件,根据文件内容进行删除


selector_unrefs.txt

五、使用fdupes扫描重复文件

减少重复文件既可以减少App体积,又可以减少启动时连接加载类的耗时,提升启动速度。

1、安装fdupes

brew install fdupes

2、扫描重复文件

扫描命令的第一个路径是被扫工程的根路径,第二个路径是扫描结果fdupes.txt文件存放的路径

// /Users/heyujia/Desktop/MPaaS/mPaasBestPracticeDemo
//是你工程的根目录地址

// /Users/heyujia/Desktop/MPaaS/fdupes.txt
//是扫描结果输出的文件地址已经文件名称

fdupes -Sr /Users/heyujia/Desktop/MPaaS/mPaasBestPracticeDemo > /Users/heyujia/Desktop/MPaaS/fdupes.txt

3、删除重复文件
根据扫描结果文件和工程实际使用情况,删除重复文件

fdupes.txt

六、使用pmd扫描重复代码

现在大多数工程都是由多人开发,开发过程或测试过程会写很多重复的代码。这些代码会增加类的体积,增加编译运行的效率。因此上线前需要删除这些重复的代码。

重复代码的标准需要使用时自行根据经验去定义,具体多少个字符相同就算重复代码。这是一个经验值。需要根据项目和团队情况去定义。

1、查询当前有哪些php版本
安装pmd 先要安装php

brew search php

2、安装php
根据查询结果选择自己想要安装的php版本

brew install php@8.0

如遇报错:php@8.0: Failed to download resource "aspell"
解决:brew upgrade

3、配置并启动PHP服务

# 配置php环境变量
echo "export PATH="$(brew --prefix php@8.0)/bin:$PATH"" >> ~/.bash_profile
source ~/.bash_profile
# 创建php启动文件夹
mkdir -p ~/Library/LaunchAgents
# 加入launchctl启动控制
cp /usr/local/Cellar/php@8.0/8.0.23_1/homebrew.mxcl.php@8.0.plist ~/Library/LaunchAgents/
launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.php@8.0.plist
#启动php服务
brew services start php@8.0

4、安装 pmd

brew install pmd

5、在工程中添加pmd脚本
安装完成后需要在Xcode 的 Build Phases 中,我们增加一个新的 Run Script
添加如下脚本

#检测swift代码
#pmd cpd --files ${EXECUTABLE_NAME} --minimum-tokens 50 --language swift --encoding UTF-8 --format net.sourceforge.pmd.cpd.XMLRenderer > cpd-output.xml --failOnViolation true

#检测objective-c代码
pmd cpd --files ${EXECUTABLE_NAME} --minimum-tokens 20 --language objectivec --encoding UTF-8 --format net.sourceforge.pmd.cpd.XMLRenderer > cpd-output.xml --failOnViolation true

# Running script
php ./cpd_script.php -cpd-xml cpd-output.xml
添加脚本

脚本中--minimum-tokens:该值决定了多少个字符串长度相同即代表为重复。是一个经验值。一般20、50、100三个值都进行扫描一次。

6、分析结果

根据扫描结果,与团队成员探讨哪些需要删除哪些需要保留。每一次探讨都是一次经验的积累。从经验中能更好的找出一个适合自己团队的--minimum-tokens值。

如遇编译报错:line 10: php: command not found Command PhaseScriptExecution failed with a nonzero exit code
解决:

brew services restart php@8.0
echo 'export PATH="/usr/local/opt/php@8.0/bin:$PATH"' >> ~/.zshrc
echo 'export PATH="/usr/local/opt/php@8.0/sbin:$PATH"' >> ~/.zshrc
source ~/.zshrc
brew services restart php@8.0
brew link --overwrite --force php@8.0

七、使用智图软件对工程中的图片进行压缩

智图官网。如果对外部软件不放心或者对压缩后的图片质量不放心。可以找自己公司的UI大佬想办法尽量压缩图片的大小。这么多年的工作经验告诉我,靠人不如靠己。

智图压缩

八、检测+(void)load使用情况

在App的冷启动过程中,会去加载load方法,如果load方法内执行的代码过多或者比较耗时。会导致App启动速度变慢。因此需要全局搜索(void)load,把每一个自己写的load方法都看一遍。检测每一个load方法的耗时,能不放在load的就不放load中,如果必须方load有没有其他代替方案或者其他执行效率较高的方法来实现。

完成上面八个步骤,静态部分就是剩下最后对Mach-o的重排了,Mach-o部分留在文章的最后,因为这是上架前的最后一步。接下来讲讲动态部分思路,动态部分只讲思路,还有些许案例。因为这个算是经验学,每个公司、团队、项目的情况不一样。

九、AppDelegate优化

这是老生常谈的启动优化流程之一,检测一下你AppDelegate里的代码,把能延迟或异步执行的代码统统放在后面执行,不要对App启动流程造成任何一点点的阻塞。虽然原则就这一点,但这一点还是有难度的,因为大多数在App启动的时候会初始化各种SDK,做各种校验等等。最瑟瑟发抖的是大部分金融类型App都会有零信任服务或者国密SSL服务,需要在App启动的时候就初始化,保障能拦截App所有的网络请求,而这种SDK有些还是异步的,需要阻塞线程来等待它初始化完成在继续执行启动流程。这部分就非常考验人的能力,如何在满足需求的情况提示App启动速度。

一般开发能纯代码开发就不要用SB、XIB,后者的执行效率比纯代码要慢很多,并且可控性也没有代码来得那么直观。而且文件还大。

十、设计App的启动流程

现在App启动耗时记录的时间不在是以前的App执行完AppDelegate的耗时了,而是用户点击App启动图标到完整的看到App的首页这段时间。因此为了提升App的启动耗时,需要开发同学单独对App的启动流程进行开发详细设计,产出详设文档。因为产品同学一般提供的PRD都只关注业务流程,对于开发而言内容太粗,细节太少,并且产品同学不是非常了解App启动的逻辑流程。所以需要开发同学从更加全面细致的思维角度去细化完善启动流程,并从设计角度提升App的启动速度,让用户能快速的进入到App首页,提升用户体验。

App的启动耗时结束的标准是用户看到完整的首页界面,那么至少要在首页ViewController的viewDidLoad执行完成后才算结束。但首页数据往往都有网络请求,更加精准的应该是网络请求数据回来并渲染完成后,才算做启动时间结束

十一、工程配置优化

1、Generate Debug Symbols 设置为NO

该选项是是否生产调试符号,设置为NO就不会生成调试符号,也会导致不会在断点处停下。所以release模式下可以设置为NO。


Generate Debug Symbols

2、舍弃arm7架构

armv7用于支持4s和4,目前大多数项目最低支持版本是iOS9.而4s最高支持iOS8.所以这个框架基本可以忽略。


armv7

3、去掉不必要的调试符号

Strip Debug Symbols During Copy release设置为YES


Strip Debug Symbols During Copy

Symbols Hidden by Default release设置为YES


Symbols Hidden by Default

4、Asset Catalog Compiler编译设置优化

通过修改Optimization这个选项可以改变在构建Assets.car时的策略,修改为space
后会按照一定策略选取编码算法,对其中的 png 图片重新编码, 从而减少包大小。


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

推荐阅读更多精彩内容