【iOS开发】如何将旧的Objective-C项目逐渐转为Swift项目

Swift从2014年发布到现在,马上接近三年,经过苹果的不断改进和完善,语法方面已经趋于稳定。如果是新建的项目,严重建议使用Swift,因为Swift必定会取代Objective-C。然后对于用Objective-C写的旧项目,我们有两个选择:1)直接整个项目用Swift重写;2)在旧项目的基础上,新的东西用Swift编写,然后再把之前用Objective-C写的代码慢慢改为Swift。我个人更偏向于在旧项目的基础上逐渐把整个项目转为Swift。下面我将会结合实际工作和苹果的官方文档《Using Swift with Cocoa and Objective-C (Swift 3.1)》来总结下如何将旧的Objective-C项目逐渐转为Swift项目。

学习Swift

首先,你要懂得Swift(这TMD不是讲废话吗 ...)。英文能力不错的建议看官方的文档《The Swift Programming Language (Swift 3.1)》,官方的文档总是最好的。不嫌弃的话,可以看看我写的《Swift文集》,总结了Swift的关键知识点。另外,大家可以看看Swift翻译组翻译的内容。

Objective-C和Swift的互用

在这部分内容里,我将会根据官方的文档,总结下Objective-C和Swift是如何互用的。

初始化

在Objective-C中,类的初始化方法通常是以init或者initWith开头的。在Swift中使用Objective-C的类时,Swift会把init开头的方法作为初始化方法,如果是以initWith开头的,在Swift中调用时,会把With去掉,例如:

在Objective-C中:

- (instancetype)init;
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style;

在Swift中调用上面的接口,就会是下面这种形式:

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }
类方法和便利初始化器

在Objective-C的类方法,在Swift中会被作为便利初始化器:

在Objective-C中:

UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0];

在Swift中,就会是下面这种形式:

let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)
访问属性

Objective-C中的属性将会按照下面这个规则来导入Swift:

  • nonnull, nullablenull_resettable标记的属性导入Swift之后,会变成optional和nonoptional类型
  • readonly标记的属性导入Swift之后,变成计算属性 ({ get })。
  • weak标记的属性导入Swift之后,同样是被weak标记 (weak var)。
  • assign, copy, strong或者unsafe_unretained标记的,将会以适当的存储导入Swift。
  • class标记的属性导入Swift之后,变成类型属性。
  • 原子性属性(atomicnonatomic)在对应的Swift属性中没有反应出来,但是在Swift中被访问的时候,Objective-C原子性的实现仍然会保留。
  • getter=setter=在Swift中被省略。

在Swift中,直接用点语法来访问Objective-C的属性。

方法

同样地,在Swift中也是使用点语法来访问方法。

当Objective-C的方法被导入Swift后,Objective-C的Selector的第一部分会被作为Swift的方法名。例如:

在Objective-C中:

[myTableView insertSubview:mySubview atIndex:2]; 

导入Swift后:

myTableView.insertSubview(mySubview, at: 2)
id兼容性

Objective-C的id类型,导入Swift之后,成为Swift的Any类型。

Swift还有一个类型AnyObject,可以代表所有的class类型,它可以动态的搜索任何@objc方法,而无需向下转型。例如:

var myObject: AnyObject = UITableViewCell()
myObject = NSDate()
let futureDate = myObject.addingTimeInterval(10)
let timeSinceNow = myObject.timeIntervalSinceNow

但是我们在运行代码之前,AnyObject的具体类型是不确定的,所以上面这种写法非常危险。,例如下面这个例子,在运行的时候会crash:

myObject.character(at: 5)
// crash, myObject doesn't respond to that method

我们可以使用可选链或者if let来解决这个问题:

// 可选链
let myChar = myObject.character?(at: 5)

// if let
if let fifthCharacter = myObject.character?(at: 5) {
    print("Found \(fifthCharacter) at index 5")
}
空属性和可选

我们都知道在Objective-C中,可以使用一些注释来标记属性、参数或者返回值是否可以为空,例如_nullable_Nonull等等。他们会按照下面的规则来导入Swift:

  • _Nonnull标记的,在导入Swift之后,会被作为非可选类型
  • _Nullable标记的,在导入Swift之后,会被作为可选类型
  • 没有被任何注释标记的,在导入Swift之后,会被作为隐式解包可选类型

例如,在Objective-C中:

@property (nullable) id nullableProperty;
@property (nonnull) id nonNullProperty;
@property id unannotatedProperty;
 
NS_ASSUME_NONNULL_BEGIN
- (id)returnsNonNullValue;
- (void)takesNonNullParameter:(id)value;
NS_ASSUME_NONNULL_END
 
- (nullable id)returnsNullableValue;
- (void)takesNullableParameter:(nullable id)value;
 
- (id)returnsUnannotatedValue;
- (void)takesUnannotatedParameter:(id)value;

导入Swift之后:

var nullableProperty: Any?
var nonNullProperty: Any
var unannotatedProperty: Any!
 
func returnsNonNullValue() -> Any
func takesNonNullParameter(value: Any)
 
func returnsNullableValue() -> Any?
func takesNullableParameter(value: Any?)
 
func returnsUnannotatedValue() -> Any!
func takesUnannotatedParameter(value: Any!)
轻量级泛型

在Swift中:

@property NSArray<NSDate *> *dates;
@property NSCache<NSObject *, id<NSDiscardableContent>> *cachedData;
@property NSDictionary <NSString *, NSArray<NSLocale *>> *supportedLocales;

导入Swift之后:

var dates: [Date]
var cachedData: NSCache<AnyObject, NSDiscardableContent>
var supportedLocales: [String: [Locale]]
扩展

Swift的扩展其实类似于Objective-C的分类。Swift的扩展可以对现有的类、结构和枚举添加新的成员,即使是在Objective-C中定义的类、结构和枚举,都可以进行扩展。

例如下面这个例子,为UIBezierPath添加一个便利初始化器,可用来画一个等边三角形:

extension UIBezierPath {
    convenience init(triangleSideLength: CGFloat, origin: CGPoint) {
        self.init()
        let squareRoot = CGFloat(sqrt(3.0))
        let altitude = (squareRoot * triangleSideLength) / 2
        move(to: origin)
        addLine(to: CGPoint(x: origin.x + triangleSideLength, y: origin.y))
        addLine(to: CGPoint(x: origin.x + triangleSideLength / 2, y: origin.y + altitude))
        close()
    }
}
闭包

Objective-C的block,导入Swift之后变为Closure。例如在Objective-C中有一个block:

void (^completionBlock)(NSData *) = ^(NSData *data) {
   // ...
}

在Swift中是这样的:

let completionBlock: (Data) -> Void = { data in
    // ...
}

Objective-C的block和Swift的Closure基本上可以说是等价的,但是有一点不同的是:外部的变量在Swift的Closure中是可变的,我们可以直接在Closure内部更新变量的值;而在Objective-C中,需要用__block标记变量。

解决Block中的循环引用问题

在Objective-C中:

__weak typeof(self) weakSelf = self;
self.block = ^{
   __strong typeof(self) strongSelf = weakSelf;
   [strongSelf doSomething];
};

在Swift中是这样解决的,[unowned self]被称为捕获列表(Capture List):

self.closure = { [unowned self] in
    self.doSomething()
}
对象之间的比较

在Swift中,比较两个对象是否相等有两种方法:1) ==:比较两个对象的内容是否相等;2) ===:比较两个常量或者变量是否引用着同一个对象实例。

Swift为继承自NSObject的子类提供了默认的=====实现,并实现了Equatable协议。默认的==实现调用了isEqual:方法,默认的===实现检查指针是否相等。我们不能重写从Objective-C导入的类的这两个操作符。

Swift类型的兼容性

下面这些Swift特有的类型,是不兼容Objective-C的:

  • 泛型
  • 元组
  • Swift中定义的没有Int类型原始值的枚举
  • Swift中定义的结构
  • Swift中定义的高阶函数
  • Swift中定义的全局变量
  • Swift中定义的类型别名
  • Swift风格的variadics
  • 嵌套类型
  • Curried functions

Swift转换为Objective-C:

  • 可选类型,被__nullable标记
  • 非可选类型,被__nonnull标记
  • 常量和计算属性,变成只读属性
  • 类型属性在Objective-C中被class标记
  • 类型方法在Objective-C是类方法
  • 初始化器和实例方法变成Objective-C的实例方法
  • 会抛出错误的方法,在Objective-C中会多了一个NSerror **参数。如果Swift的方法没有返回值,在Objective-C中会返回一个BOOL

例如,在Swift中:

class Jukebox: NSObject {
    var library: Set<String>
    
    var nowPlaying: String?
    
    var isCurrentlyPlaying: Bool {
        return nowPlaying != nil
    }
    
    class var favoritesPlaylist: [String] {
        // return an array of song names
    }
    
    init(songs: String...) {
        self.library = Set<String>(songs)
    }
    
    func playSong(named name: String) throws {
        // play song or throw an error if unavailable
    }
}

转换成Objective-C后:

@interface Jukebox : NSObject
@property (nonatomic, strong, nonnull) NSSet<NSString *> *library;
@property (nonatomic, copy, nullable) NSString *nowPlaying;
@property (nonatomic, readonly, getter=isCurrentlyPlaying) BOOL currentlyPlaying;
@property (nonatomic, class, readonly, nonnull) NSArray<NSString *> * favoritesPlaylist;
- (nonnull instancetype)initWithSongs:(NSArray<NSString *> * __nonnull)songs OBJC_DESIGNATED_INITIALIZER;
- (BOOL)playSong:(NSString * __nonnull)name error:(NSError * __nullable * __null_unspecified)error;
@end
自定义Swift在Objective-C的接口

我们可以使用@objc(name)自定义Swift的类、属性、方法、枚举类型或者枚举case在Objective-C中使用时的名字。

例如,在Swift中:

@objc(Color)
enum Цвет: Int {
    @objc(Red)
    case Красный
    
    @objc(Black)
    case Черный
}
 
@objc(Squirrel)
class Белка: NSObject {
    @objc(color)
    var цвет: Цвет = .Красный
    
    @objc(initWithName:)
    init (имя: String) {
        // ...
    }
    @objc(hideNuts:inTree:)
    func прячьОрехи(количество: Int, вДереве дерево: Дерево) {
        // ...
    }
}

Swift还提供了一个属性@nonobjc,被这个属性标记的成员将不能在Objective-C中使用。

需要动态调度

当Swift的API被Objective-C runtime使用时,不能保证能动态调度属性、方法、下标或者初始化器。Swift的编译器仍然会反虚拟化或者内联成员访问来优化代码的属性,并绕过Objective-C runtime。

我们可以使用dynamic在使用Objective-C runtime时动态的访问成员。需要动态调度的情况是非常少的。但是,在Objective-C runtime中使用key-value observing或者method_exchangeImplementations时,我们就需要动态调度,在运行的时候来动态地替换一个方法的实现。

注意:使用了dynamic标记的声明,不能再使用@nonobjc。因为使用了@nonobjc,就意味着不能在Objective-C中使用,而dynamic就是为了给Objective-C使用,这两个属性是完全冲突的。

Selector

在Objective-C中,我们使用@selector来构造一个Selector;而在Swift中,我们要使用#selector

Key和Key Path

在Swift中,可以使用#keyPath来生成编译器检查(也就是说编译的时候就能知道key和keyPath是否有误,而不必等到运行时才能确定)的key和keyPath,然后就可以给这些方法使用:value(forKey:)value(forKeyPath:)addObserver(_:forKeyPath:options:context:)#keyPath支持链式方法或者属性,如#keyPath(Person.bestFriend.name)

例如:

class Person: NSObject {
    var name: String
    var friends: [Person] = []
    var bestFriend: Person? = nil
    
    init(name: String) {
        self.name = name
    }
}
 
let gabrielle = Person(name: "Gabrielle")
let jim = Person(name: "Jim")
let yuanyuan = Person(name: "Yuanyuan")
gabrielle.friends = [jim, yuanyuan]
gabrielle.bestFriend = yuanyuan
 
#keyPath(Person.name)
// "name"
gabrielle.value(forKey: #keyPath(Person.name))
// "Gabrielle"
#keyPath(Person.bestFriend.name)
// "bestFriend.name"
gabrielle.value(forKeyPath: #keyPath(Person.bestFriend.name))
// "Yuanyuan"
#keyPath(Person.friends.name)
// "friends.name"
gabrielle.value(forKeyPath: #keyPath(Person.friends.name))
// ["Yuanyuan", "Jim"]

Cocoa Frameworks

Swift能自动地将一些类型在Swift和Objective-C之间互相转换。例如我们可以传一个String值给NSString参数。

Foundation
桥接类型

Swift Foundation提供了下列桥接值类型:

Objective-C引用类型 Swift值类型
NSAffineTransform AffineTransform
NSArray Array
NSCalendar Calendar
NSCharacterSet CharacterSet
NSData Data
NSDateComponents DateComponents
NSDateInterval DateInterval
NSDate Date
NSDecimalNumber Decimal
NSDictionary Dictionary
NSIndexPath IndexPath
NSIndexSet IndexSet
NSMeasurement Measurement
NSNotification Notification
NSNumber Swift的数字类型(IntFloat等等)
NSPersonNameComponents PersonNameComponents
NSSet Set
NSString String
NSTimeZone TimeZone
NSURL URL
NSURLComponents URLComponents
NSURLQueryItem URLQueryItem
NSURLRequest URLRequest
NSUUID UUID

我们可以看到,就是直接把Objective-C的前缀NS去掉,就是Swift的值类型(但是有些情况例外)。这些Swift的值类型拥有Objective-C引用类型的所有方法。任何使用Objective-C引用类型的地方,都可以使用对应的Swift值类型。

统一的Logging

统一的logging系统提供了一些平台通用的API来打印一些信息,但是这个API只在 iOS 10.0, macOS 10.12, tvOS 10.0和watchOS 3.0以后的版本才可用。

下面是使用的例子:

import os.log
 
 // 直接打印一个String
os_log("This is a log message.")

// 拼接一个或多个参数
let fileSize = 1234567890
os_log("Finished downloading file. Size: %{iec-bytes}d", fileSize)

// 定义打印等级,例如info、debug、error、fault
os_log("This is additional info that may be helpful for troubleshooting.", type: .info)

// 打印信息到特定的子系统
let customLog = OSLog("com.your_company.your_subsystem_name.plist", "your_category_name")
os_log("This is info that may be helpful during development or debugging.", log: customLog, type: .debug)
Cocoa的结构

当Swift的结构被桥接成Objective-C时,下面这些结构会变成NSValue

  • CATransform3D
  • CLLocationCoordinate2D
  • CGAffineTransform
  • CGPoint
  • CGRect
  • CGSize
  • CGVector
  • CMTimeMapping
  • CMTimeRange
  • CMTime
  • MKCoordinateSpan
  • NSRange
  • SCNMatrix4
  • SCNVector3
  • SCNVector4
  • UIEdgeInsets
  • UIOffset

Cocoa设计模式

代理

代理设计模式,是我们经常用到的。在Objective-C中,在调用代理方法之前,我们首先要检查代理是否有实现这个代理方法。而在Swift中,我们可以使用可选链来调用代理方法。例如:

class MyDelegate: NSObject, NSWindowDelegate {
    func window(_ window: NSWindow, willUseFullScreenContentSize proposedSize: NSSize) -> NSSize {
        return proposedSize
    }
}
myWindow.delegate = MyDelegate()
if let fullScreenSize = myWindow.delegate?.window(myWindow, willUseFullScreenContentSize: mySize) {
    print(NSStringFromSize(fullScreenSize))
}
Lazy初始化

一个lazy属性只会在第一次被访问的时候才会初始化,相当于在Objective-C的懒加载(重写getter方法)。当需要进行比较复杂或者耗时的计算才能初始化一个属性时,我们应该尽量使用lazy属性。

在Objective-C中:

@property NSXMLDocument *XML;
 
- (NSXMLDocument *)XML {
    if (_XML == nil) {
        _XML = [[NSXMLDocument alloc] initWithContentsOfURL:[[Bundle mainBundle] URLForResource:@"/path/to/resource" withExtension:@"xml"] options:0 error:nil];
    }
 
    return _XML;
}

而在Swift,我们使用lazy属性:

lazy var XML: XMLDocument = try! XMLDocument(contentsOf: Bundle.main.url(forResource: "document", withExtension: "xml")!, options: 0)

对于其他需要更复杂的初始化的属性,可以写成:

lazy var currencyFormatter: NumberFormatter = {
    let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    formatter.currencySymbol = "¤"
    return formatter
}()
单例

单例模式使我们在开发中经常用到的。

在Objective-C中,我们通常用GCD来实现:

+ (instancetype)sharedInstance {
    static id _sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedInstance = [[self alloc] init];
    });
 
    return _sharedInstance;
}

在Swift中,直接使用static类型属性即可,可以保证只初始化一次,即使时在多线程中被同时访问。

class Singleton {
    static let sharedInstance = Singleton()
}

如果我们需要其他设置,可以写成:

class Singleton {
    static let sharedInstance: Singleton = {
        let instance = Singleton()
        // setup code
        return instance
    }()
}
API可用性

有些类和方法并不是在所有平台或者版本都可用的,所有有时我们需要进行API可用性检查。

例如,CLLocationManagerrequestWhenInUseAuthorization方法只能在iOS 8.0和macOS 10.10以后的版本才能使用:

let locationManager = CLLocationManager()
if #available(iOS 8.0, macOS 10.10, *) {
    locationManager.requestWhenInUseAuthorization()
}

*是为了处理未来的平台。

平台名称:

  • iOS
  • iOSApplicationExtension
  • macOS
  • macOSApplicationExtension
  • watchOS
  • watchOSApplicationExtension
  • tvOS
  • tvOSApplicationExtension

同样地,我们在写自己的API时,也可以指定那些平台可以使用:

@available(iOS 8.0, macOS 10.10, *)
func useShinyNewFeature() {
    // ...
}

Swift和Objective-C混编

把Objective-C代码导入Swift

为了把Objective-C代码导入Swift中,我们需要用到Objective-C bridging header。当你把Objective-C文件拖入Swift项目中时,Xcode会提示你是否新建一个bridging header,如下图:

Create Bridging Header

点击Create Bridging Header,项目的文件路径下就会创建一个名为项目名称-Bridging-Header.h的文件(如果项目名称不是英文,将会以_代替;如果第一个是字母,也会以_代替)。

当然,我们也可以手动创建:File > New > File > (iOS, watchOS, tvOS, or macOS) > Source > Header File。

-Bridging-Header.h文件创建好之后,我们还需要进行以下操作:

  • 把Swift中要用到的Objective-C类的头文件,以下面这种形式添加到Bridging-Header.h文件
#import "XYZCustomCell.h"
#import "XYZCustomView.h"
#import "XYZCustomViewController.h"
  • 在Build Settings > Swift Compiler - General > Objective-C Bridging Header添加-Bridging-Header.h的路径,路径的格式:项目名/项目名称-Bridging-Header.h如图
Objective-C Bridging Header

这样我们就配置完成了,可以在Swift中调用Objective-C的代码:

let myCell = XYZCustomCell()
myCell.subtitle = "A custom cell"
Swift代码导入Objective-C

当需要在Objective-C中使用Swift的代码时,我们依赖于Xcode自动生成的头文件,这个头文件的名称是项目名-Swift.h(如果项目名称不是英文,将会以_代替;如果第一个是字母,也会以_代替)。

默认情况下,这个自动生成的头文件包含了在Swift中被public或者open标记的声明,如果这个项目中有Objective-C bridging header,那么,internal标记的声明也包含在内。被privatefileprivate标记的不包含在内。私有的声明不会暴露给Objective-C,除非他们被@IBAction@IBOutlet或者@objc标记。

当需要在Objective-C中使用Swift的代码时,直接导入头文件项目名-Swift.h,然后我们就可以在Objective-C中调用Swift的接口,用法与Objective-C的语法相同:

// 初始化实例,并调用方法
MySwiftClass *swiftObject = [[MySwiftClass alloc] init];
[swiftObject swiftMethod];

// 引用类或者协议
// MyObjcClass.h
@class MySwiftClass;
@protocol MySwiftProtocol;
 
@interface MyObjcClass : NSObject
- (MySwiftClass *)returnSwiftClassInstance;
- (id <MySwiftProtocol>)returnInstanceAdoptingSwiftProtocol;
// ...
@end

// 实现Swift协议
// MyObjcClass.m
#import "ProductModuleName-Swift.h"
 
@interface MyObjcClass () <MySwiftProtocol>
// ...
@end
 
@implementation MyObjcClass
// ...
@end



注意:如果是刚刚写的Swift代码,马上就想在Objective-C调用,我们需要先编译一下,然后Objective-C中才能访问到Swift的接口。

声明可以被Objective-C使用的Swift协议

为了声明一个可以被Objective-C使用的Swift协议,我们要用@objc标记,如果协议的方法是optional,也需要用@objc

@objc public protocol MySwiftProtocol {
    func requiredMethod()
    
    @objc optional func optionalMethod()
}

把Objective-C代码转为Swift

前面讲了一大堆基础知识,就是为了更好地将Objective-C代码转为Swift。

迁移过程
  • 创建一个对应Objective-C.m.h的Swift类,创建方法:File > New > File > (iOS, watchOS, tvOS, or macOS) > Source > Swift File。类的名称可以相同,也可以不同。
  • 导入相关的系统框架
  • 如果要需要用到Objective-C的代码,需要在bridging header中导入相关的头文件
  • 为了让这个Swift类可以在Objective-C中使用,需要让这个类继承自Objective-C的类。如果要自定义在Objective-C中调用的Swift接口的名称,使用@objc(name)
  • 我们可以通过继承Objective-C的类,实现Objective-C协议等来集成Objective-C已有的成员。
  • 在迁移过程中,我们要知道:1)Objective-C的语言特性转换成Swift后,是变成怎样;2)Cocoa框架中Objective-C的类型,在Swift中是什么类型;3)常用的设计模式;4)Objective-C的属性如何迁移到Swift。这些大部分内容我上面都有提到。
  • Objective-C的(-)和(+)方法,对应到Swift就是funcclass func
  • Objective-C的简单的宏定义改为全局常量,复杂的宏定义改为方法
  • 迁移完成后,在有导入Objective-C类的地方,用#import "项目名称-Swift.h"替换。
  • 把之前的.m文件的target membership这个勾去掉。先别着急把之前的.m.h文件删掉,因为我们刚刚写完的Swift类可能不太完善,我们还需要用之前的文件来解决问题。
target membership
  • 如果Swift的类名和之前的Objective-C的类名不一样,在用到Objective-C的类的地方,更新为新的类名。

有任何问题,欢迎大家留言!

欢迎加入我管理的Swift开发群:536353151,本群只讨论Swift相关内容。

原创文章,转载请注明出处。谢谢!

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

推荐阅读更多精彩内容