使用Swift3开发了个MacOS的程序可以检测出objc项目中无用方法,然后一键全部清理

当项目越来越大,引入第三方库越来越多,上架的 APP 体积也会越来越大,对于用户来说体验必定是不好的。在清理资源,编译选项优化,清理无用类等完成后,能够做而且效果会比较明显的就只有清理无用函数了。

一种方案是我们滴滴的王康基于clang插件这样一个源码级别的分析工具来分析代码间的调用关系达到分析出无用代码的目的,文章在这里: 基于clang插件的一种iOS包大小瘦身方案 文章里对objc方法的定义,调用,实现的全面说明达到了极致,非常值得一看。

另一种方案是根据 Linkmap 文件取到objc的所有类方法和实例方法。再用工具比如 otool 命令逆向出可执行文件里引用到的方法名然后通过求差集得到无用函数,由于API的回调也会被认为是无用函数,所以这个方案还需要将这些回调函数加到白名单里过滤。具体说明,可以看看微信团队的这篇文章: iOS微信安装包瘦身

还有一种使用了 * machoview * 从 Mach-O 里获取信息进行无用方法和文件的处理。阿里有篇文章对 Mach-O 的处理做了详细的说明: 减小ipa体积之删除frameWork中无用mach-O文件

这几个现有方案有些比较麻烦的地方,因为检索出的无用方法没法确定能够直接删除,还需要挨个检索人工判断是否可以删除,这样每次要清理时都需要这样人工排查一遍是非常耗时耗力的。

这样就只有模拟编译过程对代码进行深入分析才能够找出确定能够删除的方法。具体效果可以先试试看,程序代码在:https://github.com/ming1016/SMCheckProject 选择工程目录后程序就开始检索无用方法然后将其注释掉。

设置结构体 😚

首先确定结构,类似先把 OC 文件根据语法画出整体结构。先看看 OC Runtime 里是如何设计的结构体。

struct objc_object {  
    Class isa  OBJC_ISA_AVAILABILITY;
};

/*类*/
struct objc_class {  
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
#endif
};

/*成员变量列表*/
struct objc_ivar_list {
    int ivar_count               
#ifdef __LP64__
    int space                    
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]
}      

/*成员变量结构体*/
struct objc_ivar {
    char *ivar_name
    char *ivar_type
    int ivar_offset
#ifdef __LP64__
    int space      
#endif
}    

/*方法列表*/
struct objc_method_list {  
    struct objc_method_list *obsolete;
    int method_count;

#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_method method_list[1];
};

/*方法结构体*/
struct objc_method {  
    SEL method_name;
    char *method_types;    /* a string representing argument/return types */
    IMP method_imp;
};

一个 class 只有少量函数会被调用,为了减少较大的遍历所以创建一个 objc_cache ,在找到一个方法后将 method_name 作为 key,将 method_imp 做值,再次发起时就可以直接在 cache 里找。

使用 swift 创建类似的结构体,做些修改

//文件
class File: NSObject {
    //文件
    public var type = FileType.FileH
    public var name = ""
    public var content = ""
    public var methods = [Method]() //所有方法
    public var imports = [Import]() //引入类
}

//引入
struct Import {
    public var fileName = ""
}

//对象
class Object {
    public var name = ""
    public var superObject = ""
    public var properties = [Property]()
    public var methods = [Method]()
}

//成员变量
struct Property {
    public var name = ""
    public var type = ""
}

struct Method {
    public var classMethodTf = false //+ or -
    public var returnType = ""
    public var returnTypePointTf = false
    public var returnTypeBlockTf = false
    public var params = [MethodParam]()
    public var usedMethod = [Method]()
    public var filePath = "" //定义方法的文件路径,方便修改文件使用
    public var pnameId = ""  //唯一标识,便于快速比较
}

class MethodParam: NSObject {
    public var name = ""
    public var type = ""
    public var typePointTf = false
    public var iName = ""
}

class Type: NSObject {
    //todo:更多类型
    public var name = ""
    public var type = 0 //0是值类型 1是指针
}
```swift

## 开始语法解析 😈
首先遍历目录下所有的文件。
```swift
let fileFolderPath = self.selectFolder()
let fileFolderStringPath = fileFolderPath.replacingOccurrences(of: "file://", with: "")
let fileManager = FileManager.default;
//深度遍历
let enumeratorAtPath = fileManager.enumerator(atPath: fileFolderStringPath)
//过滤文件后缀
let filterPath = NSArray(array: (enumeratorAtPath?.allObjects)!).pathsMatchingExtensions(["h","m"])

然后将注释排除在分析之外,这样做能够有效避免无用的解析。

分析是否需要按照行来切割,在 @interface@end@ implementation@end 里面不需要换行,按照;符号,外部需要按行来。所以两种切割都需要。

先定义语法标识符

class Sb: NSObject {
    public static let add = "+"
    public static let minus = "-"
    public static let rBktL = "("
    public static let rBktR = ")"
    public static let asterisk = "*"
    public static let colon = ":"
    public static let semicolon = ";"
    public static let divide = "/"
    public static let agBktL = "<"
    public static let agBktR = ">"
    public static let quotM = "\""
    public static let pSign = "#"
    public static let braceL = "{"
    public static let braceR = "}"
    public static let bktL = "["
    public static let bktR = "]"
    public static let qM = "?"
    public static let upArrow = "^"
    
    public static let inteface = "@interface"
    public static let implementation = "@implementation"
    public static let end = "@end"
    public static let selector = "@selector"
    
    public static let space = " "
    public static let newLine = "\n"
}

接下来就要开始根据标记符号来进行切割分组了,使用 Scanner ,具体方式如下

//根据代码文件解析出一个根据标记符切分的数组
class func createOCTokens(conent:String) -> [String] {
    var str = conent

    str = self.dislodgeAnnotaion(content: str)

    //开始扫描切割
    let scanner = Scanner(string: str)
    var tokens = [String]()
    //Todo:待处理符号,.
    let operaters = [Sb.add,Sb.minus,Sb.rBktL,Sb.rBktR,Sb.asterisk,Sb.colon,Sb.semicolon,Sb.divide,Sb.agBktL,Sb.agBktR,Sb.quotM,Sb.pSign,Sb.braceL,Sb.braceR,Sb.bktL,Sb.bktR,Sb.qM]
    var operatersString = ""
    for op in operaters {
        operatersString = operatersString.appending(op)
    }

    var set = CharacterSet()
    set.insert(charactersIn: operatersString)
    set.formUnion(CharacterSet.whitespacesAndNewlines)

    while !scanner.isAtEnd {
        for operater in operaters {
            if (scanner.scanString(operater, into: nil)) {
                tokens.append(operater)
            }
        }

        var result:NSString?
        result = nil;
        if scanner.scanUpToCharacters(from: set, into: &result) {
            tokens.append(result as! String)
        }
    }
    tokens = tokens.filter {
        $0 != Sb.space
    }
    return tokens;
}

行解析的方法

//根据代码文件解析出一个根据行切分的数组
class func createOCLines(content:String) -> [String] {
    var str = content
    str = self.dislodgeAnnotaion(content: str)
    let strArr = str.components(separatedBy: CharacterSet.newlines)
    return strArr
}

根据结构将定义的方法取出 🤖

- (id)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(NSString *)path cacheTime:(NSInteger)cacheTime subDirectory:(NSString*)subDirectory;

这里按照语法规则顺序取出即可,将方法名,返回类型,参数名,参数类型记录。这里需要注意 Block 类型的参数

- (STMPartMaker *(^)(STMPartColorType))colorTypeIs;

这种类型中还带有括号的语法的解析,这里用到的方法是对括号进行计数,左括号加一右括号减一的方式取得完整方法。

获得这些数据后就可以开始检索定义的方法了。我写了一个类专门用来获得所有定义的方法

class func parsingWithArray(arr:Array<String>) -> Method {
    var mtd = Method()
    var returnTypeTf = false //是否取得返回类型
    var parsingTf = false //解析中
    var bracketCount = 0 //括弧计数
    var step = 0 //1获取参数名,2获取参数类型,3获取iName
    var types = [String]()
    var methodParam = MethodParam()
    //print("\(arr)")
    for var tk in arr {
        tk = tk.replacingOccurrences(of: Sb.newLine, with: "")
        if (tk == Sb.semicolon || tk == Sb.braceL) && step != 1 {
            var shouldAdd = false
            
            if mtd.params.count > 1 {
                //处理这种- (void)initWithC:(type)m m2:(type2)i, ... NS_REQUIRES_NIL_TERMINATION;入参为多参数情况
                if methodParam.type.characters.count > 0 {
                    shouldAdd = true
                }
            } else {
                shouldAdd = true
            }
            if shouldAdd {
                mtd.params.append(methodParam)
                mtd.pnameId = mtd.pnameId.appending("\(methodParam.name):")
            }
            
        } else if tk == Sb.rBktL {
            bracketCount += 1
            parsingTf = true
        } else if tk == Sb.rBktR {
            bracketCount -= 1
            if bracketCount == 0 {
                var typeString = ""
                for typeTk in types {
                    typeString = typeString.appending(typeTk)
                }
                if !returnTypeTf {
                    //完成获取返回
                    mtd.returnType = typeString
                    step = 1
                    returnTypeTf = true
                } else {
                    if step == 2 {
                        methodParam.type = typeString
                        step = 3
                    }
                    
                }
                //括弧结束后的重置工作
                parsingTf = false
                types = []
            }
        } else if parsingTf {
            types.append(tk)
            //todo:返回block类型会使用.设置值的方式,目前获取用过方法方式没有.这种的解析,暂时作为
            if tk == Sb.upArrow {
                mtd.returnTypeBlockTf = true
            }
        } else if tk == Sb.colon {
            step = 2
        } else if step == 1 {
            if tk == "initWithCoordinate" {
                //
            }
            methodParam.name = tk
            step = 0
        } else if step == 3 {
            methodParam.iName = tk
            step = 1
            mtd.params.append(methodParam)
            mtd.pnameId = mtd.pnameId.appending("\(methodParam.name):")
            methodParam = MethodParam()
        } else if tk != Sb.minus && tk != Sb.add {
            methodParam.name = tk
        }
        
    }//遍历
    
    return mtd
}

这个方法大概的思路就是根据标记符设置不同的状态,然后将获取的信息放入定义的结构中。

使用过的方法的解析 😱

进行使用过的方法解析前需要处理的事情

  • @“…” 里面的数据,因为这里面是允许我们定义的标识符出现的。
  • 递归出文件中 import 所有的类,根据对类的使用可以清除无用的 import
  • 继承链的获取。
  • 解析获取实例化了的成员变量列表。在解析时需要依赖列表里的成员变量名和变量的类进行方法的完整获取。

简单的方法

[view update:status animation:YES];

从左到右按照 : 符号获取

方法嵌套调用,下面这种情况如何解析出

@weakify(self);
[[[[[[SMNetManager shareInstance] fetchAllFeedWithModelArray:self.feeds] map:^id(NSNumber *value) {
    @strongify(self);
    NSUInteger index = [value integerValue];
    self.feeds[index] = [SMNetManager shareInstance].feeds[index];
    return self.feeds[index];
}] doCompleted:^{
    //抓完所有的feeds
    @strongify(self);
    NSLog(@"fetch complete");
    //完成置为默认状态
    self.tbHeaderLabel.text = @"";
    self.tableView.tableHeaderView = [[UIView alloc] init];
    self.fetchingCount = 0;
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
    //下拉刷新关闭
    [self.tableView.mj_header endRefreshing];
    //更新列表
    [self.tableView reloadData];
    //检查是否需要增加源
    if ([SMFeedStore defaultFeeds].count > self.feeds.count) {
        self.feeds = [SMFeedStore defaultFeeds];
        [self fetchAllFeeds];
    }
}] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(SMFeedModel *feedModel) {
    //抓完一个
    @strongify(self);
    self.tableView.tableHeaderView = self.tbHeaderView;
    //显示抓取状态
    self.fetchingCount += 1;
    self.tbHeaderLabel.text = [NSString stringWithFormat:@"正在获取%@...(%lu/%lu)",feedModel.title,(unsigned long)self.fetchingCount,(unsigned long)self.feeds.count];
    [self.tableView reloadData];
}];

一开始会想到使用递归,以前我做 STMAssembleView 时就是使用的递归,这样时间复杂度就会是 O(nlogn) ,这次我换了个思路,将复杂度降低到了 n ,思路大概是 创建一个字典,键值就是深度,从左到右深度的增加根据 [ 符号,减少根据 ] 符号,值会在 [ 时创建一个 Method 结构体,根据]来完成结构体,将其添加到 methods 数组中 。

具体实现如下

class func parsing(contentArr:Array<String>, inMethod:Method) -> Method {
    var mtdIn = inMethod
    //处理用过的方法
    //todo:还要过滤@""这种情况
    var psBrcStep = 0
    var uMtdDic = [Int:Method]()
    var preTk = ""
    //处理?:这种条件判断简写方式
    var psCdtTf = false
    var psCdtStep = 0
    //判断selector
    var psSelectorTf = false
    var preSelectorTk = ""
    var selectorMtd = Method()
    var selectorMtdPar = MethodParam()
    
    uMtdDic[psBrcStep] = Method() //初始时就实例化一个method,避免在define里定义只定义]符号
    
    for var tk in contentArr {
        //selector处理
        if psSelectorTf {
            if tk == Sb.colon {
                selectorMtdPar.name = preSelectorTk
                selectorMtd.params.append(selectorMtdPar)
                selectorMtd.pnameId += "\(selectorMtdPar.name):"
            } else if tk == Sb.rBktR {
                mtdIn.usedMethod.append(selectorMtd)
                psSelectorTf = false
                selectorMtd = Method()
                selectorMtdPar = MethodParam()
            } else {
                preSelectorTk = tk
            }
            continue
        }
        if tk == Sb.selector {
            psSelectorTf = true
            selectorMtd = Method()
            selectorMtdPar = MethodParam()
            continue
        }
        //通常处理
        if tk == Sb.bktL {
            if psCdtTf {
                psCdtStep += 1
            }
            psBrcStep += 1
            uMtdDic[psBrcStep] = Method()
        } else if tk == Sb.bktR {
            if psCdtTf {
                psCdtStep -= 1
            }
            if (uMtdDic[psBrcStep]?.params.count)! > 0 {
                mtdIn.usedMethod.append(uMtdDic[psBrcStep]!)
            }
            psBrcStep -= 1
            //[]不配对的容错处理
            if psBrcStep < 0 {
                psBrcStep = 0
            }
            
        } else if tk == Sb.colon {
            //条件简写情况处理
            if psCdtTf && psCdtStep == 0 {
                psCdtTf = false
                continue
            }
            //dictionary情况处理@"key":@"value"
            if preTk == Sb.quotM || preTk == "respondsToSelector" {
                continue
            }
            let prm = MethodParam()
            prm.name = preTk
            if prm.name != "" {
                uMtdDic[psBrcStep]?.params.append(prm)
                uMtdDic[psBrcStep]?.pnameId = (uMtdDic[psBrcStep]?.pnameId.appending("\(prm.name):"))!
            }
        } else if tk == Sb.qM {
            psCdtTf = true
        } else {
            tk = tk.replacingOccurrences(of: Sb.newLine, with: "")
            preTk = tk
        }
    }
    
    return mtdIn
}

在设置 Method 结构体时将参数名拼接起来成为 Method 的识别符用于后面处理时的快速比对。

解析使用过的方法时有几个问题需要注意下
1.在方法内使用的方法,会有 respondsToSelector@selector 还有条件简写语法的情况需要单独处理下。
2.在 #define 里定义使用了方法

#define CLASS_VALUE(x)    [NSValue valueWithNonretainedObject:(x)]

找出无用方法 😄

获取到所有使用方法后进行去重,和定义方法进行匹对求出差集,即全部未使用的方法。

去除无用方法 😎

比对后获得无用方法后就要开始注释掉他们了。遍历未使用的方法,根据先前 Method 结构体中定义了方法所在文件路径,根据文件集结构和File的结构体,可以避免 IO ,直接获取方法对应的文件内容和路径。
对文件内容进行行切割,逐行检测方法名和参数,匹对时开始对行加上注释, h 文件已;符号为结束, m 文件会对大括号进行计数,逐行注释。实现的方法具体如下:

//删除指定的一组方法
class func delete(methods:[Method]) {
    print("无用方法")
    for aMethod in methods {
        print("\(File.desDefineMethodParams(paramArr: aMethod.params))")

        //开始删除
        //continue
        var hContent = ""
        var mContent = ""
        var mFilePath = aMethod.filePath
        if aMethod.filePath.hasSuffix(".h") {
            hContent = try! String(contentsOf: URL(string:aMethod.filePath)!, encoding: String.Encoding.utf8)
            //todo:因为先处理了h文件的情况
            mFilePath = aMethod.filePath.trimmingCharacters(in: CharacterSet(charactersIn: "h")) //去除头尾字符集
            mFilePath = mFilePath.appending("m")
        }
        if mFilePath.hasSuffix(".m") {
            do {
                mContent = try String(contentsOf: URL(string:mFilePath)!, encoding: String.Encoding.utf8)
            } catch {
                mContent = ""
            }

        }

        let hContentArr = hContent.components(separatedBy: CharacterSet.newlines)
        let mContentArr = mContent.components(separatedBy: CharacterSet.newlines)
        //print(mContentArr)
        //----------------h文件------------------
        var psHMtdTf = false
        var hMtds = [String]()
        var hMtdStr = ""
        var hMtdAnnoStr = ""
        var hContentCleaned = ""
        for hOneLine in hContentArr {
            var line = hOneLine.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

            if line.hasPrefix(Sb.minus) || line.hasPrefix(Sb.add) {
                psHMtdTf = true
                hMtds += self.createOCTokens(conent: line)
                hMtdStr = hMtdStr.appending(hOneLine + Sb.newLine)
                hMtdAnnoStr += "//-----由SMCheckProject工具删除-----\n//"
                hMtdAnnoStr += hOneLine + Sb.newLine
                line = self.dislodgeAnnotaionInOneLine(content: line)
                line = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
            } else if psHMtdTf {
                hMtds += self.createOCTokens(conent: line)
                hMtdStr = hMtdStr.appending(hOneLine + Sb.newLine)
                hMtdAnnoStr += "//" + hOneLine + Sb.newLine
                line = self.dislodgeAnnotaionInOneLine(content: line)
                line = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
            } else {
                hContentCleaned += hOneLine + Sb.newLine
            }

            if line.hasSuffix(Sb.semicolon) && psHMtdTf{
                psHMtdTf = false

                let methodPnameId = ParsingMethod.parsingWithArray(arr: hMtds).pnameId
                if aMethod.pnameId == methodPnameId {
                    hContentCleaned += hMtdAnnoStr

                } else {
                    hContentCleaned += hMtdStr
                }
                hMtdAnnoStr = ""
                hMtdStr = ""
                hMtds = []
            }


        }
        //删除无用函数
        try! hContentCleaned.write(to: URL(string:aMethod.filePath)!, atomically: false, encoding: String.Encoding.utf8)

        //----------------m文件----------------
        var mDeletingTf = false
        var mBraceCount = 0
        var mContentCleaned = ""
        var mMtdStr = ""
        var mMtdAnnoStr = ""
        var mMtds = [String]()
        var psMMtdTf = false
        for mOneLine in mContentArr {
            let line = mOneLine.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

            if mDeletingTf {
                let lTokens = self.createOCTokens(conent: line)
                mMtdAnnoStr += "//" + mOneLine + Sb.newLine
                for tk in lTokens {
                    if tk == Sb.braceL {
                        mBraceCount += 1
                    }
                    if tk == Sb.braceR {
                        mBraceCount -= 1
                        if mBraceCount == 0 {
                            mContentCleaned = mContentCleaned.appending(mMtdAnnoStr)
                            mMtdAnnoStr = ""
                            mDeletingTf = false
                        }
                    }
                }

                continue
            }


            if line.hasPrefix(Sb.minus) || line.hasPrefix(Sb.add) {
                psMMtdTf = true
                mMtds += self.createOCTokens(conent: line)
                mMtdStr = mMtdStr.appending(mOneLine + Sb.newLine)
                mMtdAnnoStr += "//-----由SMCheckProject工具删除-----\n//" + mOneLine + Sb.newLine
            } else if psMMtdTf {
                mMtdStr = mMtdStr.appending(mOneLine + Sb.newLine)
                mMtdAnnoStr += "//" + mOneLine + Sb.newLine
                mMtds += self.createOCTokens(conent: line)
            } else {
                mContentCleaned = mContentCleaned.appending(mOneLine + Sb.newLine)
            }

            if line.hasSuffix(Sb.braceL) && psMMtdTf {
                psMMtdTf = false
                let methodPnameId = ParsingMethod.parsingWithArray(arr: mMtds).pnameId
                if aMethod.pnameId == methodPnameId {
                    mDeletingTf = true
                    mBraceCount += 1
                    mContentCleaned = mContentCleaned.appending(mMtdAnnoStr)
                } else {
                    mContentCleaned = mContentCleaned.appending(mMtdStr)
                }
                mMtdStr = ""
                mMtdAnnoStr = ""
                mMtds = []
            }

        } //m文件

        //删除无用函数
        if mContent.characters.count > 0 {
            try! mContentCleaned.write(to: URL(string:mFilePath)!, atomically: false, encoding: String.Encoding.utf8)
        }

    }
}

完整代码在:https://github.com/ming1016/SMCheckProject 这里。

后记 🦁

有了这样的结构数据就可以模拟更多人工检测的方式来检测项目。

通过获取的方法结合获取类里面定义的局部变量和全局变量,在解析过程中模拟引用的计数来分析循环引用等等类似这样的检测。
通过获取的类的完整结构还能够将其转成JavaScriptCore能解析的js语法文件等等。

对于APP瘦身的一些想法 👽

瘦身应该从平时开发时就需要注意。除了功能和组件上的复用外还需要对堆栈逻辑进行封装以达到代码压缩的效果。

比如使用ReactiveCocoa和RxSwift这样的函数响应式编程库提供的方法和编程模式进行

对于UI的视图逻辑可以使用一套统一逻辑压缩代码使用DSL来简化写法等。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,048评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,471评论 25 707
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,625评论 0 9
  • 1 起床 ,床上粉丝的被子缩成一团,红色地毯上都是狗毛和薯片碎屑,在看床上空无一人,那几个酒瓶乱七八糟的在地下。 ...
    Te小鱼儿er阅读 304评论 0 0
  • 社会各界爱心人士: 6.22火灾,我的三个孩子和我的妻子永远离开我了。我的原本幸福美满的家庭毁于一旦,至今我都不愿...
    桂霏是人才阅读 295评论 1 2