一 . Category分类 Extension 扩展

分类是Objective-C 2.0之后添加的语言特性

它有如下功能:
  • 能够在不改变原来类内容的基础上,为类增加一些方法
  • 将类的实现分开写在几个分类里面
    • 可以把不同的功能组织到不同的Category里,从而减少单个文件的体积
    • 可以由多个开发者共同完成一个类
    • 可以按需加载想要的category
    • 可以声明类的私有方法
    • 可以模拟多继承。
分类使用过程中需要注意的点:
  • 分类只能增加方法,不能增加成员变量。
  • 分类方法实现中可以访问原来类中声明的成员变量。
  • 分类可以实现与原来类中相同的方法,但是这样做会覆盖掉原来的方法,实际上这里的覆盖和覆写类方法的覆盖是有区别的,这里的覆盖不是真正的替换,而是Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,而运行时在查找方法的时候是顺着方法列表的顺序查找的,只要一找到对应名字的方法,就会停止查找,导致原来类中的方法不会被使用。
  • 当分类、原来类、原来类的父类中有相同方法时,方法调用的优先级:分类(最后参与编译的分类优先) –> 原来类 –> 父类
  • Category是在runtime时候加载,而不是在编译的时候。
  • Category不能添加成员变量因为在运行期,对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局),但是可以结合关联对象来为分类添加属性
  • 可以在类的+load方法调用category中声明的方法,因为添加category到类的工作会先于+load方法的执行,load执行循序是先主类,然后分类,分类的顺序取决于在Build Phases的顺序。

总结来说类别可以添加实例方法,类方法,甚至可以实现协议,添加属性,但是不可以添加实例变量

Extension与Category区别

刚刚开始接触Objective C的时候最容易搞混的就是扩展与分类,最初感觉扩展就是匿名的分类一样,但是后面才知道其实扩展和分类是两码事

  • Extension 扩展
    Extension是类的一部分,它和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。伴随着类的产生而产生,也随着类的消失而消失。
    Extension一般用来放置类的私有变量,必须有一个类的源码才能添加一个类的Extension

  • Category 分类
    是运行期决议的,类扩展可以添加实例变量,分类不能添加实例变量

推荐文章

深入理解Objective-C:Category
objc category的秘密
Objective-C封装技巧(一):Category和继承的博弈
YYCategories
iOS-Category

二. 协议 Protocol

学过Java的同学都知道接口这个概念,在Objective C 中对应的被称为协议也就是Protocol,Protocol只能定义公用的一套接口,但不能提供具体的实现方法。也就是说,它只告诉你要做什么,但具体怎么做,它不关心。

协议的作用很简单就是对某个或者某个系列的对象的行为起约束作用,就像给一个模版,后续只有满足这个模版的对象才能传入。

协议可用定义在单独.h文件中,也可用定义在某个类中:
  • 如果这个协议只用在某个类中,应该把协议定义在该类中
  • 如果这个协议用在很多类中,就应该定义在单独文件中
协议中方法声明的关键字
@required (默认) 要求实现,如果没有实现,会发出警告,但不报错
@optional 不要求实现,不实现也不会有警告
导入协议
@protocol 协议名称    告诉下面代码这是一个协议,但不知道协议里面有什么东西
#import “协议文件名.h” 需要知道协议里面的具体方法的时候需要通过这种方式导入
遵循协议
类遵守协议(这里可以写在外部接口,也可以写在内部扩展部分)
@interface 类名 : 父类名 <协议名称1, 协议名称2>

@end

@interface 类名()<协议名称1, 协议名称2>

@end

协议遵守协议

@protocol 协议名称 <其他协议名称1, 其他协议名称2>

@end
使用协议约束属性或者变量
@property中声明的属性也可用做一个遵守协议的限制
@property (nonatomic, strong) 类名<协议名称> *属性名;
@property (nonatomic, strong) Dog<MyProtocol> *dog;
@property (nonatomic, strong) id<协议名称> 属性名;
@property (nonatomic, strong) id<MyProtocol> dog2;
协议可以声明属性,但是不能添加实例变量

和分类一致的是协议也可以声明属性,但是一般很少这样做,一般协议中都是用于声明方法的,下面是协议声明属性的用法:

@protocol IDLTestProtocol <NSObject>

@property(nonatomic, strong, readwrite) NSString *test;

@end


@interface TestObject : NSObject<IDLTestProtocol>

@end


@implementation TestObject

@synthesize test/* 这个是供类外部使用的名称*/ = _helloWorld/* 这个是供类内部使用的名称*/;

- (instancetype)init {
self = [super init];
if (self) {
_helloWorld = @"linxiaohai";
}
return self;
}

@end

测试代码

TestObject *testObject = [[TestObject alloc] init];
IDLLogError(@"========>%@",testObject.test);

原本打算将AutoreleasePool和内存管理,Runloop,线程,事件分发放在一块介绍的,因为这些都有着密切的联系,但是考虑到这样的话文章会很长,看得都累,所以还是分开了。

1. AutoReleasePool 是什么

看过iOS内存管理的都知道,iOS内存管理使用的引用计数会在不使用某个对象的时候调用release 方法,将某个对象的引用计数减1,当某个对象当引用计数减到0的时候对象被释放,dealloc被调用。但是某些情况需要延迟释放,最常见的例子比如当你在一个方法中返回一个对象时就需要延迟释放,AutoreleasePool是一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行。具体这些对象什么时候释放,在后续的介绍中将会介绍。这部分只要知道AutoReleasePool 是什么就可以了:

Autorelase Pool 提供了一种可以允许你向一个对象延迟发送release消息的机制。当你想放弃一个对象的所有权,同时又不希望这个对象立即被释放掉(例如在一个方法中返回一个对象时),Autorelease Pool 的作用就显现出来了。
2.从宏观看AutoReleasePool位于哪个位置

终于可以上图了:

在一个应用中有两种释放池局部释放池和RunLoop释放池,上图中展示的是RunLoop释放池,局部释放池比较简单,就是在autoreleasepool花括号结束的时候释放释放池里面的对象的,所以这里最关键的就是RunLoop释放池

@autoreleasepool {

}

RunLoop释放池会在进入主线程Runloop的时候新建一个Autoreleasepool,在从睡眠被唤醒的时候,通过pop后再push清空并重建一个Autoreleasepool,在退出RunLoop的时候pop掉Autoreleasepool

3. AutoReleasePool的内部结构与机制

首先AutoReleasePool是由多个AutoreleasePoolPage构成的双向链表。
每个AutoreleasePoolPage大小为4096字节,被分成两大部分:
AutoreleasePoolPage描述部分,它是由如下字段构成,大小为56字节:

magic 用来校验 AutoreleasePoolPage 的结构是否完整;
next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin();
thread 指向当前线程;
parent 指向父结点,第一个结点的 parent 值为 nil;
child 指向子结点,最后一个结点的 child 值为 nil;
depth hiwat 不是我们的重点这里不介绍

剩余的用来存储加入到自动释放池的对象,它们其实只是指向这些对象的指针,对象还是存储在堆上。
当前正在使用的AutoreleasePoolPage被称为hotPage.

当我们在代码中使用

@autoreleasepool {

}

的时候会转化为:

void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
3.1 objc_autoreleasePoolPush

objc_autoreleasePoolPush 内部很简单就是调用AutoreleasePoolPage类的push方法。

void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}

AutoreleasePoolPage::push会往当前使用的hotPage中添加一个POOL_BOUNDARY 自动释放池边界标记,这个标记作为返回值返回,作为pop的参数。

static inline void *push() {
return autoreleaseFast(POOL_BOUNDARY);
}

之后的完自动释放池添加对象最终也是调用autoreleaseFast。

static inline id *autoreleaseFast(id obj) {
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}

它分三种情况:

  • hotPage 不存在:
    比如刚开始的时候是没有hotPage的,它会创建一个然后调用setHotPage将新建的page设置为hotpage,如果没有POOL_BOUNDARY,先往AutoreleasePoolPage添加一个POOL_BOUNDARY,然后再将带加入释放池对象的指针添加到AutoreleasePoolPage。
static id *autoreleaseNoPage(id obj) {
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);

if (obj != POOL_BOUNDARY) {
page->add(POOL_BOUNDARY);
}

return page->add(obj);
}

  • hotPage 存在并且还没满

调用 page->add(obj) 方法将对象加入当前hotPage中,也就是将对象添加到next指向的位置,然后将next指向下一个空的位置。

id *add(id obj) {
id *ret = next;
*next = obj;
next++;
return ret;
}
  • hotPage 满了

调用 autoreleaseFullPage(obj, page) 方法,该方法会先查找 hotPage 的 child,如果有则将 child page 设置为 hotPage,如果没有则将创建一个新的 hotPage,之后在这个新的 hotPage 上执行 page->add(obj) 操作

static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());

setHotPage(page);
return page->add(obj);
}

3.2 objc_autoreleasePoolPop

objc_autoreleasePoolPop

objc_autoreleasePoolPop 里面只是简单调用AutoreleasePoolPage::pop方法,传入的参数一般是一个POOL_BOUNDARY,

void objc_autoreleasePoolPop(void *ctxt){
AutoreleasePoolPage::pop(ctxt);
}

AutoreleasePoolPage::pop 首先会找到POOL_BOUNDARY所在的页面的地址,然后调用releaseUntil,releaseUntil会针对每页的每一项调用memset重置,然后调用objc_release将对应的对象release。最后将当前页面以及子页面全部删除

static inline void pop(void *token) 
{
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;

page->releaseUntil(stop);

if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}
3.3 autorelease

当某个对象调用autorelease的时候,最终会调用AutoreleasePoolPage::autorelease

static inline id autorelease(id obj) {
id *dest __unused = autoreleaseFast(obj);
return obj;
}

最终还是调用autoreleaseFast

3.4 AutoReleasePool drain 和 release区别

当我们向自动释放池pool发送 release 消息时,它会向池中的每一个发送了 autorelease 消息的对象发送一条 release 消息,并且自身也会销毁。当向它发送 drain 消息时,只会释放里面的对象,而不会销毁自己。

3.5 什么情况下会将对象放到自动释放池:
  1. 对象作为方法返回值时候
  2. 通过类方法创建对象的时候
  3. 使用如下的便捷语法来建立对象的时候
NSArray *array = @[@"abc",@"def"];
NSNumber *number = @123;

1. 内存区域分布

堆操作:
操作系统中有一个存放堆内空闲存储块地址和大小的链表,当程序员申请空间的时候,系统就会遍历整个链表,找到第一个比申请空间大的空闲块节点,系统会将该空闲块从空闲链表中删除,分配给程序,由于申请的空间不一定与找到的空闲块大小相同,多出来剩余的空闲区会被系统重新添加到空闲链表中。当我需要删除对象时,便会根据指针纪录的地址,将这一块区域重新加入到链表中

栈操作:
栈区的内存是系统自动申请的而且是有序的。我们在申请栈空间时就只能在栈的顶部进行申请,当程序执行某个方法(或者函数)时,会从内存中栈(stack)的区域分配出一块内存空间,这个内存空间被称之为帧(frame)用来储存程序在这个方法内声明的变量的值。当应用启动并运行 main 函数时,它的帧会被存在栈的底部。当 main 继续调用另外一个方法时,这个方法的帧又会继续被压入栈的顶部。被调用的方法还可以再调用其他方法,以此类推,会有帧继续被压入栈顶,在被调用的方法结束后,程序会将其帧从栈顶释放。

2. iOS 引用计数内存管理策略

引用计数是一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。从而实现资源自动管理的目的。它的做法是:当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,对象的引用计数为0时对象的内存会被立刻释放。

* 当程序调用方法名以alloc、new、copy、mutableCopy开头的方法来创建对象时,该对象的引用计数加1,这种情况我们将拥有所创建的这个对象。
* 当有一个新的指针指向这个对象时(或者调用retain方法时),我们将其引用计数加 1,接收到此调用的对象通常保证在他接收到retain所在的方法中保持有效。
* 除了以alloc、new、copy、mutableCopy 开头的方式创建对象外,其他方式创建的对象都是会被添加到AutoReleasePool,该对象的引用计数不会+1,这种情况下我们不用负责释放对象。
* 当某个指针不再指向这个对象时(或者调用release方法时),我们将其引用计数减 1
* 当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。这时候会调用该对象的dealloc方法。

* 对于数组类型其引用计数是会自动的相应变化的:
1. 当一个对象被添加进数组时 ,对象的引用计数也会相应的增加。
2. 数组移除指定的对象或者时所有对象,其被移除的对象会 release
3. 当数组销毁时,所有对象均会 release。

3. iOS开发中的内存管理四个黄金法则
* 自己生成的对象,自己持有
* 非自己生成的对象,自己也能持有
* 不再需要自己持有的对象的时候,释放
* 非自己持有的对象无法释放
4. 有关引用计数的方法:
* —retain:将该对象的引用计数器加1,从而持有该对象,但是并不拥有对象的释放权利。
* —release:将该对象的引用计数器减1,注意只有在计数为0的时候才会释放,而不是说一旦release就释放。
* —autorelease:调用 autorelease 后,对象不会被立即释放,而是注册到 autoreleasepool 中,经过一段时间后 pool结束,此时调用release方法,引用计数减1。
* —retainCount:返回该对象的引用计数的值。
* dealloc: 当一个对象一个拥有者都没有的话,dealloc就会被自动调用,dealloc方法的角色是释放对象自己的内存,并且销毁他所拥有的资源,包括所有对象变量的拥有权。

5. iOS中的变量标识符 & 属性标识符

变量标识符

__strong                持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放,从另一个角度讲只要还有一个强指针指向某个对象,这个对象就会一直存活
__weak 弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生,如果对象没有被其他对象强引用,弱引用会被置为 nil,弱引用的实现原理是这样:
系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,
继而把它们都置成 nil
__unsafe_unretained 它和__weak有点类似,只不过在没有被其他对象强引用的时候它不会被置为 nil。如果它引用的对象被回收掉了,该指针就变成了野指针。
__unsafe_unretained修饰符的变量不属于编译器的内存管理对象,赋值时即不获得强引用也不获得弱引用。

__autoreleasing 替代autorelease方法


属性标识符

@property (assign/retain/strong/weak/unsafe_unretained/copy) PropertyType* propertyType

* assign 表明 setter 仅仅是一个简单的赋值操作,没有持有不持有这一说,通常用于基本的数值类型
* strong 表明属性定义一个持有者关系。当给属性设定一个新值的时候,首先对旧值进行 release ,对新值进行retain 然后进行赋值操作。
* weak 表明属性定义了一个持有者关系。当给属性设定一个新值的时候,这个值不会进行 retain,旧值也不会进行 release, 而是进行类似 assign 的操作。
不过当属性指向的对象被销毁时,该属性会被置为nil
* unsafe_unretained 的语义和 assign 类似,不过是用于对象类型的,表示一个非拥有(unretained)的,同时也不会在对象被销毁时置为nil的(unsafe)关系。
* copy 类似于 strong,不过在赋值时进行 copy 操作而不是 retain 操作。通常在需要保留某个不可变对象,并且防止它被意外改变时使用。

概括得讲:
strong 和 copy都会持有对象,一个是持有对象的本身,一个是持有对象的副本。
weak,unsafe_unretained 更像一个旁观者,它们不会对数据的引用计数起到任何的改变,它看着对象被持有,被销毁却无能为力,只不过weak会在对象被销毁的时候会将其置为nil。而unsafe_unretained不会,unsafe_unretained 在开发中用得比较少, 如果对性能有极高的要求方可以考虑使用 unsafe_unretained 替换 weak,因为weak 其实对性能还是有影响的,只不过少量使用的时候是不会察觉到的,但是在类似YYModel这种序列化,反序列化库如果大量使用weak,肯定会对性能有较大的影响,weak的最主要作用就是解决循环引用的问题。这个会在后面做介绍,其实这个已经在Block总结的时候已经介绍过了。

6. ARC规则

与Java 中 GC 不同,ARC 是编译器特性,而不是基于运行时的,ARC 背后的原理是依赖编译器的静态分析能力,通过在编译时找出合理的插入引用计数管理代码,而不是实时监控与回收内存。

需要注意的是ARC 所做的事情并不仅仅局限于在编译期找到合适的位置帮你插入合适的 release 等等这样的内存管理方法,其在运行时期也做了一些优化,比如:

  • 合并对称的引用计数操作。比如将 +1/-1/+1/-1 直接置为 0.
  • 巧妙地跳过某些情况下 autorelease 机制的调用。
    当返回值被返回之后,紧接着就需要被 retain 的时候,没有必要进行 autorelease + retain,直接什么都不要做就好了。

ARC 打开的情况下有如下限制:

* 不能使用retain/release/retainCount/autorelease
* 不能使用NSAllocateObject/NSDeallocateObject
* 须遵守内存管理的方法命名规则
* 不要显式调用dealloc
* 使用@autoreleasepool块替代NSAutoreleasePool
* 不能使用NSZone
* 对象型变量不能作为C语言结构体(structunion)的成员: 要把对象类型添加到结构体成员中,可以强制转换为void *或是附加__unsafe_unretained修饰符。

7. 内存相关常见问题

内存问题有两种:

  • 释放得太早,还在使用中就释放:
如果某个对象有至少一个拥有者,那么就必须保留不能释放,否则的话其他对象或者方法仍然有指向这个对象的指针沦为野指针(空指针)。这称之为过早释放,这是十分危险的,因为当野指针指向的内存区域再次被某个新的对象使用时,野指针上的操作便会破坏这个新对象造成文件丢失或者崩溃。
  • 释放得太晚,已经不用了但是还没释放:
如果某个对象失去了拥有者(变成没有拥有者)那么应该将其释放掉,否则没有拥有者的对象会被孤立而程序找不到,并且始终占用着一块内存,导致内存泄漏
7.1 内存泄漏

ARC内存泄露常见场景:

  • 对象型变量作为C语言结构体,或者联合体(struct、union)的成员
struct Data {
NSMutableArray __unsafe_unretained *array;
};

__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便可能遭遇内存泄露或者程序崩溃。

  • 循环引用

循环引用常见有如下几种情况:

  1. 两个对象互相持有对象,这个可以设置弱引用解决,最常见的是block,但是需要注意并非所有的block都需要使用weak来打破循环引用,如果self没有持有block就不会造成循环引用。而有些地方之所以使用了__weak,是为了在[self dealloc]之后就不再执行了。

解决方案 1:在block外部对弱化self,在block内部强化已经弱化的weakSelf

@interface Test: NSObject {
id __weak obj_;
}

- (void)setObject:(id __strong)obj;
block持有self对象,这个要在block块外面和里面设置弱引用和强引用。

__weak __typeof(self) wself = self;
obj.block = ^{
__strong __typeof(wself) sself = wself;
[sself updateSomeThing];
}

解决方法 2: 通过将对象在block中设置为nil,但是这种需要注意的是block一定要被执行

__block TestObject *object = [[TestObject alloc] init…];
object.completionHandler = ^(NSInteger result) {
[object testMethod];
object = nil;
};
  1. NSTimer的target持有self
self.timmer = [NSTimer scheduledTimerWithTimeInterval:1.0 
target:self
selector:@selector(updateTime:)
userInfo:nil
repeats:YES];

NSTimer会造成循环引用,timer会强引用target即self,一般self又会持有timer作为属性,这样就造成了循环引用。
如果timer只作为局部变量,不把timer作为属性同样释放不了,因为在加入runloop的操作中,timer被强引用。而timer作为局部变量,是无法执行invalidate的,所以在timer被invalidate之前,self也就不会被释放。

解决方案:在恰当时机调用[timer invalidate]即可,这个需要根据业务来自己决定,但是放在dealloc中调用是无效的,因为循环引用的情况下dealloc是不会被调用的,所以[timer invalidate]也就不会被调用。

还有下面几种定时相关的情形也需要注意:

__weak __typeof(self) wself = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[wself commentAnimation];
});

__weak __typeof(self) wself = self;
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
[wself commentAnimation];
});
dispatch_resume(timer);
  1. 代理delegate

代理在一般情况下,需要使用weak修饰,我们常见的delegate 一般会是VC的属性,被VC持有,同时我们会将VC相关的属性作为delegate从而导致循环引用。
解决方案:delegate属性使用weak修饰

  1. NSNotification

使用block的方式增加notification,引用了self,在删除notification之前,self不会被释放

解决方案:在block内部使用弱引用解决

  • 对象被单例持有

我们在单例里面设置一个对象的属性,因为单例是不会释放的,所以单例会有一直持有这个对象的引用。

[Instanse shared].obj = self;
  • CF类型内存

注意以creat,copy作为关键字的函数都是需要释放内存的.

8. 内存泄漏的排查方法
  • 静态分析方法(Analyze)
  • 动态分析方法(Instrument工具库里的Leaks,Allocations)
  • 在可疑对象的dealloc方法中添加log进行查看
  • 使用三方开源库:

MLeaksFinder
PLeakSniffer
FBRetainCycleDetector
FBAllocationTracker
FBMemoryProfiler
介绍FBRetainCycleDetector,FBAllocationTracker,FBMemoryProfiler的文章

9. AutoreleasePool 与 RunLoop的关系

主线程的AutoreleasePool会在RunLoop进入的时候重新建立一个,在RunLoop退出休眠状态的时候也会进行释放后重新建立一个。在退出RunLoop的时候释放AutoreleasePool,具体见RunLoop总结

10. weak-strong dance

在7.1 介绍内存泄漏类型时候提到循环引用的一种解决方案是在block外部对弱化self,在block内部强化已经弱化的weakSelf,这也就是这里所说的 weak-strong dance,block外部对弱化self是为了避免循环引用,而在block内部强化已经弱化的weakSelf是为了避免外部_weak导致在运行block的时候self被释放。

原谅我的懒 !!!

  1. 宏定义在 #define 那一行结束。如果你要书写多行,需要使用 \ 换行

  2. 只有在定义宏之后,宏才能生效

  3. 类函数宏 #define lang_init() c_init()

  4. 宏参数 #define min(A, B) ((A) < (B) ? (A) : (B))

  5. 如果宏的内容中有字符串,那么不会被宏参数替换:#define foo(x) x, “x” ===> foo(bar) → bar, “x”

  6. 一定要为宏参数添加括号

  7. 若宏的结果为值,则为整个宏添加括号

  8. 若宏替换的是代码块,则要为这段代码添加 do{…}while(0)

  9. 可以在宏参数前添加 #,将参数转换为字符串,字符串化会将参数中的所有字符(包括引号)都字符串化,如果中间有很多空格,字符串化后将只有一个空格。如果想通过 # 来实现参数值的字符串化,就需要需要使用两层的宏:

    #define IDL_NS_STR(x) IDL_INNER_NS_STR(x)
    #define IDL_INNER_NS_STR(x) @#x
    #define FOO 4

    IDL_INNER_NS_STR(FOO)
    "FOO"
    IDL_NS_STR(FOO)
    "4"

    在使用 IDL_INNER_NS_STR 的时候,x 会立即被字符串化,而没有被宏展开。但是如果我们使用另外一个宏 IDL_NS_STR 嵌套着,那就会先展开,将值带入后,然后再字符串化。

  10. 使用”##”操作符可以实现宏中标记的连接。预处理器会将所有注释转为空格,## 会将左右的空格都忽略。

  11. 如果不确定有多少个宏参数,可以使用 … 代替,这在很多语言中都有类似的做法。相应的,使用 VA_ARGS 在具体的宏中,代替 …

    #define IDL_PRINTF(…) fprintf (stderr, ##__VA_ARGS__)

    在加上 ## 之后,预处理器就会在传入空的时候,删掉前面的 , 了

  12. C语言中预定义宏:

__FILE__ :当前源代码的文件名(字符串)
__LINE__:当前源代码中的行号(整型)
__DATE__:进行预处理的日期(”Mmm dd yyyy”形式的字符串)
__TIME__:源文件编译时间(格式“hh:mm:ss”)
__FUNCTION__:同__func__(但IDE不支持),当前源代码的函数名
__PRETTY_FUNCTION__:同__FUNCITON__,但在g++下会输类名、函数名及其他函数信息

该文档的目的在于规范整个项目的代码,使得代码更具可读性,在草拟该文档的时候常照了较多较好的代码规范文档,这些文档将会罗列在该文档参考文献部分。大家如果在使用该文档过程中发现有任何的遗漏或者有更好的规范或者异议都欢迎联系本人(文档末尾有本人的联系邮箱)。

为什么需要有代码规范

一般一个项目代码通常是由一个团队共同开发维护的产物,随着业务的不断扩展,功能的快速迭代,代码会变得越来越庞大,如果这个时候不同的开发者的code风格千差万别,那么阅读代码将会是一件十分痛苦的事情。代码规范除了一些必须遵守的规则外,大多是没有对错而言,它只是让整个项目的代码看起开风格更加统一,我们知道人对熟悉的东西接受起来会快,对于熟悉的代码风格同样也是这个道理,这就是为每个项目拟定代码规范的意义所在.

该规范整体分成三类:

  • [命名规范]
  • [编码规范]

所有规范分成两个等级

  • [必须]
  • [建议]

目录

  1. 命名规范

    • 通用命名规范
    • 文件命名规范
    • 类命名规范
    • 方法命名规范
    • Getter/Setter命名规范
    • 属性,参数命名规范
    • Delegate 命名规范
    • Protocol命名规范
    • Catogries命名规范
    • 常量命名规范
    • Exception命名规范
    • Notification命名规范
  2. 编码规范

    • Initialize 规范
    • dealloc 规范
    • Block规范
    • Notification规范
    • Collection规范
    • 控制语句规范
    • 对象判等规范
    • 懒加载规范
    • 内存管理规范
  3. 项目设计规范

    • 源码注释规范
    • 文件导入规范
    • 代码布局规范
    • interface接口文件布局规范
    • 类设计规范

命名规范

通用命名规范
  • [必须] 命名必须具备见名知意的效果,禁止中文拼音,过度缩写,以及一切无意义的命名。
  • [必须] 除了通知和掩码常量外命名禁止自我指涉(在变量的末尾增加类型后缀)
  • [必须] 参数名、成员变量、局部变量、属性名都要采用小写字母开头的驼峰命名方式。如果方法名以一个众所周知的大写缩略词开始,可以不适用驼峰命名方式。比如FTP、WWW URL等。
  • [建议] 不同文件中或者不同类中具有相同功能或相似功能的属性的命名应该是相同的或者相似的。比如:count同时定义在NSDictionary、NSArray、NSSet这三个集合类中。且这三个集合类中的count属性都代表同一个意思,即集合中对象的个数。
  • [建议] 一般情况下,不要缩写或省略单词,建议拼写出来,即使它有点长。
文件命名规范

我们在刚拿到代码的时候首先会先看项目的目录结构,其次就是文件的组织,而了解文件的组织就是从文件名开始,所以文件命名也是一个非常重要的工作。

  • [必须] 分类文件必须使用分类所依附的主类名 + 分类名称的形式 比如:UIImage+NMAddition.h
  • [必须] 一般建议一个文件中只定义一个类,但是如果定义多个的时候,使用最主要的那个类的名称作为文件名。
  • [必须] 文件后缀选择:
    Extension    Type
    .h C/C++/Objective-C header file
    .m Objective-C implementation file
    .mm Objective-C++ implementation file
    .cc Pure C++ implementation file
    .c C implementation file
类命名规范
  • [必须] 类的名称应该由两部分组成,前缀+名称,前缀用大写字符,名称用大写开头的驼峰规则命名。
  • [建议] 前缀一般使用项目产品名的缩写,之所以不使用公司组织的缩写是因为一个公司有多个产品,使用公司名来作为前缀有可能导致重复。对于前缀一般使用多于两位的大写字符,因为苹果默认保留了两位字符的缩写前缀,但是这不是必须的。
方法命名规范
  • [必须] 方法名必须使用小写开头的驼峰命名方式,如果方法名以一个中所周知的大写缩略词开头,该规则可以忽略。
  • [必须] 一般类方法名不需要使用前缀,因为它们存在于特定类的命名空间中,私有方法可以使用统一的前缀来分组和辨识
  • [必须] 禁止在方法前面加下划线“ _ ”。Apple官网团队经常在方法前面加下划线”_”。为了避免方法覆盖,导致不可预知的意外,禁止在方法前面加下划线。
  • [必须] 如果一个方法代表某个名词执行的动作,则该方法应该以一个动词开头。但不要使用“do”或者”does”作为方法名称的一部分,因为这些助动词不能为方法名称增加太多的意义,反而让方法看起来更加臃肿。同时,也请不要在动词前面使用副词或者形容词。
  • [建议] 方法实现时,如果参数过长,则令每个参数占用一行,以冒号对齐,在分行时,如果第一段名称过短,后续名称可以以Tab的长度(4个空格)为单位进行缩进,
    -(id)initWithModel:(IPCModle)model
    ConnectType:(IPCConnectType)connectType
    Resolution:(IPCResolution)resolution
    AuthName:(NSString *)authName
    Password:(NSString *)password
    MAC:(NSString *)mac
    AzIp:(NSString *)az_ip
    AzDns:(NSString *)az_dns
    Token:(NSString *)token
    Email:(NSString *)email
    Delegate:(id<IPCConnectHandlerDelegate>)delegate;



    - (void)short:(GTMFoo *)theFoo
    longKeyword:(NSRect)theRect
    evenLongerKeyword:(float)theInterval
    error:(NSError **)theError {
    ...
    }
  • [建议] 如果一个方法调用语句太长,需要对参数进行冒号对齐,如果只有一个冒号,但是还是太长了,可以换行,并且第二行与第一行的第二个字符对齐。
  • [必须] 只有在访问某个属性的时候使用”点访问,其他的使用空格调用。
  • [建议] 对输入参数的正确性和有效性进行检查,参数错误立即返回
  • [建议] 对于有返回值的方法,每一个分支都必须有返回值。
  • [必须] 禁止直接调用NSObject的类方法+new,也不要在子类中重载它。使用alloc和init方法
    self.productsRequest = [[SKProductsRequest alloc]   
    initWithProductIdentifiers:productIdentifiers];
  • [必须] 在方法定义的时候需要在-/+符号后面添加一个空格
    - (void)invokeWithTarget:(id)target;
    - (void)selectTabViewItem:(NSTabViewItem *)tabViewItem
  • [必须] 如果某个方法返回一个对象,那么名字应该使用返回对象的名字来命名
  • [必须] 如果方法返回接收者的某个属性,那么请直接以属性名作为方法名。如果方法间接的返回一个或多个值,我们可以使用“getxxx”的方式来命名方法。相反,无需额外的在方法名前面添加”get”。
    - (NSSize)cellSize;     OK
    - (NSSize)calcCellSize; 不OK
    - (NSSize)getCellSize; 不OK
  • [必须] 所有参数前面都应该添加关键字
  • [必须] 尽量使用”with”, “from”, and “to”,进行连接,请不要使用“and”连接接收者属性,但是如果方法描述了两个独立的动作,可以考虑使用“and”连接起来。
  • [必须] 方法定义keyword 和参数之间不能有空格
    可以写成这样
    - (void)setExample:(NSString *)text;
    不能写成这样
    - (void)setExample: (NSString *)text;
    - (void)setExample:(NSString *) text;
Getter/Setter命名规范
  • [建议] 如果属性是名词,推荐格式如下:
    - (type)noun;
    - (void)setNoun:(type)aNoun;
    例如:
    - (NSString *)title;
    - (void)setTitle:(NSString *)aTitle;
  • [建议] 如果某个属性或者变量的名称是一个形容词,可以省略is前缀,并在属性定义的时候使用getter来指定getter方法的名称。
    @property (assign, getter=isEditable) BOOL editable;
  • [建议] 如果属性是一个动词,动词使用一般现在时。推荐格式如下:
    - (BOOL)verbObject;
    - (void)setVerbObject:(BOOL)flag;
    例如:
    - (BOOL)showsAlpha;
    - (void)setShowsAlpha:(BOOL)flag;
  • [必须] 不要把动词的过去分词形式当做形容词来使用。
  • [建议] 可以使用情态动词(can、should、will等)明确方法意义,但不要使用do、does这类无意义的情态动词。
  • [建议] 只有方法间接的返回一个数值,或者需要多个数值需要被返回的时候,才有必要在方法名称中使用“get”。像这种接收多个参数的方法应该能够传入nil,因为调用者未必对每个参数都感兴趣
    - (void)getLineDash:(float *)pattern count:(int *)count phase:(float *)phase;
属性,参数命名规范
  • [建议] 每个属性命名都加上类型后缀,也就是说变量的名称必须同时包含功能与类型,如,按钮就加上Button后缀,模型就加上Model后缀
  • [建议] 属性的关键字推荐按照 原子性,读写,内存管理的顺序排列。
    @property (nonatomic, readwrite, copy) NSString *name;
    @property (nonatomic, readonly, copy) NSString *gender;
    @property (nonatomic, readwrite, strong) UIView *headerView;
  • [必须] Block,NSString属性应该使用copy关键字
  • [必须] delegate 应该使用weak关键字
  • [必须] 禁止使用synthesize关键词
  • [建议] 如果是静态常量,仅限本类内使用的,加上前缀s_,如果是整个工程共用,以sg_为前缀。如:
    s_kMaxHeight; 
    sg_kMaxHeight;
  • [建议] 对于本地变量,在最靠京它们使用的地方声明,并且在它们声明的同时对其进行初始化
  • [建议] 由于32位和64位版本的大小不同,请避免使用long类型,NSInteger,NSUInteger和CGFloat,除非匹配系统接口。类型long,NSInteger,NSUInteger和CGFloat在32位和64位版本之间的大小不同。在处理由系统接口公开的值时,使用这些类型是合适的,但对于大多数其他计算应该避免使用它们。
  • [必须] 在使用整形变量存储ID之类的值时,需要使用uint32 uint64这一类,长度不会随着平台版本不同而不同的变量类型。
  • [必须] 在遇到遵循某个协议的对象定义的时候,不要在id和协议之间加空格
  • [必须] 在定义NSArray和NSDictionary时使用泛型,可以保证程序的安全性:
  • [必须] 当使用属性,对象实例变量,应该使用self.形式访问,这样可以确保调用的是具备Setter/Getter方法修饰过的属性值。但是在init和dealloc内建议使用类似_variableName形式的访问方式避免Setter/Getter 带来的副作用,更不要在Setter/Getter 方法中使用self.这样会导致死循环。
  • [必须] 在定义属性或者变量的时候指针应该归变量,也就是说建议使用:
    NSString *text

    而不是

    NSString* text
    NSString * text
  • [建议] 私有属性强烈建议写在类的空扩展中
    @interface IDLCodeStylePrivateDemo ()

    @property (nonatomic, strong, readwrite) NSString *privateProperty;

    @end
  • [必须] 对于对外只读属性仅在.h接口文件中定义并指定其读写属性为readonly,
    @interface IDLCodeStylePrivateDemo

    @property (nonatomic, strong, readonly) NSString *readonlyProperty;

    @end
  • [必须] 如果在实现内部需要修改该属性需要在空扩展内部添加读写属性为readwrite的同名属性:
    @interface IDLCodeStylePrivateDemo ()

    @property (nonatomic, strong, readwrite) NSString *readonlyProperty;

    @end
  • [必须] 当变量释放后,需要将变量置为nil,避免因为野指针引起的程序崩溃。
  • [必须] 变量在使用前应初始化,防止未初始化的变量被引用。
Delegate 方法命名规范
  • [建议] 用delegate做后缀,如 当你的委托的方法过多, 可以拆分数据部分和其他逻辑部分, 数据部分用dataSource做后缀. 如
  • [建议] 名称以标示发送消息的对象的类名开头,省略类名的前缀并⼩小写第⼀个字⺟
    - (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
    - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename;
  • [建议] 除非delegate方法只有一个参数,即触发delegate方法调用的delegating对象,否则冒号是紧跟在类名后面的。
    - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender;
  • [建议] ⽤于通知委托对象操作即将发生或已经发⽣的方法名中要使⽤did或will
  • [建议] 用于询问委托对象可否执行某操作的⽅法名中可使⽤did或will,但最好使⽤should
    - (BOOL)windowShouldClose:(id)sender;
Protocol命名规范
  • [建议] 有时候protocol只是声明了一堆相关方法,并不关联class。这种不关联class的protocol使用ing形式以和class区分开来。比如NSLocking而非NSLock。
  • [建议] 如果proctocol不仅声明了一堆相关方法,还关联了某个class。这种关联class的protocol的命名取决于关联的class,然后再后面再加上protocol或delegate用于显示的声明这是一份协议。
  • [必须] 用optional修饰可以不实现的方法,用required修饰必须实现的方法
Catogries命名规范
  • [必须] category中不要声明属性和成员变量。
  • [必须] 避免category中的方法覆盖系统方法。可以使用前缀来区分系统方法和category方法。但前缀不要仅仅使用下划线”_“。
  • [必须] 如果一个类比较复杂,建议使用category的方式组织代码。具体可以参考UIView。
常量命名规范
  • [必须] 在代码中的常量必须抽成宏或者静态常量,避免使用硬编码内容
枚举常量
  • [必须] 使用枚举类型来表示一组相关的整型常量。
    typedef NS_ENUM(NSUInteger, VPLeftMenuTopItemType) {
    VPLeftMenuTopItemTypeMain = 0,
    VPLeftMenuTopItemTypeShows,
    VPLeftMenuTopItemTypeSchedule,
    VPLeftMenuTopItemTypeWatchLive,
    VPLeftMenuTopItemTypeMax,
    };



    typedef NS_ENUM(NSInteger, RBKGlobalConstants) {
    RBKPinSizeMin = 1,
    RBKPinSizeMax = 5,
    RBKPinCountMin = 100,
    RBKPinCountMax = 500,
    };
const常量
  • [必须] 使用const关键字创建浮点型常量。如果一个整型常量和其他常量不相关,可以使用const来创建,否则,使用枚举类型表示一组相关的整型常量。
    static const int kFileCount = 12;
    static NSString *const kUserKey = @"kUserKey";
  • [必须] 通常情况下,不要使用#define预处理命令创建常量。
  • [必须] 对于局限于某编译单元(实现文件)的常量,以字符k开头,例如kAnimationDuration,且需要以static const修饰
    推荐这样写:
    static const NSTimeInterval kFadeOutAnimationDuration = 0.4;
    不推荐这样写:
    static const NSTimeInterval fadeOutTime = 0.4;
  • [必须] 对于定义于类头文件的常量,外部可见,则以定义该常量所在类的类名开头,例如EOCViewClassAnimationDuration, 仿照苹果风格,在头文件中进行extern声明,在实现文件中定义其值
    例如 在头文件中声明如下定义
    extern const float EOCViewClassAnimationDuration;
    在实现文件中做如下声明
    const float EOCViewClassAnimationDuration = 18.0;
宏常量
  • [必须] #define 预处理定义的常量全部大写,单词间用 _ 分隔
  • [必须] 宏定义中如果包含表达式或变量,表达式或变量必须用小括号括起来。
Exception命名规范
  • [必须] 异常是用全局的NSString字符串进行标识。命名方式如下:
    [Prefix] + [异常模块] + [异常简要描述] + Exception
Notification命名规范
  • [必须] notification的命名使用全局的NSString字符串进行标识。命名方式如下:
    [Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification

    例如:
    NSApplicationDidBecomeActiveNotification
    NSWindowDidMiniaturizeNotification
    NSTextViewDidChangeSelectionNotification
    NSColorPanelColorDidChangeNotification
  • [必须] object通常是指发出notification的对象,如果在发送notification的同时要传递一些额外的信息,请使用userInfo,而不是object。
  • [必须] 如果某个通知是为了告知外界某个事件”即将”发生或者”已经”发生,则请在通知名称中使用“will”或者“did”这样的助动词。例
    如:
    UIKeyboardWillChangeFrameNotification;
    UIKeyboardDidChangeFrameNotification;

编码规范

Initialize 规范

initialize类方法先于其他的方法调用。且initialize方法给我们提供了一个让代码once、lazy执行的地方。initialize通常被用于设置class的版本号,initialize方法的调用遵循继承规则(所谓继承规则,简单来讲是指:子类方法中可以调用到父类的同名方法,即使没有调用[super xxx])。如果我们没有实现initialize方法,运行时初次调用这个类的时候,系统会沿着继承链(类继承体系),先后给继承链上游中的每个超类发送一条initialize消息,直到某个超类实现了initlialize方法,才会停止向上调用。因此,在运行时,某个类的initialize方法可能会被调用多次

  • [必须] 如果我们想要让initialize方法仅仅被调用一次,那么需要借助于GCD的dispatch_once()
    + (void)initialize {
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
    // the initializing code
    }
    }
  • [建议] 如果我们想在继承体系的某个指定的类的initialize方法中执行一些初始化代码,可以使用类型检查和而非dispatch_once()
    if (self == [NSFoo class]) {
    // the initializing code
    }
  • [必须] initialize是由系统自动调用的方法,我们不应该显示或手动调用initialize方法
Init 规范

Objective-C有designated Initializers和secondary Initializers的概念:
指定初始化方法(designated initializer)是提供所有的(最多的)参数的初始化方法,间接初始化方法(secondary initializer)有一个或部分参数的初始化方法。一个类可以有一个或者多个designated Initializers。但是要保证所有的其他secondary initializers都要调用designated Initializers。即:只有designated Initializers才会存储对象的信息。这样的好处是:当这个类底层的某些数据存储机制发生变化时(可能是一些property的变更),只需要修改这个designated Initializers内部的代码即可。无需改动其他secondary Initializers初始化方法的代码。

  • [必须] 所有secondary 初始化方法都应该调用designated 初始化方法。
  • [必须] 所有子类的designated初始化方法都要调用父类的designated初始化方法。使这种调用关系沿着类的继承体系形成一条链。
  • [必须] 如果子类的designated初始化方法与超类的designated初始化方法不同,则子类应该覆写超类的designated初始化方法。(因为开发者很有可能直接调用超类的某个designated方法来初始化一个子类对象,这样也是合情合理的,但使用超类的方法初始化子类,可能会导致子类在初始化时缺失一些必要信息)。
  • [必须] 如果超类的某个初始化方法不适用于子类,则子类应该覆写这个超类的方法,并在其中抛出异常。
  • [必须] 禁止子类的designated初始化方法调用父类的secondary初始化方法。否则容易陷入方法调用死循环。
    如果想在当前类自定义一个新的全能初始化方法,则需要如下几个步骤:
  • 定义新的指定初始化方法,并确保调用了直接父类的初始化方法。
  • 重载直接父类的初始化方法,在内部调用新定义的指定初始化方法。
  • 为新的指定初始化方法写文档。
    重载父类的初始化方法并在内部调用新定义的指定初始化方法的原因是你不能确定调用者调用的就一定是你定义的这个新的指定初始化方法,而不是原来从父类继承来的指定初始化方法。假设你没有重载父类的指定初始化方法,而调用者却恰恰调用了父类的初始化方法。那么调用者可能永远都调用不到你自己定义的新指定初始化方法了。而如果你成功定义了一个新的指定初始化方法并能保证调用者一定能调用它,你最好要在文档中明确写出哪一个才是你定义的新初始化方法。或者你也可以使用编译器指令__attribute__((objc_designated_initializer))来标记它。
    // 超类
    @interface ParentObject : NSObject

    @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
  • [必须] 禁止在init方法中使用self.xxx的方式访问属性。如果存在继承的情况下,很有可能导致崩溃。原因见文章为什么不能在init和dealloc函数中使用accessor方法
  • [必须] 校验父类designated初始化方法返回的对象是否为nil。如果初始化当前对象的时候发生了错误,应该给予对应的处理:释放对象,并返回nil。
dealloc 规范
  • [必须] 不要忘记在dealloc方法中移除通知和KVO。
  • [建议] dealloc 方法应该放在实现文件的最上面,在任何类中,init 都应该直接放在 dealloc 方法的下面,如果有多个初始化方法,应该将指定初始化方法放在最前面,其他初始化方法放在其后。
  • [必须] 在dealloc方法中,禁止将self作为参数传递出去,如果self被retain住,到下个runloop周期再释放,则会造成多次释放crash。如下:
    - (void)dealloc{
    [self unsafeMethod:self];
    //因为当前已经在self这个指针所指向的对象的销毁阶段,销毁self所指向的对象已经在所难免。如果在unsafeMethod:中把self放到了autorelease poll中,那么self会被retain住,计划下个runloop周期在进行销毁。但是dealloc运行结束后,self所指向的对象的内存空间就直接被回收了,但是self这个指针还没有销毁(即没有被置为nil),导致self变成了一个名副其实的野指针。
    // 到了下一个runloop周期,因为self所指向的对象已经被销毁,会因为非法访问而造成crash问题。
    }
  • [必须] 和init方法一样,禁止在dealloc方法中使用self.xxx的方式访问属性。如果存在继承的情况下,很有可能导致崩溃。
Block规范
  • [必须] 调用block时需要对block判空。
  • [必须] 注意block潜在的引用循环。
  • [建议] 较短的block可以写在一行内。
  • [建议] 如果分行显示的话,block的右括号}应该和调用block那行代码的第一个非空字符对齐。block内的代码采用4个空格的缩进。
    如果block过于庞大,应该单独声明成一个变量来使用。^和(之间,^和{之间都没有空格,参数列表的右括号)和{之间有一个空格。
    //较短的block写在一行内
    [operation setCompletionBlock:^{ [self onOperationDone]; }];

    //分行书写的block,内部使用4空格缩进
    [operation setCompletionBlock:^{
    [self.delegate newDataAvailable];
    }];

    //使用C语言API调用的block遵循同样的书写规则
    dispatch_async(_fileIOQueue, ^{
    NSString* path = [self sessionFilePath];
    if (path) {
    // ...
    }
    });

    //较长的block关键字可以缩进后在新行书写,注意block的右括号'}'和调用block那行代码的第一个非空字符对齐
    [[SessionService sharedService]
    loadWindowWithCompletionBlock:^(SessionWindow *window) {
    if (window) {
    [self windowDidLoad:window];
    } else {
    [self errorLoadingWindow];
    }
    }];

    //较长的block参数列表同样可以缩进后在新行书写
    [[SessionService sharedService]
    loadWindowWithCompletionBlock:
    ^(SessionWindow *window) {
    if (window) {
    [self windowDidLoad:window];
    } else {
    [self errorLoadingWindow];
    }
    }];

    //庞大的block应该单独定义成变量使用
    void (^largeBlock)(void) = ^{
    // ...
    };
    [_operationQueue addOperationWithBlock:largeBlock];

    //在一个调用中使用多个block,注意到他们不是像方法那样通过':'对齐的,而是同时进行了4个空格的缩进
    [myObject doSomethingWith:arg1
    firstBlock:^(Foo *a) {
    // ...
    }
    secondBlock:^(Bar *b) {
    // ...
    }];
Notification规范
  • [必须] 当我们使用通知时,必须要思考,有没有更好的办法来代替这个通知。禁止遇到问题就想到通知,把通知作为备选项而非首选项。
  • [必须] post通知时,object通常是指发出notification的对象,如果在发送notification的同时要传递一些额外的信息,请使用userInfo,而不是object。
  • [必须] 在多线程应用中,Notification在哪个线程中post,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中。如果post消息不在主线程,而接受消息的回调里做了UI操作,需要让其在主线程执行。
    说明:每个进程都会创建一个NotificationCenter,这个center通过NSNotificationCenter defaultCenter获取,当然也可以自己创建一个center。NoticiationCenter是以同步(非异步,当前线程,会等待,会阻塞)的方式发送请求。即,当post通知时,center会一直等待所有的observer都收到并且处理了通知才会返回到poster。如果需要异步发送通知,请使用notificationQueue,在一个多线程的应用中,通知会发送到所有的线程中。
    Collection规范
  • [必须] 不要用一个可能为nil的对象初始化集合对象,否则可能会导致crash。
  • [必须] 对插入到集合对象里面的对象也要进行判空。
  • [必须] 注意在多线程环境下访问可变集合对象的问题,必要时应该加锁保护。不可变集合(比如NSArray)类默认是线程安全的,而可变集合类(比如NSMutableArray)不是线程安全的。
  • [必须] 禁止在多线程环境下直接访问可变集合对象中的元素。应该先对其进行copy,然后访问不可变集合对象内的元素。
  • [必须] 注意使用enumerateObjectsUsingBlock遍历集合对象中的对象时,关键字return的作用域。block中的return代表的是使当前的block返回,而非使当前的整个函数体返回。
  • [必须] 禁止返回mutable对象,禁止mutable对象作为入参传递。
  • [必须] 在访问集合的时候需要做内存操作越界判断
  • [建议] 应该使用可读性更好的字面量来构造NSArray,NSDictionary等数据结构,避免使用冗长的alloc,init方法。
  • [建议] 如果构造代码写在一行,需要在括号两端留有一个空格,使得被构造的元素于与构造语法区分开来:
    //正确,在字面量的"[]"或者"{}"两端留有空格
    NSArray *array = @[ [foo description], @"Another String", [bar description] ];
    NSDictionary *dict = @{ NSForegroundColorAttributeName : [NSColor redColor] };

    //不正确,不留有空格降低了可读性
    NSArray* array = @[[foo description], [bar description]];
    NSDictionary* dict = @{NSForegroundColorAttributeName: [NSColor redColor]};
  • [建议] 如果使用NSMutableDictionary作为缓存,建议使用NSCache代替。
    NSCache优于NSDictionary的几点:
    当系统资源将要耗尽时,NSCache具备自动删减缓冲的功能。并且还会先删减“最久未使用”的对象。
    NSCache不拷贝键,而是保留键。因为并不是所有的键都遵从拷贝协议(字典的键是必须要支持拷贝协议的,有局限性)。
    NSCache是线程安全的:不编写加锁代码的前提下,多个线程可以同时访问NSCache
  • [建议] 集合类使用泛型来指定对象的类型。
  • [必须] 取下标的时候要判断是否越界。
  • [建议] 取第一个元素或最后一个元素的时候使用firtstObject和lastObject
  • [建议] 如果构造代码不写在一行内,构造元素需要使用 两个空格 来进行缩进,右括号]或者}写在新的一行,并且与调用字面量那行代码的第一个非空字符对齐,构造字典时,字典的Key和Value与中间的冒号:都要留有一个空格,多行书写时,也可以冒号对齐:
    NSArray *array = @[
    @"This",
    @"is",
    @"an",
    @"array"
    ];

    NSDictionary *dictionary = @{
    NSFontAttributeName : [NSFont fontWithName:@"Helvetica-Bold" size:12],
    NSForegroundColorAttributeName : fontColor
    };
控制语句规范
  • [建议] 相关的赋值语句等号对齐
    promotionsEntity.promotionImageStr   = activityItemDict[@"promotion_image"];
    promotionsEntity.promotionIdNum = activityItemDict[@"promotion_id"];
    promotionsEntity.promotionNameStr = activityItemDict[@"promotion_name"];
    promotionsEntity.promotionColorStr = activityItemDict[@"promotion_color"];
  • [建议] if条件判断语句后面必须要加大括号{}。不然随着业务的发展和代码迭代,极有可能引起逻辑问题。
  • [必须] 条件过多,过长的时候应该换行。条件表达式如果很长,则需要将他们提取出来赋给一个BOOL值,或者抽取出一个方法
  • [必须] 不要使用过多的分支,要善于使用return来提前返回错误的情况,把最正确的情况放到最后返回。
  • [建议] 对于条件语句的真假,因为 nil 解析为 NO,所以没有必要在条件中与它进行比较。永远不要直接和 YES 和 NO进行比较,因为 YES 被定义为 1,而 BOOL 可以多达 8 位。
  • [必须]使用switch…case…语句的时候,不要丢掉default:。除非switch枚举。
  • [必须] switch…case…语句的每个case都要添加break关键字,避免出现fall-through。
  • [必须] 不可在for循环内修改循环变量,防止for循环失去控制。
    for (int index = 0; index < 10; index++){
    ...
    logicToChange(index)
    }
  • [建议] 在使用?:的时候不要嵌套多重?: 这里顺便提一个?: 另外一个用得比较多的场景–为某个属性添加默认值:
    dotImage = [_delegate pageControl:self selectedImageForDotAtIndex:i] ?: _selectedDotImage;
  • [必须] 在访问CGRect 的 x,y,width,height 属性的时候不要直接访问,建议使用CGRectGetxxxx方法。
  • [必须] 如果某个方法的错误传递方式包括引用和返回两种方式的时候,建议使用返回的那个值:
    建议:
    NSError *error;
    if (![self trySomethingWithError:&error]) {
    // Handle Error
    }

    不建议:

    NSError *error;
    [self trySomethingWithError:&error];
    if (error) {
    // Handle Error
    }
对象判等规范
 - (BOOL)isEqual:(id)object {
  if (self == object) {
  return YES; //判断内存地址
  } 
    if (![object isKindOfClass:[ZOCPerson class]]) { 
        return NO; //是否为当前类或派生类
    }                                                                      
    return [self isEqualToPerson:(ZOCPerson *)object];
}
//自定义的判断相等性的方法
- (BOOL)isEqualToPerson:(Person *)person { 
if (!person) { 
return NO;
}
BOOL namesMatch = (!self.name && !person.name) || [self.name isEqualToString:person.name];
BOOL birthdaysMatch = (!self.birthday && !person.birthday)
|| [self.birthday isEqualToDate:person.birthday];
return haveEqualNames && haveEqualBirthdays; 
}

 

懒加载规范
懒加载适合的场景:
一个对象在整个app过程中,可能被使用,也可能不被使用。
一个对象的创建需要经过大量的计算或者比较消耗性能。除以上情形之外,请不要使用懒加载。
  • [建议] 懒加载本质上就是延迟初始化某个对象,所以,懒加载仅仅是初始化一个对象,然后对这个对象的属性赋值。懒加载中不应该有其他的不必要的逻辑性的代码,如果有,请把那些逻辑性代码放到合适的地方。
  • [必须] 不要滥用懒加载,只对那些真正需要懒加载的对象采用懒加载。
  • [必须] 如果一个对象在懒加载后,某些场景下又被设置为nil。我们很难保证这个懒加载不被再次触发。
    内存管理规范
  • [必须] 函数体提前return时,要注意是否有对象没有被释放掉(常见于CF对象),避免造成内存泄露。
  • [建议] 请慎重使用单例,避免产生不必要的常驻内存。
  • [建议] 除非你清除的知道自己在做什么。否则不建议将UIView类的对象加入到NSArray、NSDictionary、NSSet中。如有需要可以添加到NSMapTable 和 NSHashTable。因为NSArray、NSDictionary、NSSet会对加入的对象做strong引用(即使你把加入的对象进行了weak)。而NSMapTable、NSHashTable会对加入的对象做weak引用。说明:简单的说,NSHashTable相当于weak的NSMutableArray;NSMapTable相当于weak的NSMutableDictionary.
其他规范
  • [必须] performSelector:withObject:afterDelay:要在有Runloop的线程里调用,否则调用无法生效。

项目设计规范

文件布局
-.h
-- 文件注释
-- #import // 导入类
-- NS_ENUM // 枚举类
-- @protocol // 代理
-- @interface // 文件入口
-- @property // 属性
-- methods // 方法

-.m
-- 文件注释
-- #import // 导入类
-- #define // 宏定义
-- static // 静态变量
-- @interface // 文件入口
-- @property // 属性
-- @implementation // 实现
-- methods // 方法
源码注释规范

优秀的代码大部分是可以自描述的,我们完全可以用代码本身来表达它到底在干什么,而不需要注释的辅助。
但并不是说一定不能写注释,有以下三种情况比较适合写注释:

  • 公共接口(注释要告诉阅读代码的人,当前类能实现什么功能)。

  • 涉及到比较深层专业知识的代码(注释要体现出实现原理和思想)。

  • 容易产生歧义的代码(但是严格来说,容易让人产生歧义的代码是不允许存在的)。
    除了上述这三种情况,如果别人只能依靠注释才能读懂你的代码的时候,就要反思代码出现了什么问题。
    最后,对于注释的内容,相对于“做了什么”,更应该说明“为什么这么做”。

  • [建议] 注释符与注释内容之间要用一个空格进行分割。

较好的注释例子

#import <Foundation/Foundation.h>

@class Bar;

/**
* 接口,分类,和协议都必须添加块注释用于说明该模块的作用
*/
@interface Foo : NSObject

/** 属性定义注释 */
@property(nonatomic) Bar *bar;

/**
* 方法说明
* See -initWithBar: for details about @c bar.
*
* @param bar The string for fooing.
* @return An instance of Foo.
*/
+ (instancetype)fooWithBar:(Bar *)bar;

/**
* Initializes and returns a Foo object using the provided Bar instance.
*
* @param bar A string that represents a thing that does a thing.
*/
- (instancetype)initWithBar:(Bar *)bar NS_DESIGNATED_INITIALIZER;

/**
* Does some work with @c blah.
*
* @param blah
* @return YES if the work was completed; NO otherwise.
*/
- (BOOL)doWorkWithBlah:(NSString *)blah;

@end

** 方法的注释使用Xcode自带注释快捷键:Commond+option+/ **

文件导入规范
  • [建议] 在类的头文件中尽量少引用其他头文件,有时,类A需要将类B的实例变量作为它公共API的属性。这个时候,我们不应该引入类B的头文件,而应该使用向前声明(forward declaring)使用class关键字,并且在A的实现文件引用B的头文件。
  • [必须] 使用import 导入Objective-C 和 Objective-C++ 头文件,使用include 来导入C/C++ 头文件
  • [建议] 优先导入框架的头文件,再导入自己的头文件,最后导入三方的头文件。每个类别之间使用一个空行隔开,每个类别的头文件导入中使用//类别进行分类。
  • [必须] 务必保持头文件导入没有多余的内容,不需要的头文件导入,切记删除。
  • [建议] 共同的接口、结构体、常量和数据类型要定义在同一个头文件里
代码布局规范

项目中的代码需要根据每个方法的具体功能使用#pragma mark - 进行分类,#pragma mark - 上空两行,下空一行

#pragma mark - Life cycle

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

#pragma mark - Public methods

#pragma mark - Override methods

#pragma mark - Getters / Setters

#pragma mark - Others(Delegate/DataSource等)

#pragma mark - Private methods

  • [建议] 方法和方法之间建议使用一行空行隔开
  • [建议] 方法内部一类的代码之间不能有空行,一类代码之间需要添加空行来隔开,这样可以比较清晰
    - (void)awakeFromNib {
    UIStoryboard *signatureStoryboard = [UIStoryboard storyboardWithName:@"BBPopoverSignature" bundle:nil];
    self.signatureViewController = [signatureStoryboard instantiateViewControllerWithIdentifier:@"BBPopoverSignature"];
    self.signatureViewController.modalPresentationStyle = UIModalPresentationPopover;
    self.signatureViewController.preferredContentSize = CGSizeMake(BBPopoverSignatureWidth, BBPopoverSignatureHeight);
    self.signatureViewController.signatureImageView = self;

    UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(initiateSignatureCapture)];
    [self addGestureRecognizer:tapRecognizer];
    }
  • [必须] 代码块换行,建议使用
    if (user.isHappy) {
    //Do something
    } else {
    //Do something else
    }

    而不是

    if (user.isHappy)
    {
    //Do something
    }
    else {
    //Do something else
    }
  • [必须] 代码块冒号对齐,使用
    [UIView animateWithDuration:1.0 animations:^{
    // something
    } completion:^(BOOL finished) {
    // something
    }];

    而不是

    [UIView animateWithDuration:1.0
    animations:^{
    // something
    }
    completion:^(BOOL finished) {
    // something
    }];
  • [建议] 在类扩展和实现文件中保证一行的空行
    @interface MyClass ()

    // Properties - 在这里保证前后行有一个空行

    @end

    @implementation MyClass

    // Body - 在这里保证前后行有一个空行

    @end
    //这里也要留个空行
  • [建议] 在类声明中包含多个protocal,每个protocal占用一行并对齐。
    @interface CustomBackButtonViewController () <UITextFieldDelegate,
    MyProtocalDelegate,
    UITabBarControllerDelegate,
    UITabBarDelegate>
interface接口文件布局规范
  • [建议] interface接口的排列顺序建议如下:属性, 类方法, 初始化方法, 实例方法.
类设计规范
  • [建议] 尽量减少继承,类的继承关系不要超过3层。可以考虑使用category、protocol来代替继承。
  • [建议] 把一些稳定的、公共的变量或者方法抽取到父类中。子类尽量只维持父类所不具备的特性和功能。
  • [建议] .h文件中的属性尽量声明为只读。
  • [建议] .h文件中只暴露出一些必要的类、公开的方法、只读属性;私有类、私有方法和私有属性以及成员变量,尽量写在.m文件中。
  • [建议] 如果某个类的方法较多可以考虑把类的实现代码分散到便于管理的多个分类中

[参考文档]
The Objective-C Programming Language
Cocoa Fundamentals Guide
Coding Guidelines for Cocoa
iOS App Programming Guide
Raywenderlich Objective C Style Guide
Robots & Pencils
New York Times
Google
GitHub
Adium
Sam Soffes
CocoaDevCentral
Luke Redpath
Marcus Zarra
一份走心的iOS开发规范前言约定
Coding Guidelines for Cocoa

可能大家会比较奇怪明明讲的是Django为啥要介绍Docker,实不相瞒如果大家不知道Docker对Django的学习并无影响,但是个人之前很早就听说Docker了,借着这个机会也学习下,趁着有这个应用场景也事件下,其实个人对Docker也不是很熟悉,只是现学现卖。将整个过程贯通起来,个人接触新东西一般喜欢围绕着问题展开,遇到不明白的在网上找资料或者找书去了解。这篇博客也采用这种方式,下面就围绕着几个问题展开,这篇博客不会对Docker进行太深入了解,目标是够用就好。深入的知识在后续大家使用的时候遇到问题再在实践中解决,毕竟精力有限。

1. 容器化 vs 虚拟化

虚拟化是通过中间件将一台或者多台独立机器虚拟运行与物理硬件之上,用户并不能感知为他们服务的到底是哪台机器,事实上呈现在用户面前的就和使用一部机器是一样的感觉,只不过这部机器在物理范畴上可能不是单纯一台主机,可能有多台机器组成的一个集群。虚拟机是抽象硬件资源,每一个虚拟机实例占用指定数量的CPU、内存、硬盘等资源,这些资源每个虚拟机实例之间不会共享。

而什么是容器化呢?容器化从应用出发,将应用分割成多个容器,而这些容器直接运行在操作系统内核上的用户空间,容器技术可以让多个独立用户空间运行在同一台宿主机上。也就是说容器技术是抽象软件资源,它和Linux上运行的一个应用程序没有太大区别。

早期,大家都认为虚拟化方式可以最大程度上提供虚拟化管理的灵活性。但是随着时间推移,大家发现,虚拟化技术有个问题就是:每个虚拟机都需要运行一个完整的操作系统以及其中安装好的大量应用程序。但实际生产开发环境里,我们更关注的是自己部署的应用程序,如果每次部署发布我都得搞一个完整操作系统和附带的依赖环境,那么这让任务和性能变得很重和很低下。这时候,人们就在想,有没有其他什么方式能让人更加的关注应用程序本身,底层多余的操作系统和环境可以共享和复用?换句话来说,那就是我部署一个服务运行好后,我再想移植到另外一个地方,我可以不用再安装一套操作系统和依赖环境,这就是容器化提出的场景。

2. 容器化的特点

容器是自包含的,它打包了应用程序及其所有依赖,可以直接运行。
容器是可移植的,这就可以确保应用在开发环境、测试环境、生产环境等都有完全一样的运行环境。
容器是互相隔离的,同一主机上运行的多个容器,不会互相影响。
容器是轻量级的,体现在容器的秒级启动,并且占用资源很少。

3. 什么是Docker,Docker的组成及镜像结构

Docker是一个能够把开发应用程序自动部署到容器的开源引擎,使用Docker开发人员只需要关心容器中运行的应用程序,运维人员只需要关心如何管理容器。
它能保证写代码的开发环境与应用程序要部署的生产环境一致性。这对经常出现开发环境是好的,等到部署上去各种问题,又得各种联调的程序员来说无疑是巨大的福音。

Docker 目前大量用于:
持续集成和持续部署 (CI/CD), 加速应用管道自动化和应用部署,
以及结合微服务技术构建可伸缩扩展的服务框架,
服务器资源共享
创建隔离的运行环境
这些场景。

那Docker又是由那些组件组成的呢?
Docker 主要由

  1. Docker引擎:Docker引擎是由客户端服务器架构的程序,客户端通过docker命令行工具以及一套restful API向Docker服务器发出请求,Docker服务器或者称为守护进程完成所有工作并返回。Docker服务器和客户端可以在同一台宿主机器上运行,也可以从本地的Docker客户端连接到另一台宿主机上远程Docker服务器。
  2. Docker镜像:用户基于镜像运行自己的容器,可以把镜像当作容器的源代码,或者相当于我们安装系统的光盘,写Dockerfile就相当于刻录系统光盘。
  3. Docker容器:如果说Docker镜像相当于系统光盘,那么Docker容器就是由这个系统光盘制作出来的可以跑的系统。
  4. Registry: 和我们的github类型,github存储的是代码,而Registry存储的是Docker的镜像,换句话说它就是Docker镜像仓库。

下面是整个Docker组件的组成图:

从 Docker 的使用角度来说最为关键的是镜像的制作,Docker 镜像的制作是通过Dockerfile来完成的,Dockerfile的编写我们会在下面进行介绍,这里我们先来看下Docker 镜像的组成:

容器基于镜像启动和运行。可以说Docker镜像是容器的基石,Docker的镜像是一个层叠的只读文件系统,它的最底端是一个引导文件系统及bootfs。 Docker用户几乎永远都不会和引导文件系统有交互,实际上当一个容器启动后,bootfs会被移到内存中,引导文件将被卸载。Docker镜像的第二层是rootfs(root文件系统),位于引导文件系统之上,可以有多种操作系统。 在传统的linux系统中root文件系统最先会以只读的方式加载,当引导和启动完成后他才会被切换成读写模式。 但是在Docker里,root文件系统永远只能是只读,并且Docker会用联合加载系统在rootfs之上加载更多的只读文件系统。 联合加载只得是一次加载多个文件系统。但是在外面看来只有一个文件系统。联合加载会将各层文件系统加载到一起, 这样最终的文件系统会包含所有的文件及目录。Docker将这样的文件系统称为镜像。 一个镜像可以放到另一个镜像顶部,位于下面的镜像称为父镜像。一个容器中可以运行用户的一个或多个进程。当一个容器启动时,Docker会在镜像的最顶层增加一个读写文件系统,我们在Docker中运行的程序就是在这个层运行并执行的。第一次启动Docker时,读写层是空的,当文件发生变化后都会应用到这一层。比如修改一个文件,先将该文件从只读层复制到读写层,然后隐藏只读层,这就是Docker的写时复制。

4. Docker的常用操作命令

镜像操作:

将镜像拉到本地             [docker pull ubuntu]
查看当前已经有的镜像 [docker images]
查找镜像 [docker search xxx]
删除镜像 [docker rmi d5a6e75613ea]

登录注销docker hub [docker login/logout]
上传镜像:在docker hub 上创建一个docker地址。 标准格式为 用户名/docker镜像名 比如我这边创建的docker镜像名 为testdocker 构建命令如下:
docker build -t "tbfungeek/testdocker:0.0.1" .
使用下面命令就可以进行push到dockerhub了
docker push tbfungeek/testdocker:0.0.1

容器操作:

查看docker info      [docker info]
查看当前正在运行的容器 [docker ps -a]
创建容器 [docker run -dit -p 8888:80 --name test ubuntu /bin/bash]
删除容器 [docker rm 容器id]
启动容器 [docker start xxxx]
重启容器 [docker restart xxxx]
附加到容器中 [docker attach xxxx]
退出容器 [exit]
停止容器 [docker stop]
查看日志 [docker logs -f xxxx]
查看端口 [docker port 4d17d19e34e2]

5. DockerFile的常用指令

构建会在Docker后台守护进程(daemon)中执行,而不是CLI中。构建前,构建进程会将全部内容(递归)发送到守护进程。
在创建一个Docker 镜像的时候推荐重新新建一个空的目录作为构建Docker的上下文,并且将Dockerfile放在上下文目录下的顶层目录(虽然可以通过-f参数来指定构建Docker的目录但是推荐还是放在上下文目录的顶层),在这个上下文文件夹中只存放用于构建当前 Docker镜像所必须的文件,对于不需要的文件通过dockerignore文件进行忽略。

Docker 守护进程会一条一条的执行Dockerfile中的指令,而且会在每一步提交并生成一个新镜像,最后会输出最终镜像的ID。生成完成后,Docker 守护进程会自动清理你发送的上下文。
Dockerfile文件中的每条指令会被独立执行,并会创建一个新镜像,RUN cd /tmp等命令不会对下条指令产生影响。
Docker 会重用已生成的中间镜像,以加速docker build的构建速度。

1. 创建目录
2. 创建Dockerfile
3. 编写Dockerfile
# Version: 0.0.1
FROM ubuntu:latest
MAINTAINER linxiaohai "tbfungeek@163.com"
RUN apt-get update && apt-get install vim
EXPOSE 80
4. 编译 Dockerfile生成镜像
docker build -f web_container/Dockerfile .
docker build --no-cache -t "标签linxiaohai/web:v1" .
docker build --no-cache -t "标签linxiaohai/web:v1" git@github.com:xxx/web_container
FROM
FROM <image>
FROM <image>:<tag>
FROM <image>@<digest>

在Dockerfile中第一条非注释指令一定是FROM,它指定了以哪一个镜像作为基准镜像,首先会先判断本地是否存在,如果不存在则会从仓库下载,这里推荐使用官方镜像

LABEL

给构建的镜像打标签。
如果base image中也有标签,则继承,如果是同名标签,则覆盖。为了减少layer数量,尽量将标签写在一个LABEL指令中去,如:

LABEL author="lin xiaohai" \
version="0.0.1"
指定后可以通过docker inspect查看:
"Labels": {
"author": "lin xiaohai",
"version": "0.0.1"
}
VOLUME

VOLUME用于创建挂载点,即向基于所构建镜像创始的容器添加卷

VOLUME ["/var/log"]
VOLUME /var/log /var/db

如,通过VOLUME创建一个挂载点:

ENV volum "/home/mydata"
VOLUME ${volum}

构建的镜像,并指定镜像名为docker_file。构建镜像后,使用新构建的运行一个容器。运行容器时,需-v参将能本地目录绑定到容器的卷(挂载点)上,以使容器可以访问宿主机的数据。

docker run -dit -v ~/test:/home/mydata/ --name "volumetests" docker_file
USER

USER用于指定运行镜像所使用的用户:

USER daemon

使用USER指定用户时,可以使用用户名、UID或GID,或是两者的组合。以下都是合法的指定试:

USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group

使用USER指定用户后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT都将使用该用户。镜像构建完成后,通过docker run运行容器时,可以通过-u参数来覆盖所指定的用户。

WORKDIR
WORKDIR /path/to/workdir

WORKDIR指令用于设置Dockerfile中的RUN、CMD和ENTRYPOINT指令执行命令的工作目录(默认为/目录),该指令在Dockerfile文件中可以出现多次,如果使用相对路径则为相对于WORKDIR上一次的值

ARG

ARG用于指定传递给构建运行时的变量:

ARG <name>[=<default value>]

在使用docker build构建镜像时,可以通过–build-arg =参数来指定或重设置这些变量的值。
docker内置了一批构建参数,可以不用在Dockerfile中声明:HTTP_PROXY、http_proxy、HTTPS_PROXY、https_proxy、FTP_PROXY、ftp_proxy、NO_PROXY、no_proxy

RUN

RUN指令会在当前镜像的顶层执行任何命令,并commit成新的(中间)镜像,提交的镜像会在后面继续用到。
上面看到RUN后的格式有两种写法。

shell格式,相当于执行/bin/sh -c ““:

RUN apt-get install vim -y

exec格式,不会触发shell,所以$HOME这样的环境变量无法使用,但它可以在没有bash的镜像中执行,而且可以避免错误的解析命令字符串:

RUN ["apt-get", "install", "vim", "-y"]

RUN ["/bin/bash", "-c", "apt-get install vim -y"] 与shell风格相同

RUN可以执行任何命令,然后在当前镜像上创建一个新层并提交。提交后的结果镜像将会用在Dockerfile文件的下一步。
通过RUN执行多条命令时,可以通过\换行执行,也可以在同一行中,通过分号分隔命令:

CMD

一个Dockerfile里只能有一个CMD,如果有多个,只有最后一个生效。CMD指令的主要功能是在build完成后,为了给docker run启动到容器时提供默认命令或参数,这些默认值可以包含可执行的命令,也可以只是参数(此时可执行命令就必须提前在ENTRYPOINT中指定)。
它与ENTRYPOINT的功能极为相似,区别在于如果docker run后面出现与CMD指定的相同命令,那么CMD会被覆盖;而ENTRYPOINT会把容器名后面的所有内容都当成参数传递给其指定的命令(不会对命令覆盖)。另外CMD还可以单独作为ENTRYPOINT的所接命令的可选参数。
CMD与RUN的区别在于,RUN是在build成镜像时就运行的,先于CMD和ENTRYPOINT的,CMD会在每次启动容器的时候运行,而RUN只在创建镜像时执行一次,固化在image中。

ENTRYPOINT

ENTRYPOINT命令设置在容器启动时执行命令,如果有多个ENTRYPOINT指令,那只有最后一个生效。
使用exec格式,在docker run 的所有参数,都会追加到ENTRYPOINT之后,并且会覆盖CMD所指定的参数(如果有的话)。当然可以在run时使用–entrypoint来覆盖ENTRYPOINT指令。
以推荐使用的exec格式为例:
我们可以使用ENTRYPOINT来设置基本不会变化的命令,用CMD来设置其它的可能改变的默认启动命令或选项(docker run会覆盖的)。

ENV

用于设置环境变量:

ENV <key> <value>
设置了后,后续的RUN命令都可以使用,当运行生成的镜像时这些环境变量依然有效,如果需要在运行时更改这些环境变量可以在运行docker run时添加-env <key>=<value>参数来修改
ADD

在构建镜像时,复制上下文中的文件到镜像内,格式:

ADD <src>... <dest>
ADD ["<src>",... "<dest>"]

可以是文件、目录,也可以是文件URL。可以使用模糊匹配(wildcards,类似shell的匹配),可以指定多个,必须是在上下文目录和子目录中,无法添加../a.txt这样的文件。如果是个目录,则复制的是目录下的所有内容,但不包括该目录。如果是个可被docker识别的压缩包,docker会以tar -x的方式解压后将内容复制到
可以是绝对路径,也可以是相对WORKDIR目录的相对路径。如果路径不存在则会自动级联创建,根据你的需要是里是否需要反斜杠/,习惯使用/结尾从而避免被当成文件。

COPY

COPY的语法与功能与ADD相同,只是不支持上面讲到的是远程URL、自动解压这两个特性,但是Best Practices for Writing Dockerfiles建议尽量使用COPY,并使用RUN与COPY的组合来代替ADD,这是因为虽然COPY只支持本地文件拷贝到container,但它的处理比ADD更加透明,建议只在复制tar文件时使用ADD,如ADD trusty-core-amd64.tar.gz /。

EXPOSE

EXPOSE指令告诉容器在运行时要监听的端口,但是这个端口是用于多个容器之间通信用的(links),外面的host是访问不到的。要把端口暴露给外面的主机,在启动容器时使用-p选项。

ONBUILD

向镜像中添加一个触发器,当以该镜像为base image再次构建新的镜像时,会触发执行其中的指令。格式:

ONBUILD [INSTRUCTION]

比如我们生成的镜像是用来部署Python代码的,但是因为有多个项目可能会复用该镜像。所以一个合适的方式是:

[...]
# 在下一次以此镜像为base image的构建中,执行ADD . /app/src,将项目代目添加到新镜像中去
ONBUILD ADD . /app/src
# 并且build Python代码
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

注意
ONBUILD只会继承给子节点的镜像,不会再继承给孙子节点。
ONBUILD ONBUILD或者ONBUILD FROM或者ONBUILD MAINTAINER是不允许的。

STOPSIGNAL

STOPSIGNAL用于设置停止容器所要发送的系统调用信号:

STOPSIGNAL signal

所使用的信号必须是内核系统调用表中的合法的值,如:9、SIGKILL

可以通过如下材料进行进一步学习:

http://www.cnblogs.com/qcloud1001/p/9273549.html
https://legacy.gitbook.com/book/yeasy/docker_practice/details
http://product.dangdang.com/23941643.html
https://github.com/qianlei90/Blog/issues/35
https://github.com/qianlei90/Blog/issues/36
http://seanlook.com/2014/11/17/dockerfile-introduction/
https://docs.docker.com/get-started/#docker-concepts

https://www.zhihu.com/question/22969309
https://juejin.im/post/5b4615b0f265da0f6d72c130

1.为什么选用Django

为什么选用Django,从个人角度来看就是因为它简单,上手容易,等到用熟后再切换到其他的框架比如Nodejs,Spring Boot,无非就是换个语言,思想是可以共同的,这就是我选用Django的原因,那它的简单体现在哪里?

  1. 开发语言为Python简单,在IT界有句名言“人生苦短,我用python”,python简单吗?只能说上手容易,能够做的覆盖面广,一门语言到深入的时候都是困难的。
  2. 切换框架组件十分容易,换个数据库就只要修改个setting配置
  3. 集成的组件大而全,对我而言最为诱人的是它集成的Admin后台。

2.Django架构总览

说到Django的架构不得不提到MTV,大家可能听说过MVC MVVM MVP这些,其实MTV也可以看成MVC,那MTV分别代表什么呢?
M - Model 也就是 Django中的数据模型
T - Template 等同MVC中的V 也就是Django中的视图层
V - View 等同于MVC中的C 也就是Django中的控制层

我们就依靠下面的图来讲下整个请求处理流程:

  1. 请求源发起网络请求,这里以常见的GET/POST为例子,这里的请求源有很多,比如常见的浏览器,手机app
  2. 请求到达框架后首先会经过请求中间件的处理
  3. 经过中间件处理后的请求会发送到路由上进行路由分配,路由分配是将这些请求分配到各个View中。这些请求作为View的参数传递进去。
  4. 到达view后,在view中会从数据库中将数据取出来,封装成一个一个Model对象,使用这些对象来完成我们的任务,在这个过程中可能会返回文件系统,比如图片,音视频资源,然后将这些资源整合在一起,传递给Template
  5. 在Template层中会将从view层中传递过来的数据整合到界面上,渲染出来,形成一个response。
  6. 生成response后在送达到用户浏览器之前可能还会经过response 中间件处理后送达到用户浏览器。

3.WSGI uWSGI ngix

这里我们不涉及到部署方面的知识,这些会在后面专门章节进行介绍,这里只不过介绍下这些概念,让大家对整个服务端结构有个大体的认识,我们前面讲的是从一个web app角度来看的一个流程,下面我们介绍的是从一台服务器的角度来看这个问题,区别在哪里呢?一个服务器可以包含一个或者多个应用。那就多出了服务器与web app的一个交互过程。

首先我们简要介绍下用户通过浏览器访问网页,具体经过了哪些环节。

  1. 用户输入需要访问网站的url,浏览器将这些封装成符合http格式的Request请求,这个请求中可以包含请求首行、请求头和请求体这些内容。
  2. 上面的Request请求是应用层数据,要通过网络请求发送出去需要再由操作系统完成TCP、IP、MAC层封装,最终送到网卡以比特流形式传递出去。
  3. 经过网络传输,比特流到达服务器端,被服务器接收,服务器操作系统依次剥离MAC、IP、TCP层封装,取出应用层数据,也就是浏览器发送出的Request请求,并交给应用层的Web应用。
  4. Web 应用解析Request请求内容,并生成Respond响应,交给服务器
  5. Respond响应也是应用层数据,由服务器OS完成TCP、IP、MAC层封装,送到网卡处以比特流形式送出。
  6. 经过网络传输,包含resonse的比特流到达服务器端,被用户机器接收。
  7. 用户机器逐一去掉 MAC、IP、TCP层封装,取出应用层数据,也就是Respond响应,并交给应用层的浏览器。浏览器根据Response响应内容,显示在用户面前。

我们再看下下面这张图:

我们上面讲的内容是django 那个框,这个框的输入是一个request输出是一个response,我们必须注意到一点服务器操作系统和web app也是有交互的:服务器操作系统将Request请求传给Web APP,Web APP处理后,将Respond响应传给服务器操作系统,那么,服务器操作系统怎么把Request请求传给Web APP?这就涉及到了WSGI接口。WSGI接口其实是一个协议,一个规范是抽象的东西。这个接口连接了服务器操作系统和和Web APP。

一般我们讲到一个东西的必要性就会做出如下假设?如果没有这个东西情况会怎样?如果没有wsgi,那么服务器OS来了个请求后首先需要判断这个请求是发送给哪个web app,必须根据不同的web app类型调用不同的接口,通过不同的接口将request传递过去。这显然不合常规,那正常的做法是怎样呢?正常应该有个公共的协议,我们不关心具体的web framework的类型是啥,你是Django 也好,是Flask也要,只要你是符合wsgi接口的web框架,我都能保证调用同一个接口将request从服务器操作系统传递给web app。也就是保证了不同web框架对外的一致性。

那么ngix的作用是什么呢?nginx的功能十分强大,我们只关心它的请求分发的功能。谈到它需要将它与web app的路由进行对比,web app的路由是将到达app的请求,细分到各个响应函数进行处理,而nginx的作用是决定某个请求分发到哪个web app,也就是说,首先某个请求从一个端口进来,通过ngix将其分配到该台服务器上的某个web app上面,web app路由再将这些请求,细分到函数进行处理。大致是这样一个区别。不知道大家理解了没有。记住一个服务器有不止一个web app,这里的web app用的web框架不止有一种,假设来了个请求你要怎么在这些不同的web框架之间进行分配。理顺了这些问题就ok了。