Object-C规约

语言规约

命名规约

  • 【强制】命名约定通用准则:清晰、一致性、不能自我指涉
    清晰:命名应该既清晰又简短,但拒绝为了追求简短而丧失清晰性,拒绝为了简洁进行随意缩写。

正例:

命名 说明
insertObject:atIndex: 好的命名
removeObjectAtIndex: 好的命名
removeObject: 这样命名也不错,因为方法将移除通过参数引用的对象。
destinationSelection 好的命名
setBackgroundColor: 好的命名

反例:

命名 说明
insert:at: 不清晰;插入什么?“at”表示什么?
remove: 不清晰:要移除什么?
destSel 不清晰,缩写含义不清
setBkgdColor: 缩写含义不清

一致性:命名含义应该具有前后,全局的一致性,同个功能也应该使用同个名称。

命名 说明
- (NSInteger)tag 该方法同时定义在 NSView、 NSCell、 NSControl 这三个类里面。

不能自我指涉:除掩码常量、通知外,名称不应该自我指涉(self-reference)。

正例:

命名 说明
NSString 可以使用
NSUnderlineByWordMask 掩码常量,可以使用Mask自我指涉
NSTableViewColumnDidMoveNotification 通知常量,可以使用Mask自我指涉

反例:

命名 说明
NSStringObject NSString本身已经是Object了,不需要再在名字里显示指出
  • 【强制】杜绝一切缩写,除以下已经长期使用形成共识的内容可以使用缩写。
命名 说明
alloc Allocate
alt Alternate
app Application.
calc Calculate
dealloc Deallocate
func Function
horiz Horizontal
info Information
init Initialize
int Integer
max Maximum
min Minimum
msg Message
nib Interface Builder archive
pboard Pasteboard
rect Rectangle
Rep Representation
temp Temporary
vert Vertical
  • 【强制】文件名、自定义类、Protocol禁止以以下系统前缀开头。
    【AC,AB,AS,AL,AU,AV,CX,CF,CK,CN,CA,CB,NS,CF,CG,CI,CL,CM,MIDI,CM,CS,CT,CV,EK,EA,GC,GK,GLK,GSS,HK,HM,AD,CG,IN,GS,LA,MK,MA,MP,MT,MS,MF,MTL,MTK,MDL,MC,NE,NK,NC,AL,EAGL,PK,PH,CA,QL,RP,SF,SCN,SL,SF,SK,SC,TW,UI,UN,VS,VT,WC,WK】

  • 【参考】文件名、类、Protocol、常量、枚举等全局可见内容需要添加三个大写字符作为前缀,双字母前缀为 Apple 的类预留。尽管这个规范看起来有些古怪,但是这样做可以减少 Objective-C 没有命名空间所带来的问题。

  • 【强制】方法名、参数名、成员变量、局部变量都采用小写字符开头,名称中的单词首字符要大写的小驼峰形式。另外,请不要在方法名称中使用前缀(category方法除外)。

fileExistsAtPath:isDirectory:
  • 【强制】如果方法代表一个对象执行的动作,则其名称应该以一个动词开头:
- (void)invokeWithTarget:(id)target; 
- (void)selectTabViewItem:(NSTabViewItem *)tabViewItem;
  • 【强制】请不要使用 “do”或者 “does”作为名称的一部分,因为这些辅助性的动词不能为名称增加更多的含义。同时,请不要在动词之前使用副词或者形容词。

  • 【强制】如果方法返回接收者的某个属性,则以属性名称作为方法名。如果方法没有间接地返回 一个或多个值,您也无须使用”get“这样的单词。

正例:

- (NSSize)cellSize;

反例:

 - (NSSize)calcCellSize;
 - (NSSize)getCellSize;
  • 【强制】所有参数前面都应使用关键字。

正例:

- (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag;

反例:

- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
  • 【强制】如果您当前创建的方法比起它所继承的方法更有针对性,则您应该在已有的方法名称后 面添加关键字,并将其作为新方法的名称。
命名 说明
- (instancetype)initWithFrame:(NSRect)frameRect; NSView
- (instancetype)initWithFrame:(NSRect)frameRect mode:(int)aMode cellClass:(Class)factoryId numberOfRows:(int)rowsHigh numberOfColumns:(int)colsWide; NSMatrix 是 NSView 的子 类。
  • 【推荐】请不要使用”and“来连接两个表示接受者属性的关键字。
    虽然下面的例子使用”and“这个词感觉还不错,但是随着创建的方法所带有的关键字越来 越多,这种方式会引起问题

正例:

- (int)runModalForDirectory:(NSString *)path file:(NSString *) name types:(NSArray *)fileTypes;

反例:

- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
  • 【推荐】如果方法描述了两个独立的动作,请使用”and“把它们连接起来。

正例:

- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
  • 【强制】您可以使用情态动词(在动词前冠以“can”,"should","will"等),使得方法的名称更加明 确,但是请不要使用“do”或“does”这样的情态动词。

正例:

- (void)setCanHide:(BOOL)flag;      
- (BOOL)canHide;
- (void)setShouldCloseDocument:(BOOL)flag;
- (BOOL)shouldCloseDocument; 

反例:

- (void)setDoesAcceptGlyphInfo:(BOOL)flag; 
- (BOOL)doesAcceptGlyphInfo;
  • 【强制】只有当方法间接地返回对象或者数值,您才需要在方法名称中使用 get"。这种格式只适 用于需要返回多个数据项的方法。

正例:

- (void)getLineDash:(float *)pattern count:(int*)count phase:(float *)phase;
  • 【强制】下面是数条和方法参数命名相关的通用规则:
    (1)和方法名称一样, 参数的名称也是以小写的字符开头,并且后续单词的首字符要大写。 例如:removeObject:(id)anObject。
    (2)请不要在参数名称中使用"pointer"或者"ptr"。您应该使用参数的类型来声明参数是否是 一个指针。
    (3)请不要使用一到两个字符的名称作为参数名。
    (4)请不要使用只剩几个字符的缩写。

  • 【强制】方法名称的开头应标识出发送消息的对象所属的类:

正例:

- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
- (BOOL)application:(NSApplication *)sender openFile:(NSString
*)filename;
  • 【强制】如果调用某个方法是为了通知委托某个事件已经发生或者即将发生, 则请在方法名称 中使用“did”或者“will”这样的助动词。

正例:

- (void)browserDidScroll:(NSBrowser *)sender;
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;
  • 【强制】如果调用某个方法是为了要求委托代表其他对象执行某件事,当然,您也可以在方法名 称中使用“did”或者“will”,但我们倾向于使用“should”。

正例:

- (BOOL)windowShouldClose:(id)sender;

常量定义

  • 【强制】请使用NS_ENUM枚举类型来表示一群相互关联的整数值常量。枚举项以枚举类型为前缀。

示例:

typedef NS_ENUM(NSInteger,NSMatrixMode) {
NSMatrixModeRadio = 0,
NSMatrixModeHighlight = 1,
NSMatrixModeList = 2,
NSMatrixModeTrack = 3
} ;
  • 【强制】请使用NS_OPTIONS定义一组相互关联的位移枚举常量。位移枚举常量是可以组合使用的。枚举项以枚举类型为前缀。

示例:

typedef NS_OPTIONS(NSUInteger,NSMatrixModeMask) {
NSMatrixModeMaskBorderless     = 0,
NSMatrixModeMaskTitled = 1 << 0,
NSMatrixModeMaskClosable = 1 << 1,
NSMatrixModeMaskMiniaturizable = 1 << 2,
NSMatrixModeMaskResizable = 1 << 3
};
  • 【强制】请使用 const 来创建浮点值常量。如果某个整数值常量和其他的常量不相关,您也可以使用 const 来创建,否则,则应使用枚举类型。下面的声明展示了 const 常量的格式:
const float NSLightGray;
  • 【强制】通常情况下,请不要使用#define 预处器理命令创建常量。对于整数值常量,请使用枚举类型创建,而对于浮点值常量,请使用const 修饰符创建。

  • 【强制】有些符号,预处理器需要对其进行计算,以便决定是否要对某一代码块进行处理,则它 们应该使用大写字符表示。例如:

#ifdef DEBUG
  • 【强制】推荐使用常量来代替字符串字面值和数字。常量应该用 static 声明为静态常量,而不要用 #define,除非它明确的作为一个宏来使用。
    这样能够方便复用,而且可以快速修改而不需要查找和替换

正例:

static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;

反例:

#define CompanyName @"Apple Inc."
#define magicNumber 42

常量应该在头文件中以这样的形式暴露给外部:

extern NSString *const ZOCCacheControllerDidClearCacheNotification;

并在实现文件中为它赋值。

  • 【强制】异常使用全局的 NSString 对象来标识,其名称按如下的方式进行组合:异常名称中的具有唯一性的那部分,其组成词应该拼写在一起, 并且每个单词的首字符要大写。
    [Prefix] + [UniquePartOfName] + Exception

正例:

NSColorListIOException
NSColorListNotEditableException 
NSDraggingException
NSFontUnavailableException
NSIllegalSelectorException
  • 【强制】Notification消息使用全局的 NSString 对象进行标识,其名称按如下的方式组合:
    [Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification

正例:

NSApplicationDidBecomeActiveNotification  
NSWindowDidMiniaturizeNotification
NSTextViewDidChangeSelectionNotification
NSColorPanelColorDidChangeNotification

类定义规约

  • 【推荐】要尽可能地使用属性定义代替无修饰的实例变量。

正例:

@interface Item : NSObject
@property (nonatomic, copy) NSString* name;
@end

反例:

@interface Item : NSObject {
    NSString* _name
}
@end
  • 【推荐】如果需要自定义property的getter或setter方法时,请在声明property时一起声明掉。
@property(getter=my_XXX ,setter=my_setXXX) id xxx;
  • 【推荐】对外暴露的属性,尽量定义为readonly。

  • 【推荐】不建议使用@dynamic修饰属性,除非你真的知道自己在干什么。

  • 【强制】在为某个类添加实例变量时,请记住下面几个因素:
    (1)只暴露必须的对外接口或属性在.h中,其它private保留在.m里。
    (2)请确保实例变量的名称能够扼要地描述它所保存的属性。

  • 【强制】不复写任何 + (void) load方法。所有的load方法的执行在Class的装载阶段,会延长App的启动时间.且如果存在稳定性问题,也没有可以修复的时机。

  • 【强制】+(void)initialize必须判断class类型或使用dispatch_once防止执行多次.
    由于任何继承类也会执行父类的initilize,所以这里一定要做类型判断,或使用dispatch_once来保障不会执行多次

if (self == [NSFoo class]) {
    // the initializing code
  }
  • 【强制】不应该显式地调用 initialize方法。如果需要触发初始化行为,则请调用一些无害的方法。

正例:

[NSImage self];

反例:

[NSImage initialize];
  • 【强制】在property的getter方法里不能再显式或者隐式的调用同一个property的getter方法,会导致死循环。

反例:

@interface ALPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation ALPerson
- (void)setName:(NSString *)name {
    self.name = name;//死循环!
}
@end
  • 【推荐】方法调用,方法调用应尽量保持与方法声明的格式一致。当格式的风格有多种选择时,新的代码要与已有代码保持一致。

调用时所有参数应该在同一行:

[myObject doSomethingWith:arg1 name:arg2 error:arg3];

或者每行一个参数,以冒号对齐:

[myObject doSomethingWith:arg1
               name:arg2
              error:arg3];

方法定义与方法声明一样,当关键字的长度不足以以冒号对齐时,下一行都要以四个空格进行缩进。

[myObj short:arg1
    longKeyword:arg2
    evenLongerKeyword:arg3];
  • 【推荐】使用nonnull、nullable,__kindof来修饰方入参数、返回值、属性
@property (nonatomic, strong, nonnull) Sark *sark;
@property (nonatomic, copy, readonly, nullable) NSArray *friends;
+ (nullable NSString *)friendWithName:(nonnull NSString *)name;
  • 【强制】禁止从designated initializer 里面调用一个 secondary initializer。如果这样,调用很可能会调用一个子类重写的init方法并且陷入无限递归之中。
    a. Objective-C 有指定初始化方法(designated initializer)和间接(secondary initializer)初始化方法的观念。designated 初始化方法是提供所有的参数,secondary 初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用designated 初始化的初始化方法。
    b. 一个类应该有且只有一个designated初始化方法,其他的初始化方法应该调用这个designated的初始化方法
    c. 在希望提供你自己的初始化函数的时候,应该遵守这三个步骤来保证获得正确的行为:
    (1)定义你的designated initializer,确保调用了直接超类的 designated initializer
    (2)重载直接超类的 designated initializer。调用你的新的 designated initializer
    (3)为新的 designated initializer 写文档。可以用编译器的指令 __attribute__((objc_designated_initializer))来标记。用编译器指令__attribute__((unavailable(Invoke the new designated initializer))让父类的 designated initializer 失效.

正例

@interface ZOCNewsViewController : UIViewController

- (instancetype)initWithNews:(ZOCNews *)news __attribute__((objc_designated_initializer));
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil __attribute__((unavailable("Invoke the designated initializer,call initWithNews:")));
- (instancetype)init __attribute__((unavailable("Invoke the designated initializer,call initWithNews:"));

@end

@implementation ZOCNewsViewController

- (id)initWithNews:(ZOCNews *)news
{
    //调用直接父类的 designated initializer
    self = [super initWithNibName:nil bundle:nil];
    if (self) {
        _news = news;
    }
    return self;
}

// 重载直接父类的  designated initializer
// 如果你没重载 initWithNibName:bundle: ,而且调用者决定用这个方法初始化你的类(这是完全合法的)。 initWithNews: 永远不会被调用,所以导致了不正确的初始化流程,你的类的特定初始化逻辑没有被执行。
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    // call the new designated initializer
    return [self initWithNews:nil];
}

@end

反例

@implementation ParentObject

//designated initializer    
- (instancetype)initWithURL:(NSString*)url title:(NSString*)title {
    if (self = [super init]) {
        _url = [url copy];
        _title = [title copy];
    }
    return self;
}
//secondary initializer
- (instancetype)initWithURL:(NSString*)url {
    return [self initWithURL:url title:nil];
}

@end

@interface ChildObject : ParentObject

@end

@implementation ChildObject
//designated initializer
- (instancetype)initWithURL:(NSString*)url title:(NSString*)title {
    //在designated intializer中调用 secondary initializer,错误的
    if (self = [super initWithURL:url]) {

    }
    return self;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 这里会死循环
    ChildObject* child = [[ChildObject alloc] initWithURL:@"url" title:@"title"];
}
@end

注释规约

  • 【强制】头文件中的暴露的方法或者属性都必须添加注释

  • 注释建议使用Xcode自带工具插入默认格式。option+command+/即可自动插入。

  • 【强制】自动生成的代码注释中的placeholder要替换掉

  • 【推荐】建议对于复杂难懂逻辑添加注释

代码组织规约

  • 【推荐】当一个类功能很多时,建议使用Category的方式进行功能划分,这些Category可以放在同一个文件中。

示例:

@interface UIViewController (UIViewControllerRotation)

+ (void)attemptRotationToDeviceOrientation NS_AVAILABLE_IOS(5_0) __TVOS_PROHIBITED;
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation NS_DEPRECATED_IOS(2_0, 6_0) __TVOS_PROHIBITED;
@end

@interface UIViewController (UILayoutSupport)
@property(nonatomic,readonly,strong) id<UILayoutSupport> topLayoutGuide NS_AVAILABLE_IOS(7_0);
@property(nonatomic,readonly,strong) id<UILayoutSupport> bottomLayoutGuide NS_AVAILABLE_IOS(7_0);
@end

@interface UIViewController (UIKeyCommand)
- (void)addKeyCommand:(UIKeyCommand *)keyCommand NS_AVAILABLE_IOS(9_0);
- (void)removeKeyCommand:(UIKeyCommand *)keyCommand NS_AVAILABLE_IOS(9_0);

@end
  • 【推荐】建议使用#pragma marks -来进行方法分组,提高可读性,具体样例如下,建议把生命周期,事件,property方法以及protocol方法进行区分。

示例:

#pragma mark - Lifecycle

- (instancetype)init {}
- (void)dealloc {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
- (void)didReceiveMemoryWarning {}

#pragma mark - Custom Accessors

- (void)setCustomProperty:(id)value {}
- (id)customProperty {}

#pragma mark - IBActions

- (IBAction)submitData:(id)sender {}

#pragma mark - Public

- (void)publicMethod {}

#pragma mark - Private

- (void)privateMethod {}

#pragma mark - Protocol conformance
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate

#pragma mark - NSCopying

- (id)copyWithZone:(NSZone *)zone {}

#pragma mark - NSObject

- (NSString *)description {}
  • 【推荐】建议合理使用group或folder来组织工程结构,而不是全部放在source里,物理group与工程中group要对应

  • 【推荐】过期方法,不要直接删除,先标记为depcrated

  • 【推荐】建议类继承关系不要超过2层,并且抽取公共逻辑到父类,尽量避免父类,子类方法调用跳跃

  • 【参考】尽量减少继承,可以考虑组合,category,protocol等方式

  • 【推荐】每个文件.m的方法数目不应该超过20个,每个方法的行数不应该超过200行。

每个方法应该只做一件事情。当函数过长时,它做的事情通常会不明确,后续会很难理解与维护。

  • 【强制】函数内嵌套不能太深,一个函数内大括号里嵌套大括号不能超过三层。

超过三层已经很难理解一个函数的作用,可以将其中的一些逻辑抽离成一个单独的函数

  • 【推荐】建议业务bundle使用统一的前缀来标识

  • 【推荐】头文件中只暴露出需要给他人调用的类、方法及属性,私有类、方法、变量放在.m中

  • 【强制】Release包必须关闭Log

  • 【强制】必须清理工程中的所有warning

  • 【推荐】长条件判断建议使用bool变量来代替

太长不容易调试,且不直观。

正例:

BOOL isConditionSatisfied = (1 == a.x &&  3==b.y && 2 == c.x);
if (isConditionSatisfied){
 doSomething()
}

反例:

if (a.x = 1 && b.y =3 && c.x = 2){
 doSomething()
}
  • 【推荐】条件判断,推荐加大括号,即使一行,容易导致的错误为,当 if 语句里面的一行被注释掉,下一行就会在不经意间成为了这个 if 语句的一部分。参考

正例:

if (!error) {
    return success;
}

反对:

if (!error)
    return success;

if (!error) return success;
  • 【推荐】对三目运算使用时,要注意简化,x=a?a:b只要写成x=a?:b 即可;

  • 【推荐】编写switch语句的时候, 一定要实现default:,防止外部异常调用,内部没有处理的情况,

  • 【强制】switch里每个case里需要强制有break;

  • 【强制】switch里每个case里都要使用{}所有代码括起来,就算只有一行;

最佳实践

多线程

  • 【强制】自建线程必须命名。

  • 【强制】多线程访问同一个对象时,必须注意临界区的保护

  • 【强制】单例创建要使用线程安全模式,并且禁止在单例的init方法中使用dispatch_sync来阻塞线程,极易出现死锁

正例:

+ (instancetype)sharedInstance {
       static id sharedInstance = nil;
       static dispatch_once_t onceToken;
       dispatch_once(&onceToken, ^{ 
          sharedInstance = [[self alloc] init];
       });
       return sharedInstance;
    }
  • 【强制】在多线程环境下使用懒加载方式加载变量,会有crash风险,必须加锁保护

正例:

//多线程环境下调用
- (NSCache *)contactCache
{
    if (!_contactCache) {
        @synchronized(self) { 
            if (!_contactCache) {
                _contactCache = [[NSCache alloc] init];
                _contactCache.name = @"contactCache";
            }
        }
    }
    return _contactCache;
}
  • 【强制】performSelector:withObject:afterDelay:要在有Runloop的线程里调用,否则调用无法生效。

说明:异步线程默认是没有runloop的,除非手动创建;而主线程是系统会自动创建Runloop的。所以在异步线程调用是请先确保该线程是有Runloop的。

  • 【强制】禁止随意创建长驻线程,除非是在整个app运行周期内都必须存在且有任务运行的。

  • 【推荐】NSNotificationCenter在iOS 8及更老系统上存在多线程bug,selector执行到一半时可能会因为self销毁而触发crash,解决方案是在selector里开始的地方引入下面的宏:

- (void)onMultiThreadNotificationTrigged:(NSNotification *)notify
{
   __weak typeof(self) weakSelf = self;
   __strong typeof(self) strongSelf = wself;
   if (! weakSelf) { 
    return; 
   }

   [strongSelf doSomething];
}
  • 【推荐】在多线程应用中,Notification在哪个线程中post,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中。如果发送消息的不在主线程,而接受消息的回调里做了UI操作,需要让其在主线程执行。

  • 【推荐】仅当必须保证顺序执行时才使用dispatch_sync,否则容易出现死锁,应避免使用,可使用dispatch_async

反例:

// 禁止。出现死锁,报错:EXC_BAD_INSTRUCTION。原因:在主队列中同步的添加一个block到主队列中
- (void)viewDidLoad {
  [super viewDidLoad];

  dispatch_queue_t mainQueue = dispatch_get_main_queue();
  dispatch_block_t block = ^() {
      NSLog(@"%@", [NSThread currentThread]);
  };
  dispatch_sync(mainQueue, block);
}

正例:

 // 推荐
- (void)viewDidLoad {
  [super viewDidLoad];

  dispatch_queue_t mainQueue = dispatch_get_main_queue();
  dispatch_block_t block = ^() {
      NSLog(@"%@", [NSThread currentThread]);
  };
  dispatch_async(mainQueue, block); //使用异步操作
}
  • 【参考】使用 performSelector:withObject:afterDelay:cancelPreviousPerformRequestsWithTarget组合的时候要小心
  • afterDelay会增加receiver的引用计数,cancel则会对应减一
  • 如果在receiver的引用计数只剩下1 (仅为delay)时,调用cancel之后会立即销毁receiver,后续再调用receiver的方法就会crash

正例:

__weak typeof(self) weakSelf = self;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
if (!weakSelf)
{
//NSLog(@"self被销毁");
    return;
}

[self doOther];
  • 【强制】禁止在非主线程中进行UI元素的操作

  • 【强制】在主线程中禁止进行同步网络资源读取,使用NSURLSession进行异步获取

  • 【强制】如果需要进行大文件或者多文件的IO操作,禁止主线程使用,必须进行异步处理

  • 【强制】对剪贴板的读取必须要放在异步线程处理,最新Mac和iOS里的剪贴板共享功能会导致有可能需要读取大量的内容,导致读取线程被长时间阻塞

正例:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; 
   if (pasteboard.string.length > 0) {//这个方法会阻塞线程
      NSString *text = [pasteboard.string copy];
      [pasteboard setValue:@"" forPasteboardType:UIPasteboardNameGeneral];
      if (text == nil || [text isEqualToString:@""]) {
          return ;
      }
      dispatch_async(dispatch_get_main_queue(), ^{
          [self processShareCode:text];
      });
   }
});

内存管理

  • 【推荐】请慎重使用单例,避免造成产生不必要的常驻内存。

  • 【推荐】单例初始化方法中尽量保证单一职责,尤其不要进行其他单例的调用。极端情况下,两个单例对象在各自的单例初始化方法中调用,会造成死锁。

  • 【强制】Delegate需要用weak进行引用。

  • 【强制】使用block时,需要在block访问外部weak修饰的self,内部在重新strong处理。避免RetainCycle

  • 【推荐】strong引用 子实例,weak引用parent,基础类型使用assign,NSString,NSArray,block使用copy

  • 【强制】对类添加属性时使用 copy方式还是使用retain方式规约:

  • 对实现 NSCopying协议的对象使用copy方式。通常情况下,诸如NSString、NSURL, block,NSArray 这样的对象应该能被copy;
  • UIView的对象则应该可以被保持。strong引用 子实例,weak引用parent.
  • 基础类型使用assign
  • 【强制】在dealloc中要记得要remove observer, callback=null

  • 【强制】会循环使用的Timer(指定了repeat参数为YES),必须要在合适的时机调用invalidate方法,否则会出现内存泄漏,在使用类的析构函数中调用Timerinvalidate方法为时已晚,因为timer会对其传递的目标object增加引用计数,若不调用invalidate,使用类根本得不到析构。
    对于指定了repeat参数为NOTimer,则可以不调用invalidate方法。

  • 【强制】在initdealloc中不允许使用self访问属性(父类属性除外),只允许通过"_变量名"直接访问。

  • 容易出现重复创建对象,甚至crash问题
  • initdealloc阶段,self是一个不完整的对象。
  • 由于accessor方法是可以被子类重写的,在调用父类init初始化的时候,使用self访问属性会调到子类重写的(如果有)gettersetter,这就出现了先于子类init访问其属性或调用子类方法的情况,如果子类gettersetter中有一些特殊的处理逻辑,在某些极端情况下就可能出现行为不一致的问题。 由于在init函数返回前,对象结构和结构是不稳定的,在init函数内对任何方法的调用(尤其是public方法)都应该慎之又慎。dealloc同理。
  • 【推荐】在非initdealloc方法中访问属性推荐通过getter方法获取,不推荐直接使用“_变量名”。

  • 【推荐】在init中不需要直接使用的Property,建议使用lazyloading的方法创建。

  • 【强制】在创建大量临时的UIImage,或者 Model 之类的对象的时,用@autoreleasepool使autorelease 对象在结束时间释放,缓解内存的压力。比如:

正例:

NSMutableArray *dataList = [NSMutableArray new];
NSMutableArray *imageList = [NSMutableArray new];
[dataList enumerateObjectsUsingBlock:^(NSDictionary *dict, NSUInteger idx, BOOL *stop) {
    @autoreleasepool {
        NSData *data = dataList[idx];
        UIImage *image = [[UIImage alloc] initWithData:data];
        //可能对 image 进行一些处理,裁剪之类的
        [imageList addObject:image];
    }
}];
  • 【强制】在使用到 UIScrollView,UITableView,UICollectionViewClass 中,需要在dealloc方法里手动的把对应的 delegate, dataSouce置为 nil
  • 防止在scrollView滑动时页面退出,delegate释放,出现crash问题
  • 苹果在iOS9上已经将以上类的delegatedatasourceassign改为了weak,如果只支持9.0以上,则不需要手动置nil
  • 【推荐】在dealloc中,避免将self作为参数传递。如果被retain住,到下个runloop周期再次释放,则会造成多次释放crash
-(void)dealloc{
    [self unsafeMethod:self]; 
    //因为当前已经在self所指向对象的销毁阶段,如果在unsafeMethod:中将self放到了autorelease pool中,那么self会被retain住,计划下个runloop周期再进行销毁;但是dealloc运行结束后,self对象的内存空间就直接被回收了,self变成了野指针
    //当到了下个runloop周期,self指向的对象实际上已经被销毁,会因为非法访问造成crash问题
}
  • 【推荐】除非是非法参数等提前判断提前return的可以写在最前面。其他的return建议有效返回值尽量只剩最后一个。提前return时,要注意是否有对象没有被释放(常见的有CF对象),是否有锁没有释放等配对问题。

  • 【强制】禁止一次性申请超过10MB的内存。

内存过高将会导致app被kill,并且没有crash堆栈。而申请大内存将会增加内存峰值,更容易出现内存过高而crash。

集合

包括,但不限于 NSMutableDictionay,NSMutableArray,NSMutableSet

  • 【强制】插入对象需要做判空处理。

  • 【强制】注意线程安全问题,必要时加锁,保障线程安全

  • 【强制】先copy,再枚举操作,禁止对非临时变量的可变集合进行枚举操作,多线程情况下有可能因为可变集合在进行枚举时发生改变进而crash。

正例:

//正确的写法
- (void)checkAllValidItems{
    [_arrayLock lock];
    NSArray *array = [oldArray copy];
    [_arrayLock unlock];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        //do something using obj

    }];
}

反例:

//错误的写法
-(void)checkAllValidItems{
    [self.allItems enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
         //do something using obj
         //如果在enumerate过程中其它线程对self.allItems进行了变更操作,这里就会引发crash
     }];
}
  • 【推荐】大部分情况下都不使用可变集合作为成员变量,如果确实需要进行集合的增删改操作,使用临时可变集合变量处理,之后再进行赋值操作。

  • 【强制】禁止返回mutable对象,禁止mutable对象作为入参传递。

  • 【推荐】如果使用NSMutableDictionary作为缓存,推荐使用NSCache代替

  • 【推荐】容器类使用泛型来指定对象的类型

正例:

@property (readonly) NSArray<NSURL *> *imageURLs;

NSDictionary<NSString *, NSNumber *> *mapping = @{@"a": @1, @"b": @2};

反例:

@property (readonly) NSArray *imageURLs;

NSDictionary *mapping = @{@"a": @1, @"b": @2};

字符串

  • 【推荐】当使用keypaths:@"xx"时候,尽量使用NSStringFromSelector(@selector(xx))方式,防止某个key被删除后没有编译感知

  • 【强制】取substring的时候要考虑emoji字符的问题,防止截到中间crash

- (NSString *)dt_substringToIndex:(NSUInteger)index
 {
    //... 越界判断
    NSRange wRange = [self rangeOfComposedCharacterSequencesForRange:NSMakeRange(0, index)];
    return [self substringWithRange:wRange];
 }

  • 【推荐】专锁专用,一个lock对象只负责一个任务。这样可以在逻辑上进行区分,也可以避免潜在的死锁问题

  • 【推荐】不同锁的使用场景:
    (1)性能最好的属pthread_mutex、dispatch_semaphore,另外dispatch_semaphore在等待的时候会释放CPU资源,所以适合用在等待耗时较长的场景;
    (2)@synchronized是最简单易用的递归锁,不会有忘记unlock的情况,但性能也是最低的,适合用在对性能要求不高的场景;
    (3)其他的还有NSLock,性能介于上面二者之间,也有对应的条件锁NSConditionLock和递归锁NSRecursiveLock,因为是Objective-C对象,适合用在偏Objective-C编程的场景,比如需要把锁存放在NSDictionary中的场景。

  • 【强制】在使用锁的过程中如果要return,切记要先进行unlock; 如果可能有exception发生,那么需要在@finally中进行锁的释放

正例:

- (void) exclusiveMethod1{
    [self.lock lock];

    if (condition == true){
        //这里要记得unlcok,否则下次在进入这个方法就会发生线程被死锁的问题
        [self.lock unlock];
        return;
    }

    [self.lock unlock];
}

- (void) exclusiveMethod2{
    [self.lock lock];

    @try{
        //异常发生
    }@catch(NSException* ex){
    }@finally{
        //此处需要进行锁的回收
        [self.lock unlock];
    }
}

IO

  • 【参考】尽量减少使用NSUserDefault.

  • 【推荐】[[NSUserDefaults standardUserDefaults] synchronize]block当前线程直到所有UserDefault里的内容写回存储;如果内容过多,重复调用的话会严重影响性能。建议只有在合适的时候(比如退到后台)再进行持久化操作(此方法即将deprecated,可以不再调用)

  • 【推荐】一些经常被读取的本地文件建议做好内存缓存,减少IO开销

  • 【推荐】文件存储路径请遵循以下规则:

(1) Documents目录:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。该路径可通过配置实现iTunes共享文件。可被iTunes备份。
(2) AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。
(3) Library目录:这个目录下有两个子目录:
Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.
Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。
可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。
(4) tmp 目录:这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。该路径下的文件不会被iTunes备份。

UI

  • 【推荐】不要在除了viewDidLoad方法之外调用ViewControllerself.view来进行view操作,特别是在一些系统通知之类的回调中,有可能造成self.view创建出来之后没有被加入到当前层级,导致子view的诡异问题.
- (void)didReceiveMemoryWarning{
    [super didReceiveMemoryWarning];
    [self.view doSomething]; //如果当VC已经被创建,但是view还没有加入到view层级中时(比如Tabbar初始化之后的非选中VC),此时接收到了内存警告,那么self.view会被直接创建,没有加入到层级,导致其子view可能处于异常的状态
}
  • 【推荐】如果想要获取appwindow,不要view.window来获取,可以使用[[UIApplication sharedApplication] keyWindow]来获取。

说明:如果view不在展示时,获取window会是nil,而不是真正的app所在的window.

  • 【强制】UI对象只允许在主线程访问。(避免在异步线程里释放,这样可以避免在dealloc时访问view结构导致问题)

  • 【强制】禁止在ViewControllerdealloc方法中访问self.view,会导致已经释放的view被再次重建,可能会造成各种不可预知的问题

  • 【强制】显示带textfieldalert之前,一定要确保键盘不在显示状态,否则会crash

参考文章
可以直接: [[[UIApplication sharedApplication].delegate window] endEditing:YES];

  • 【强制】禁止使用drawViewHierarchyInRect截屏

原因:截屏会消耗大内存和耗性能,不建议使用该技术方案.
推荐使用 snapshotViewAfterScreenUpdates

  • 【推荐】不建议将UIView类的对象加入到NSDictionary, NSSet,如有需要可以添加到NSMapTableNSHashTable

NSDictionary,NSSet会对加入的对象做strong引用,而NSMapTable、NSHashTable会对加入的对象做weak引用。

Category

  • 【强制】category方法加自定义前缀。防止与其它人冲突。

正例:

@interface NSString(category)
- (NSString*)ali_stripWhiteSpace;
@end

反例:

@interface NSString(category)
- (NSString*)stripWhiteSpace;
@end
  • 【强制】禁止category方法覆盖系统方法,防止出现方法调用的不确定性

  • 【推荐】对于一些提供category的工具库,建议根据不同类型功能拆分成不同的子bundle,方便引用方按需引用,控制App体积

  • 【强制】Category的源文件名称必须是“类名+扩展名.{h,m}”

正例:

NSString+ALiCategory.h

反例:

NSStringALiCategory.h
NSString_ALiCategory.h

异常

  • 【强制】不要在@finally块中使用return或者@throw等导致方法执行中断的语句,会导致@try内的return失效

其它

  • 【推荐】使用Method swizzle之前考虑是否有其他方法可以代替,禁止swizzle其他基础库,二方三方库的方法

  • 【强制】NSNotification接口,userInfoobject的使用要规范。

说明:object通常是指发出notification的对象,如果在发送notification的同时要传递一些信息,请使用userInfo,而不是object.

  • 【推荐】在使用固定格式的dateFormatter时候,需要设置setLocale"en_US_POSIX",防止一些不同日历下格式异常,

示例:

NSDate* now = [NSDate date];
NSDateFormatter* fmt = [[NSDateFormatter alloc] init];
fmt.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
NSString* string = "1996-12-19T16:39:57-08:00";
NSDate* date = fmt.dateFromString(string);
  • 【推荐】在使用CTTelephonyNetworkInfo的时候,务必使用全局的单例实例,这个类本身存在bug,如果有多实例会存在会导致小概率的crash。

  • 【强制】调用block时务必判断block是否为nil

  • 【推荐】调用delegateoptional方法时,判断delegate能否响应该方法,避免crash

  • 【强制】禁止访问对象的结构体变量(使用->)

  • 【强制】需要使用磁盘缓存的业务,务必提供清理缓存的能力

  • 【强制】对于不确定对象类型的比较,可以使用isEqual:方法,其会对类型进行判断;对于确定对象类型的比较,比如NSString,可以使用isEqualToString:,其不对类型进行判断,但相比前者性能更好。

工程规约

版本管理规约

  • 【建议】遵循语义化版本号规范,版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

主版本号:当你做了不兼容的 API 修改,
次版本号:当你做了向下兼容的功能性新增,
修订号:当你做了向下兼容的问题修正。
先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

  • 【建议】App灰度使用四位版本号

  • 【建议】业务方维护自己业务SDK的版本号,不要使用主App的版本号来做业务逻辑判断,如果有需要可以使用业务SDK的版本号来判断

分支管理

  • 【建议】主分支Master
    代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。

  • 【建议】开发分支Develop
    日常开发分支在Develop,如果想正式对外发布,就在Master分支上,对Develop分支进行"合并"(merge)。

  • 【建议】临时性分支,按不同的需求,开启相应的临时分支,使用完以后,应该删除

功能(feature)分支
预发布(release)分支
修补bug(fixbug)分支

  • 【强制】每次版本发布之后,都应该在代码仓库中对应的节点添加tag,保证版本的可回溯

  • 【参考】在 Git 提交时可以使用 [添加],[修改],[删除],[修复],[更新]等前缀词语来表明当前的Commit 信息。

包管理

  • 【参考】可以使用CocoaPods作为包管理工具
  • 【强制】检查podspecresource选项,不要把Podfile、podspec、InfoPlist.strings、Info.plist或者源文件等导出到使用方的工程中

Show Me The Code!

千言万语,不如直接看代码。这里我们简单写了一个Demo,用于展示规约中比较常见的一些条款的应用。

#import <Foundation/Foundation.h>

/**
 性别的枚举值

 - ALIPersonGenderMale: 男人
 - ALIPersonGenderFemale: 女人
 - ALIPersonGenderUnknown: 不知道是男是女
 */
typedef NS_ENUM(NSInteger, ALIPersonGender) {
    ALIPersonGenderMale,
    ALIPersonGenderFemale,
    ALIPersonGenderUnknown
};

/**
 人的基本类型
 */
@interface ALIPerson : NSObject

/**
 名称
 */
@property(nonatomic, copy, nonnull) NSString *name;

/**
 性别
 */
@property(nonatomic, assign) ALIPersonGender gender;

/**
 初始化方法

 @param aName 人的名字
 @param aGender 人的性别
 @return 返回人的实例
 */
- (nonnull instancetype)initWithName:(nonnull NSString *)aName gender:(ALIPersonGender)aGender;

@end


/**
 一个人的体育运动
 */
@interface ALIPerson (Sports)

/**
 扩展方法强调要有前缀,以免冲突
 */
- (void)alPlayBasketBallWithPerson:(nullable ALIPerson *)person;

@end

/**
 程序员常用的语言枚举

 - ALICoderLanguagesJava: Java
 - ALICoderLanguagesC: C语言
 - ALICoderLanguagesCPP: C++
 - ALICoderLanguagesJavaScript: JavaScript
 - ALICoderLanguagesPHP: PHP是世界上最好的语言
 - ALICoderLanguagesRuby: ruby
 - ALICoderLanguagesPython: python
 - ALICoderLanguagesOC: Objective-C
 */
typedef NS_OPTIONS(NSUInteger, ALICoderLanguages){
    ALICoderLanguagesNone = 0,
    ALICoderLanguagesJava = 1 << 0,
    ALICoderLanguagesC = 1 << 1,
    ALICoderLanguagesCPP = 1 << 2,
    ALICoderLanguagesJavaScript = 1 << 3,
    ALICoderLanguagesPHP = 1 << 4,
    ALICoderLanguagesRuby = 1 << 5,
    ALICoderLanguagesPython = 1 << 6,
    ALICoderLanguagesOC = 1 << 7
};

/**
 程序员的基本协议
 */
@protocol ALICoderProtocol <NSObject>

/**
 对大家说一句helloworld

 @return 具体的helloworld内容
 */
- (nonnull NSString *)sayHelloWorld;


/**
 喜欢的语言

 @return 具体的语言,这里为了做事例用了mask
 */
- (ALICoderLanguages)preferLanguages;

@end

//Notification消息使用全局的 NSString 对象进行标识,其名称按如下的方式组合:
//[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
//程序员即将写出一个bug的通知
extern NSString * _Nonnull const ALIChinaCoderWillWriteBugsNotification;
//程序员已经制造了一个bug的通知
extern NSString * _Nonnull const ALIChinaCoderDidWriteBugsNotification;

/**
 程序员的代理,主要负责在程序员要写出bug的时候告诉代理,让代理处理下,是否要修复bug,或者打醒程序员
 */
@protocol ALIChinaCoderDelegate <NSObject>

/**
 程序员即将写出bug的回调

 @param coder 程序员本身
 @param partner 程序员的搭档
 */
- (void)coder:(nonnull ALIPerson<ALICoderProtocol> *)coder willWriteBugsWithPartner:(nullable ALIPerson<ALICoderProtocol> *)partner;

/**
 程序员已经写出bug的回调

 @param coder 程序员本身
 @param partner 程序员的搭档,我们一般认为跟妹子一起,由于程序员喜欢装逼,更容易写出bug
 */
- (void)coder:(nonnull ALIPerson<ALICoderProtocol> *)coder didWriteBugsWithPartner:(nullable ALIPerson<ALICoderProtocol> *)partner;


@end

/**
 中国程序员
 */
@interface ALIChinaCoder : ALIPerson<ALICoderProtocol>

@property(nonatomic, weak, nullable) id<ALIChinaCoderDelegate> delegate;
/**
 结对编程

 @param partner 一起编程的程序员
 */
- (void)pairProgrammingWithPartner:(nullable ALIPerson<ALICoderProtocol> *)partner;

@end

/**
 美国程序员
 */
@interface ALIUSACoder : ALIPerson<ALICoderProtocol>

@end
#import "ALIPerson.h"

@implementation ALIPerson

#pragma mark - Life Cycle

- (instancetype)initWithName:(NSString *)aName gender:(ALIPersonGender)aGender{
    if (self = [super init]) {
        _name = aName;
        _gender = aGender;
    }
    return self;
}

@end

NSString * const ALIPersonNameYaoMing = @"姚明";

@implementation ALIPerson (Sports)

- (void)alPlayBasketBallWithPerson:(ALIPerson *)person{
    if ([person.name isEqualToString:ALIPersonNameYaoMing]) {
        NSLog(@"把球给我,我要回家!");
    }else{
        NSLog(@"看我装逼看我飞");
    }
}

@end

//尽量声明变量而非用宏
NSString * const ALIChinaCoderNameNotFound = @"没人跟你说你好世界";

NSString * const ALIChinaCoderWillWriteBugsNotification = @"ALChinaCoderWillWriteBugsNotification";
NSString * const ALIChinaCoderDidWriteBugsNotification = @"ALChinaCoderDidWriteBugsNotification";

@implementation ALIChinaCoder

#pragma mark - ALCoderProtocol

- (NSString *)sayHelloWorld{
    if (!self.name) {
        return ALIChinaCoderNameNotFound;
    }
    return [NSString stringWithFormat:@"%@说你好世界",self.name];
}

- (ALICoderLanguages)preferLanguages{
    return ALICoderLanguagesOC|ALICoderLanguagesJava|ALICoderLanguagesC;
}

#pragma mark - public

- (void)pairProgrammingWithPartner:(ALIPerson<ALICoderProtocol> *)partner{
    switch (partner.gender) {
        case ALIPersonGenderMale: {
            NSLog(@"我去。。");
            break;
        }
        case ALIPersonGenderFemale: {
            [[NSNotificationCenter defaultCenter] postNotificationName:ALIChinaCoderWillWriteBugsNotification object:self];
            [self.delegate coder:self willWriteBugsWithPartner:partner];
            NSLog(@"哦也。。");
            NSLog(@"看我手把手教妹子写代码");
            [[NSNotificationCenter defaultCenter] postNotificationName:ALIChinaCoderDidWriteBugsNotification object:self];
            [self.delegate coder:self didWriteBugsWithPartner:partner];
            break;
        }
        default: {
            NSLog(@"看情况再说。。");
            break;
        }
    }
}

@end

NSString * const ALIUSACoderNameNotFound = @"No one say hello world";

@implementation ALIUSACoder

#pragma mark - ALICoderProtocol

- (NSString *)sayHelloWorld{
    if (!self.name) {
        return ALIUSACoderNameNotFound;
    }
    return [NSString stringWithFormat:@"%@ say Hello World",self.name];
}

- (ALICoderLanguages)preferLanguages{
    return ALICoderLanguagesPHP;
}


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

推荐阅读更多精彩内容