##1.对属性修饰符的理解
MRC下
ARC下
@property (nonatomic, strong) NSString *name;
self.name = @"mikejing";
NSMutableString *mStr = [[NSMutableString alloc] initWithString:@"mutablejiaojiao"];
self.name = mStr;
[mStr appendString:@"111"];
NSLog(@"%@",self.name);
可以看到,本身是不可变类型NSString,但是它会有可变类型的属性在,如果你用Strong修饰他,当该指针指向了NSMutableString类型的数据,Strong只是把源数据的多了一个新的指针指向而已,当源数据改变,那么原来设置的name不可变类型,就可以变了。这就是上面遇到修改之前的数据改变了现有的数据,上下文依赖了,因此copy来减少上下文依赖,如果本身是不可变类型NSstring,那么Strong也一样,如果变成可变类型了,那么减少上下文依赖,copy就会对可变类型执行深copy,从而让数据源更干净
strong 与copy都会使引用计数加1,但strong是两个指针指向同一个内存地址,copy会在内存里拷贝一份对象,两个指针指向不同的内存地址
举个简单的例子
@property (nonatomic,copy) NSString *name;
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release];
_name = [name copy];
}
}
- (NSString *)name
{
return [[_name copy] autorelease];
}
针对copy这个属性,以string类型为例,copy就是普通的指针copy(浅copy),而mutableCopy就是内容copy(深copy),如果是mutableString类型的话,那么copy就全是内容copy了
如果是容器类型,例如NSArray和NSDictionary,copy属性操作的指针针对容器本身,而容器内部的都还是同一个指针引用,并不会根据容器的copy或者strong而变化,指针根据自己的属性修饰符有关
__block与__weak的区别
__block是用来修饰一个变量,这个变量就可以在block中被修改
__block:使用 __block修饰的变量在block代码块中会被retain(ARC下会retain,MRC下不会retain)
__weak:使用__weak修饰的变量不会在block代码块中被retain
同时,在ARC下,要避免block出现循环引用 __weak typedof(self)weakSelf = self;
这两个都是和block相关,block就是OC对闭包的实现,闭包就是匿名函数,或者理解为指向函数的指针
block变量定义时为什么用copy?block是放在哪里的?
block本身是像对象一样可以retain,和release。但是,block在创建的时候,它的内存是分配在栈(stack)上,可能被随时回收,而不是在堆(heap)上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。通过copy可以把block拷贝(copy)到堆,保证block的声明域外使用。
特别需要注意的地方就是在把block放到集合类当中去的时候,如果直接把生成的block放入到集合类中,是无法在其他地方使用block,必须要对block进行copy。
[array addObject:[[^{
NSLog(@"hello!");
} copy] autorelease]];
Atomic和NonAtomic
默认是是线程安全的Atomic,加锁操作,但是速度会很慢,我们就会写上NonAtomic,保证速度,但是不保证多线程安全问题
//@property(nonatomic, retain) UITextField *userName;
//系统生成的代码如下:
- (UITextField *) userName {
return userName;
}
- (void) setUserName:(UITextField *)userName_ {
[userName_ retain];
[userName release];
userName = userName_;
}
/@property(retain) UITextField *userName;
//系统生成的代码如下:
- (UITextField *) userName {
UITextField *retval = nil;
@synchronized(self) {
retval = [[userName retain] autorelease];
}
return retval;
}
- (void) setUserName:(UITextField *)userName_ {
@synchronized(self) {
[userName release];
userName = [userName_ retain];
}
}
为什么不用atomic?其实atomic也不是线程安全的看如下代码
@property (atomic, assign) NSInteger count;
self.count = 0;
NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
[threadA start];
NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
[threadB start];
}
- (void)doSomething {
for (NSInteger i = 0; i < 10; i++) {
[NSThread sleepForTimeInterval:1.0];
self.count++;
NSLog(@"self.count = %@ %@", @(self.count), [NSThread currentThread]);
}
}
2018-08-27 16:37:35.619901+0800 test[16093:320829] self.count = 8 {number = 4, name = (null)}
2018-08-27 16:37:36.621834+0800 test[16093:320828] self.count = 9 {number = 3, name = (null)}
2018-08-27 16:37:36.621837+0800 test[16093:320829] self.count = 9 {number = 4, name = (null)}
2018-08-27 16:37:37.623387+0800 test[16093:320829] self.count = 10 {number = 4, name = (null)}
上面代码中,把属性 count 声明为 atomic 的。在 viewDidLoad 中创建了两个线程 threadA 和 threadB,都去执行 doSomething 方法。在 doSomething 方法中,去给 self.count 的值通过每次循环 +1 增加 10 次,然后打印 self.count 的值。为了让异常情况出现的概率提高,加入一句 [NSThread sleepForTimeInterval:1.0];。
运行上面的代码,会发现打印的结果中,最后一条 self.count 的值往往是小于 20 的,在中间的某些打印日志中,会发现有些数字被重复打印的两次。
self.count++;
这句代码做了两件事,先读取 self.count 的值,然后把读取到的值 + 1 后赋值给 self.count。
由于 atomic 仅仅能保证写是线程安全的,而不是保证 读 -> +1 -> 写,这个整体是线程安全的。
当两个线程都执行到读取完 self.count 的值后,再去写,就会写成一样的值。
所以大部分情况下,为了保证线程安全,还是要自己加锁,可以根据需要来保证某块代码整体的线程安全。
线程安全的代码:
- (void)doSomething {
for (NSInteger i = 0; i < 10; i++) {
[NSThread sleepForTimeInterval:1.0];
@synchronized(self){
self.count++;
NSLog(@"self.count = %@ %@", @(self.count), [NSThread currentThread]);
}
}
}
这就是为什么不用atomic的原因了,反正加了也避免不了,索性都用Nonatomic,自己手动加锁即可
http://liuduo.me/2018/02/08/objective-c-atomic/
https://stackoverflow.com/questions/8382523/setter-and-getter-for-an-atomic-property
##1.1说说你对Weak关键字的理解
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
追问的问题一:
1.实现weak后,为什么对象释放后会自动为nil?
runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为 0 的时候会 dealloc,假如 weak 指向的对象内存地址是 a ,那么就会以 a 为键, 在这个 weak 表中搜索,找到所有以 a 为键的 weak 对象,从而设置为 nil 。
追问的问题二:
2.当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?
1、调用objc_release
2、因为对象的引用计数为0,所以执行dealloc
3、在dealloc中,调用了_objc_rootDealloc函数
4、在_objc_rootDealloc中,调用了object_dispose函数
5、调用objc_destructInstance
6、最后调用objc_clear_deallocating,详细过程如下:
a. 从weak表中获取废弃对象的地址为键值的记录
b. 将包含在记录中的所有附有 weak修饰符变量的地址,赋值为 nil
c. 将weak表中该记录删除
d. 从引用计数表中删除废弃对象的地址为键值的记录
通俗来说就是对象引用计数为0的时候,也就是被释放的时候会调用dealloc,然后在之前根据这个对象地址注册创建的weak标中找到以该对象地址为key的记录,将这个key对应value数组的左右变量地址都置为nil,然后把value数组清空,然后把key的记录也移除,清干净
##1.3OC中堆和栈的区别
一个由C/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其
操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回
收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的
全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另
一块区域。 - 程序结束后由系统释放。
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
申请方式:
栈:系统分配
堆:程序员自己申请
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢
出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表
中删除,并将该结点的空间分配给程序,
申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。
申请效率的比较
栈由系统自动分配,速度较快。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
OC采用引用计数器对内存进行管理,当一个对象的引用计数(retainCount)为0,则被释放。
MRC:每当alloc/new/copy/mutableCopy生成对象的时候引用计数+1,后续对变量的管理相关的方法有:retain, release 和 autorelease。retain 和 release 方法操作的是引用记数,当引用记数为零时,便自动释放内存,执行dealloc。并且可以用 NSAutoreleasePool 对象,对加入自动释放池(autorelease 调用)的变量进行管理,当 drain 时回收内存。
ARC:自动管理引用计数
虽然Apple自动帮我们管理了,但是也会有情况出现内存泄露
1.Block循环引用
2.WebView注册的JS未释放 Delegate没有置nil
3.CoreFoundation框架下有些还是要手动管理
上面是很浅的回答,先看下Demo
UILabel *label = [UILabel new];
CFIndex widx = CFGetRetainCount((__bridge CFTypeRef)label);
NSLog(@"初始值count:%ld",widx);
NSLog(@"");
__weak typeof(label) weakLabel = label;
CFIndex idx11 = CFGetRetainCount((__bridge CFTypeRef)label);
CFIndex widx22 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
NSLog(@"count:%ld",idx11);
NSLog(@"count:%ld",widx22);
NSLog(@"");
__strong typeof(label) strongLabel = weakLabel;
CFIndex idx111 = CFGetRetainCount((__bridge CFTypeRef)label);
CFIndex widx222 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
CFIndex widx333 = CFGetRetainCount((__bridge CFTypeRef)strongLabel);
NSLog(@"count:%ld",idx111);
NSLog(@"count:%ld",widx222);
NSLog(@"count:%ld",widx333);
NSLog(@"");
__weak UILabel *lb = label;
CFIndex idx1111 = CFGetRetainCount((__bridge CFTypeRef)label);
CFIndex widx2222 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
CFIndex widx3333 = CFGetRetainCount((__bridge CFTypeRef)strongLabel);
CFIndex widx4444 = CFGetRetainCount((__bridge CFTypeRef)lb);
NSLog(@"count:%ld",idx1111);
NSLog(@"count:%ld",widx2222);
NSLog(@"count:%ld",widx3333);
NSLog(@"count:%ld",widx4444);
2018-09-03 14:59:29.804428+0800 retaincount[27022:536855] 初始值count:1
2018-09-03 14:59:29.804553+0800 retaincount[27022:536855]
2018-09-03 14:59:29.804626+0800 retaincount[27022:536855] count:1
2018-09-03 14:59:29.804719+0800 retaincount[27022:536855] count:2
2018-09-03 14:59:29.804796+0800 retaincount[27022:536855]
2018-09-03 14:59:29.804870+0800 retaincount[27022:536855] count:2
2018-09-03 14:59:29.804944+0800 retaincount[27022:536855] count:3
2018-09-03 14:59:29.805023+0800 retaincount[27022:536855] count:2
2018-09-03 14:59:29.805125+0800 retaincount[27022:536855]
2018-09-03 14:59:29.805193+0800 retaincount[27022:536855] count:2
2018-09-03 14:59:29.805268+0800 retaincount[27022:536855] count:3
2018-09-03 14:59:29.805337+0800 retaincount[27022:536855] count:2
2018-09-03 14:59:29.805436+0800 retaincount[27022:536855] count:3
上面的代码可以看出
__weak修饰的对象,如果打印原指针,不会增加原指针的引用计数,如果打印__weak类型指针,会再原始基础上+1,如论你加多少个weak,你打印weak指针总是原引用计数+1,这是为什么呢?看下面的文章 底层代码
https://juejin.im/post/5b4c59a55188251ac9767872
里面有个代码块
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
这里获取引用计数的时候,还是会去判断一下是否 has_sidetable_rc
也就是是有有weak hash表,我猜测当我们用strong类型访问的时候,就为no,如果是weak类型指针调用的时候就是yes,这也就是上面的引用计数会在weak修饰下weak打印会+1,而strong还是原来的引用计数
如果用__unsafe_unretain来修饰就不会,因为这里没有维护weak hash表,不会有这一层判断,但是最后一个weak修饰打印weak指针就还是比原来的引用计数+1
UILabel *label = [UILabel new];
CFIndex widx = CFGetRetainCount((__bridge CFTypeRef)label);
NSLog(@"初始值count:%ld",widx);
NSLog(@"");
__unsafe_unretained typeof(label) weakLabel = label;
CFIndex idx11 = CFGetRetainCount((__bridge CFTypeRef)label);
CFIndex widx22 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
NSLog(@"count:%ld",idx11);
NSLog(@"count:%ld",widx22);
NSLog(@"");
__strong typeof(label) strongLabel = weakLabel;
CFIndex idx111 = CFGetRetainCount((__bridge CFTypeRef)label);
CFIndex widx222 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
CFIndex widx333 = CFGetRetainCount((__bridge CFTypeRef)strongLabel);
NSLog(@"count:%ld",idx111);
NSLog(@"count:%ld",widx222);
NSLog(@"count:%ld",widx333);
NSLog(@"");
__weak UILabel *lb = label;
CFIndex idx1111 = CFGetRetainCount((__bridge CFTypeRef)label);
CFIndex widx2222 = CFGetRetainCount((__bridge CFTypeRef)weakLabel);
CFIndex widx3333 = CFGetRetainCount((__bridge CFTypeRef)strongLabel);
CFIndex widx4444 = CFGetRetainCount((__bridge CFTypeRef)lb);
NSLog(@"count:%ld",idx1111);
NSLog(@"count:%ld",widx2222);
NSLog(@"count:%ld",widx3333);
NSLog(@"count:%ld",widx4444);
2018-09-03 16:00:17.617836+0800 retaincount[28912:574549] 初始值count:1
2018-09-03 16:00:17.618007+0800 retaincount[28912:574549]
2018-09-03 16:00:17.618091+0800 retaincount[28912:574549] count:1
2018-09-03 16:00:17.618171+0800 retaincount[28912:574549] count:1
2018-09-03 16:00:17.618246+0800 retaincount[28912:574549]
2018-09-03 16:00:17.618330+0800 retaincount[28912:574549] count:2
2018-09-03 16:00:17.618411+0800 retaincount[28912:574549] count:2
2018-09-03 16:00:17.618486+0800 retaincount[28912:574549] count:2
2018-09-03 16:00:17.618564+0800 retaincount[28912:574549]
2018-09-03 16:00:17.618638+0800 retaincount[28912:574549] count:2
2018-09-03 16:00:17.618714+0800 retaincount[28912:574549] count:2
2018-09-03 16:00:17.618800+0800 retaincount[28912:574549] count:2
2018-09-03 16:00:17.618884+0800 retaincount[28912:574549] count:3
##2.NSTimer为什么不准
NSTimer会受到Runloop的影响 CADisplaylink一样会受到影响
用GCD创建就不会
dispatch_source_create 创建
dispatch_source_set_timer 设置时间
dispatch_source_set_event_handler 设置时间回调
disaptch_resume 启动
##3.内存管理
alloc/new/copy/mutableCopy进行对象的创建
retain 引用计数 + 1
release -1
当引用计数为0的时候,对象被释放,调用dealloc
id objc = [[NSObject alloc] init];
id objc1 = [NSObject new];
id obj = [NSArray array];
[obj retain];
id objc = [[NSObject alloc] init];
[objc release];
指向对象的指针仍旧保留在ob这个变量中,但是对象已经被释放,不可以访问
id obj = [NSArray array];
[obj release];
##4.Runloop了解下
概念
一般来讲线程只会执行一次任务,然后就会退出,因此就出现了一个机制,让线程随时能处理事件但并不退出。所以Runloop就是一个对象,管理需要处理的消息和事件,并提供一个入口函数启动do while循环。该循环能随时处理事件和消息,处理完进入休眠避免资源占用,有消息来时立刻被唤醒,除非有特定条件下就会退出
Runloop他的本质就是一个do,while循环
(1)保持程序的持续运行,当有事做时做事,
(2)负责监听处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
(3)节省CPU资源,提高程序性能,需要的时候处理事件和消息,否则进入休眠等待唤醒
与线程的关系
每个线程都有一个Run Loop,主线程的Run Loop会在App运行时自动运行,子线程中需要手动获取运行,创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。获取线程对应的Runloop的时候,全局变量有一个字典和自旋锁,字典存放线程对应的Runloop,按需懒加载创建Runloop和线程绑定,获取的时候通过自旋锁加锁保证线程安全。
Runloop结构
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。当创建的loop,没有任何上面的属性,就会直接退出。
Run的时候必须制定mode,那么这里有个知识点是CommonMode,不能指定该Mode进行运行,但是可以把事件标记到该属性,例如NSTimer时间标记到ModeItems里面,而且系统的Default和TrackingMode都是Common属性的,当切换Mode的时候,如果是Common属性,会把添加到ModeItems里面的item同步到对应的Mode,那么无论切换到哪个CommonMode都会执行
Apple应用
1.AutoreleasePool
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer。
第一个 Observer 监视的事件是 Entry(即将进入Loop),执行方法去创建pool,优先级最高
第二个Observer监听两个事件,BeforeWaiting(准备进入休眠) 时调用pop and push,先清除旧pool,然后创建新pool,或者再退出的时候释放自动释放池子,优先级最低
2.启动时注册了一些观察者和事件回调来处理事件响应,手势识别和UI更新
2.1事件响应处理成source1,然后根据回调方法分发处理
2.2手势触发会进行标记,通过添加BeforeWaiting/Exit的观察者,在休眠前把标记的手势处理
2.3页面更新,也就是CADIsplayLink中iOS屏幕刷新频率1/60,当UI操作的时候改变UI层级,设置Frame,或者手动调用UIView/CALayer的SetNeedsLayout/SetNeedsDisaplay的时候,标记为待处理,放入全局容器,Apple注册了BeforeWaiting和Exit的的监听,会在对应的回调函数中遍历所有标记的UI进行页面更新。
3.NSTimer这个很容易理解,Runloop代码中也有很明确的处理函数
4.GCD中的dispatch_getmainqueue,libDispatch会向主线程Runloop发送消息,Runloop被唤醒,这里也有对应的代码可以看到,一般唤醒无非就是处理NSTimer,dispatch_getmainqueue和source事件
开发者应用:
1.常驻线程 处理网络请求的回调,子线程懒加载获取并开启loop,这里需要手动添加一个mach事件,保活
2.Common在不同模式下也执行NSTimer
3.添加Observe属性的观察,在即将进入休眠的ASDK的原理,以下就是AS框架的Runloop应用,这里把排版,绘制等操作放到异步做,然后存储起来
在kCFRunLoopBeforeWaiting和kCFRunLoopExit两个activity的回调中将之前异步完成的工作同步到主线程中去。
用户操作卡顿以及屏幕刷新频率的理解
1.iOS显示系统是由VSync驱动的,VSync由硬件生成,也就是每秒钟60次,
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
对应到Runloop里面就是在App启动的Runloop里面注册回调函数CFRunLoopSource接收mach_port传递过来的时钟信号通知。随后source的回调驱动整个App的动画和UI显示。
Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。
亦或者
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
OK 下面按我个人理解解释下VSync这个图卡顿是如何在Runloop中影响的。
一次VSync只是把缓冲区内计算好渲染好的图像显示出来,根据上面的介绍,UI的点击事件之类的其实是由Source1来触发的,手势就比如各种UI操作就是把手势或者UIView/CALayer标记为Dirty,添加Observer,在BeforeWaiting/Exit的时候,系统设有回调函数操作更新UI(这里的回调函数会在App启动的主线程Runloop结构体添加),VSync是1/60一次刷新频率,这是由mach_port传递硬件信号给App注册的回调函数,也就是上面提到的Source0来唤醒Runloop,可以理解为主线程的Runloop一直在处于休眠唤醒切换,一秒钟理论上能切换60次,随后Source0的对应的回调显示动画和UI,那么问题就来了,一次手势或者一些其他操作导致需要UI的绘制,排版,渲染以及图片的压缩,然后交给GPU进行合成和渲染,如果时间过长,超过了1/60,那么就会跳帧。通常来说,计算机系统中 CPU、GPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。也就是一次耗时的计算会错过时间把结果放入缓冲区,就会掉帧。
1.Runloop do while代码中可以看到,如果App处于静止状态,系统硬件发出的时钟通知VSync信号,通过Source1唤醒Runloop,这个时候Runloop的周期可以认为是一个VSync,但这是不准确的,Runloop没有周期限制,然后会检测Timer,Obeserve,Source等事件,其中Observer中会有BeforeWaiting/Exit的状态被监听,例如手势和Core Animation的,UIView和CALayer更新等,这一次唤醒的这些UI处理例如绘制,排版,图片压缩处理等,以及GPU的合成和渲染,如果正常情况下都在主线程的话就会很耗时,这里Timer没有,Source没有,Observer的事件就是被手势和UI处理包揽了,因此,Runloop在唤醒执行一次循环后立马进入mach_msg()进入休眠,之前就会发出通知处理绘制等耗时操作,如果这些操作没能在一次VSync中处理完,GPU就没能把新的图像资源提交给缓冲区,就会跳帧,就卡顿了。因此ASDK就把这些耗时操作丢到了后台线程,他会在系统当 RunLoop 进入休眠前、CA 处理完事件后(一定是系统方法处理完之后),然后把UI处理的耗时操作结果从容器中提取出来即可(耗时操作已经被丢到了后台),这么做如果数据没处理完,缓冲区也会有数据,只是页面是空的而已,但是不会像上面一样连结果都没来得及放入缓冲区,直接卡顿了。
注意:这里一次VSync的触发,就会跑一次Runloop代码,正常下没有Timer和Source的耗时操作,会直接进入BeforeWaiting的Observer通知,这个通知被监听的函数会执行绘制操作,这就是那些CPU或者GPU耗时操作,默认在主线程,无法完成把数据放入GPU处理后的缓冲区,会错过下一次VSync的显示,卡顿
2.如果App用户在操作,手势等,可以理解为 VSync-------VSync之间,用户在滑动屏幕看东西,静止状态下一次CPU和GPU处理完提交给缓冲区的图像时间是1/60,如果用户在滑动,时间就会少于1/60了,怎么理解呢?就是上面静止状态下,一次Runloop的唤醒会执行Timer,Observer和Source事件,静止状态下可以理解为没有Timer和Source0,只有Source1,loop会马上进入Observer发送BeforeWaiting的通知,让监听者处理耗时(Core Animation等),现在操作情况下无非就是多了Source0的时间,在BeforeWaiting之前还要标记下UI(SetNeedsLayout或SetNeedsDisplay),多了这一步而已,然后继续进入BeforeWaiting,CoreAnimtion会把通话提交到一个中间态,UIView和CALayer(手势操作的更改对象)会遍历把标记为需要更新的对象进行绘制和调整,然后交给GPU处理完给缓冲区,如果UI的绘制等耗时操作更多,就更容易卡顿,因此,更需要把这些耗时的操作丢到后台线程,避免主线程的CPU和GPU计算卡顿,导致来不及把下一帧数据显示出来,即便是空的数据,那也是能显示,不会说缓冲区没数据,就会卡顿了。
3.VSync是用来唤醒一次Runloop周期的,针对UI处理来讲,期间Timer,Observer和Source0,Source1都会在这个循环方法内进行对象的标记(SetNeedsLayout等),标记后Runloop就会马上进去休眠状态并发出BeforeWaiting的通知,观察者(Core Animation,UIView/CALayler,ASDK等)处理耗时操作,如果这些操作是在主线程并且真的太耗时,很明显,下一次VSync到来的时候,CPU或者GPU没能把数据放入缓冲区,显示不了,就掉帧,卡爆了。。。。。。这个时候ASDK这种耗时操作,就能把操作放入后台线程的在显示的时候例如CALayer的dispalay,直接放进去后台并发队列,在最后BeforeWaiting的时候去处理好的队列里面取结果即可,不在主线程做任何复杂的计算,从而提高流畅的滑动体验。
灯神ASDK
ASDK goole的UI框架原理
一个自称用cell实现Runloop应用的人
__unsafe_unretained __typeof__(self) weakSelf = self;
void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
[weakSelf processQueue];
};
_runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, handlerBlock);
CFRunLoopAddObserver(_runLoop, _runLoopObserver, kCFRunLoopCommonModes);
##4.什么是Runtime
Runtime是用C++编写的运行时库,为 C 添加了面向对象的能力,这也是区别于C语言这样静态语言的重要特性,C语言函数调用在编译期间就已经决定了,在编译完成之后直接顺序执行。OC是动态语言(多态和运行时),函数调用就是消息发送,在编译期间不知道具体调用哪个函数,所以Runtime就是去解决运行时找到调用哪个方法的问题
多态:OC中的多态是不同对象对同一消息的不同响应方式,子类通过重写父类的方法来改变同一方法的实现,体现多态性
总结:三个能力
1.面向对象能力
2.动态加载类信息,进行消息的分发
3.消息转发
顺序:
instance—>class---->method----->sel----->imp---->实现函数
实例对象中存放isa指针,有isa指针就可以找到实力变量对应的所属类,类中存放着实例方法列表,SEL为Key,IMP为value,在编译期间,根据方法名会生成一个唯一的int标识符,这就是SEL标识,IMP就是函数指针,指向最终函数实现。runtime核心就是objc_msgSend函数,通过给SEL传递消息,找到匹配的IMP
##4.1 iOS消息转发机制
Runtime机制详解
消息转发机制如何保护程序找不到方法而崩溃
消息转发机制基本分为三个步骤:
1、动态方法解析 resolveInstanceMethod 和 resolveClassMethod
2、备用接受者 forwardingTargetForSelector
3、完整转发 forwardInvocation 和 methodSignatureForSelector
1.动态方法解析
对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法”“。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。
可以理解为动态在该所属类下面根据Sel重新指定对应的IMP(class_addMethod)
2.备用接收者
动态方法解析无法处理消息,则会走备用接受者。这个备用接受者只能是一个新的对象,不能是self本身,否则就会出现无限循环。如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果
可以理解为上面resolveInstanceMethod没有在本类实现,那么该方法就在另一个类来进行实现
3.最终完整消息转发
如果动态方法解析和备用接受者都没有处理这个消息,那么就会走完整消息转发:
该方法我感觉和上面的备用接收是一样的操作,至少备用接收者无法设置为self,而该方法可以设置任意对象进行消息的实现
具体的代码上面两个链接都有,这补充一个可用的知识点
如何避免出现unrecognize selector的崩溃
1.对象未接收到消息时会进行动态消息转发,第一步就是动态方法解析,例如实例化方法会调用resolveInstanceMethod
进行拦截,一般不再这里处理
2.这里不处理的话就会走到下一步,也就是备用接收者,forwardingTargetSelector来进行消息拦截(因此,给一个Çategory就会是非常好的转发接收者来处理避免找不到方法而崩溃)
// A Selector for a method that the receiver does not implement.
// 当category重写类已有的方法时会出现此警告。
// Category is implementing a method which will also be implemented by its primary class
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"unrecognized selector : classe:%@ sel:%@",NSStringFromClass([self class]),NSStringFromSelector(aSelector));
// 元类 meta class 创建 重新指定Selector 防止崩溃 http://ios.jobbole.com/81657/
// 1、为”class pair”分配内存 (使用objc_allocateClassPair).
// 2、添加方法或成员变量到有需要的类里 (我已经使用class_addMethod添加了一个方法).
// 3、创建出来
// 用objc_allocateClassPair创建一个自定义名字的元类
Class class = objc_allocateClassPair(NSClassFromString(@"NSObject"), "UnrecognizedSel", 0);
// 类添加方法 Sel 和 Imp
class_addMethod(class, aSelector, class_getMethodImplementation([self class], @selector(customMethod)), "v@:");
// class_addIvar(<#Class _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
// objc_registerClassPair(class)
// 创建
id tempObject = [[class alloc] init];
return tempObject;
}
#pragma clang diagnostic pop
- (void)customMethod{
NSLog(@"呵呵");
}
runtime可以做的几个点:
1.json转model
2.Category动态关联属性
3.MethodSwizzling (1.全局VC容器 2.无码埋点 3.全局拖翻手势 4.避免崩溃越界啥的替换原有方法 5.通过hook alloc new dealloc等,主要思路是在一个时间切片内检测对象的声明周期以观察内存是否会无限增长。通过 hook 掉 alloc,dealloc,retain,release 等方法,来记录对象的生命周期。)
这个hook可以操作很多东西
4.JSPatch
5.消息转发失败检测
##5.setNeedLayout,setNeedsDisplay,layoutIfNeed的区别
setNeedsDisplay和setNeedsLayout
1,UIView的setNeedsDisplay和setNeedsLayout方法
首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphicsGetCurrentContext,就可以画画了。而setNeedsLayout会默认调用layoutSubViews,
就可以 处理子视图中的一些数据。
综上所诉,setNeedsDisplay方便绘图,而layoutSubViews方便出来数据。
layoutSubviews在以下情况下会被调用:
1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转Screen会触发父UIView上的layoutSubviews事件。
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。drawRect在以下情况下会被调用:
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
以上1,2推荐;而3,4不提倡drawRect方法使用注意点:
1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。
2、若使用calayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕
勾选creat folders refrence的时候是蓝色文件夹,这个意思是直接存放到包里面,别人下载了你的app,就能打开包内容,看到里面的资源
当你做app换肤的时候,这个时候做几套一样名字的图片,然后分别放到A,B,C三个不同的文件夹,然后放到包内部,使用的时候直接用NSBundle mainbundle path加载文件路径就行了, skin/%@/%@即可
UIImage imageWithContentOfFIle来获取,那么你在外面直接用之前的方法名就好了,你选择皮肤的时候,只是更改了沙盒里面的字段,在方法内部进行一层包装,方法还是一样的调用,只是保存一个本地的切换选择
那么如何实现下载皮肤?
之前已经有几套皮肤了,但是自己下载的话可以给标识,默认的几套皮肤是放在之前的NSBundle包里面的,根据路径获取,但是下载皮肤是放在沙盒里面的,根据NSUserDefault判断去哪里获取
##7.iOS的响应链 事件传递和响应过程分析
https://www.jianshu.com/p/2e074db792ba
注意:如果父控件不能接收触摸事件,那么子控件就不能接收
首先肯定是UIResponder的子类才能接受和处理事件因为有 begin move end cancel等几个方法
事件传递(查找最合适的控件处理事件)
产生触摸事件 → UIApplication事件队列 → UIWindow的hitTest:withEvent:→ UIView的hitTest:withEvent: → 子View的hitTest:withEvent: → 子View的hitTest:withEvent:
事件传递主要是查找最合适响应事件的View
1.1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
1.2.触摸点是否在自己身上
1.3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)
1.4.如果没有符合条件的子控件,那么就认为自己最合适处理
注:在对象执行hitTest:withEvent:的过程中,如果对象自己的pointInside: withEvent:方法返回NO,就返回nil,否则开始查找所有的子View,一旦没有子View或者子View全部返回nil,就会把自己作为最合适View返回,UIWindow拿到最合适的View
事件分发(然后把具体事件传递)
UIApplication sendEvent: → UIWindow sendEvent: → 最合适的view开始响应
事件响应(开始响应)
根据事件类型调用对应方法,以touchBegan为例:
最合适的view touchesBegan: withEvent: → 所在ViewController touchesBegan: withEvent:→ parentView touchesBegan: withEvent: → … → UIWindow touchesBegan: withEvent: → UIAplication touchesBegan: withEvent: → AplicationDelegate touchesBegan: withEvent: → 结束
注:如果某个View或ViewController未调用super touchesBegan: withEvent:则响应结束 touchs事件调用super默认是向上一层层传递的过程,需要在对应层实现就重写,结束传递就不要调用super,如果传递到application还是没人处理,结束丢弃即可
##8.load和initialize的区别
load:
文件被程序加载时也就是第一次调用的时候会调用,例如编译器跑文件进去的时候,优先是父类,子类
但是load加载时的环境不安全,我们应该尽量避免减少load方法的逻辑,load是线程安全的,内部有锁,因此应该避免
线程阻塞在这里,能做的就是在load方法的时候进行method_swizzing,其他就比搞了
initialize
这个方法是在第一次在给某个类发送消息是调用(比如实例化一个对象),只会调用一次
1.两者都是在实例化对象之前调用,以main为分割,前者main函数之前调用,后者在之后调用,都是自动的
2.load用来method_swizzle方法,initialize用于初始化变量和静态变量
3.都是线程安全的,内部都加了锁,因此操作的时候尽可能简单,避免阻塞
##9.KVC和KVO
KVC
key value coding 键值编码 不通过getter或者setter方法进行访问,而是通过属性字符串间接访问属性的机制
1.首先查找有无属性,setter getter的,若有直接使用
2.若无直接访问实例变量,若无,就直接抛出异常undefinedkey进行异常抛出,除非你重写
扩展:可以根据这个思路搞一个容器接收查找不到的属性值,实现兼容
KVO (KVO内部实现机制的简单代码思路)
观察者模式,对属性的setter方法进行观察,期间会衍生出子类进行正真的通知,可以用来进行view和model的绑定
1.基本理解
KVO是基于Runtime机制实现的,当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
2.深入理解
1.Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为: NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
2.NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;
3.所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。
4.(isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
下面的代码片段是首先给类创建一个Çategory,当我们实例化该类的时候,调用下这个方法,内部会用object_setClass重新把对象的isa指针指向新创建的子类,当外部调用setter方法修改属性的时候,会重新跑到子类里面进行setter的重写,最终在子类里面通知观察者,由于这个是Category,观察者只能通过runtime的objc_setAssociatedObject
来存储观察者,进行最后的通知消息转发
- (void)mkj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
// 修改调用者的isa指针为动态创建的子类,能让set方法改变的时候,去子类进行操作
object_setClass(self, [MKJKVONotifying_CalculateManager class]);
// 由于observr没有任何存储,子类set方法改变之后如何调用observe实现的方法?runtime
// 动态给对象关联一个属性
objc_setAssociatedObject(self, "hehe", observer, OBJC_ASSOCIATION_RETAIN);
}
5.子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法: 被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
- (Class)class
{
return [CalculateManager class];
}
- (void)setName:(NSString *)name
{
// 调用父类
[super setName:name];
// 根据关联字段拿出关联的观察者
id obj = objc_getAssociatedObject(self, "hehe");
// 子类最终通知调用观察者的observer方法
[obj observeValueForKeyPath:@"name" ofObject:self change:@{@"change":name} context:nil];
}
##10.如何用NSValue包装C语言结构体
typedef struct mystruct{
int count;
float height;
}mystruct
如何将结构体转为NSValue,并存入数组
mystruct mst;
mst.count = 10;
mst.height = 100;
NSValue *value = [NSValue value:&mst withObjcType:@encode(mystruct)];
##11.自己写一个NSString的实现
+ (id)initWithCString:(char *)nullTerminatedCString encoding:(NSStringEncoding)encoding;
+ (id) stringWithCString: (char*)nullTerminatedCString
encoding: (NSStringEncoding)encoding
{
NSString *obj;
obj = [self allocWithZone: NSDefaultMallocZone()];
obj = [obj initWithCString: nullTerminatedCString encoding: encoding];
return AUTORELEASE(obj);
}
##12.静态库和动态库
静态库:.a和.framework
链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
如果静态函数库改变了,那么你的程序必须重新编译
特点:模块化,分工合作,可重用
动态库:.dylib和.framework
链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存
特点:可以将最终可执行文件体积缩小,多个文件共享,节省资源,动态更改库文件更新程序
.a和framework文件的区别
.a是二进制文件,不能直接拿来使用,需要配合头文件资源文件一起使用
因为打包静态库的时候只能打包代码,不能打包资源文件和头文件,因此用的时候需要.a文件+需要暴露的头文件+资源文件
framework除了二进制文件之外还有资源文件,可以直接拿来使用
##13.Base64和MD5
MD5是一种不可逆的消息摘要算法。为计算机安全领域广泛使⽤的一种散列函数
效果:
把一个任意长度的字节串变换成⼀定⻓度的⼗六进制数字串。
目的是让⼤容量信息在⽤数字签名软件签署私⼈密钥前被"压缩"成⼀种保密的格式。
应用:
1、一致性验证:
从网上下载⽂件,软件,各种资料的时候,有些文件会提供MD5对照信息。利⽤MD5校验软件来核对下载的⽂件,以此观测下载得到的文件与传送者是否相符。
利用MD5算法来进⾏文件校验的方案被大量应用到软件下载站、论坛数据库、系统文件安全等⽅方⾯面
2、数字证书:
对一段Message(字节串)产生fingerprint(指纹),以防止被"篡改”。举个例子,你将一段话写在一个叫readme.txt文件中,并对这个readme.txt产生一个MD5的值并记录在案,然后你可以传播这个文件给别人别人如果修改了文件中的任何内容,你对这个文件重新计算MD5时就会发现。如果再有一个第三方的认证机构,用MD5还可以防止文件作者的"抵赖",这 就是所谓的数字签名应用。
3、安全访问认证:
用于操作系统的登陆认证上。当用户登录的时候,系统把⽤户输入的密码进⾏MD5 Hash运算,然后再去和保存在文件系统中的MD5值进⾏比较, 进⽽确定输⼊的密码是否正确。
通过这样的步骤,系统在并不知道用户密码的明码的情况下就可以确定用户登录系统的合法性。这可以避免⽤户的密码被具有系统管理员权限的用户知道。
概念:
Base64是一种基于64个可打印字符来表示二进制数据的表示方法。Base64是一种编码方式。
Base64编码本质上是一种将二进制数据转成文本数据的方案。对于非二进制数据,是先将其转换成二进制形式,然后每连续6比特(2的6次方=64)计算其十进制值,根据该值在上面的索引表中找到对应的字符,最终得到一个文本字符串。
效果:
Base64编码的思想是是采用64个基本的ASCII码字符对数据进行重新编码。
采用Base64编码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。
应用:
Base64主要用于将二进制数据转换为文本数据,方便使用HTTP协议等,是可逆的。处理文本数据的场合,表示、传输、存储一些二进制数据。包括MIME的email,email via MIME,在XML中存储复杂数据.
注意:base64的主要作用不是加密,而是用来避免“字节”中不能转换成可显示字符的数值。项目中有用到的地方是当你支付完成之后,返回的用户信息是进行BASE64加密之后的字符串,你需要进行Base64解密,然后把转换成的二进制文件再转换成NSString
##1.Socket相关
Socket实现的一个简单群聊功能
概念:
1.网络上两个程序通过双向通信链接实现数据交换,这个链接的其中一端就是socket
2.Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议
3.在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原意那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务
通信要素:
1.网络上的请求就是通过Socket来建立链接然后相互通信
2.IP地址(网络上主机设备的唯一标示,寻找主机)
3.端口号(定位程序)
例如我有一个服务器(百度的 192.168.10.11)里面有个
Web的应用程序:80(提供HTTP服务)
MySQL:3306/SQLServer数据库应用程序
###概括下
TCP 是传输控制协议,面向连接,传输数据流。是TCP/IP协议的传输层协议。
socket本质是(API),对TCP/IP的封装,可供程序员做网络通讯开发是所调用。
http 是基于TCP面向互联网的请求响应模型的一种协议。HTTP协议:—> 网络上数据传输的约束,服务器才知道提交的数据,和XMPP一样,只是应用层的数据传输协议,规定了以何种数据进行传输
##2.TCP和UDP
4.传输协议(用什么方式进行交互)
TCP,UDP —>交互方式 前者需要建立链接,后者不需要
概念:
TCP(传输控制协议,HTTP的交互方式就是TCP交互方式,需要建立连接)
UDP (用户数据报协议,无连接,不可靠的网络协议,用于多播,广播,例如上课同步直播)
而UDP不是面向连接的,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,当然也不用重发,所以说UDP是无连接的、不可靠的一种数据传输协议。
2、也正由于1所说的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。
知道了TCP和UDP的区别,就不难理解为何采用TCP传输协议的MSN比采用UDP的QQ传输文件慢了,但并不能说QQ的通信是不安全的,
因为程序员可以手动对UDP的数据收发进行验证,比如发送方对每个数据包进行编号然后由接收方进行验证啊什么的,
即使是这样,UDP因为在底层协议的封装上没有采用类似TCP的“三次握手”而实现了TCP所无法达到的传输效率。
##3.HTTP
1.HTTP构建于TCP/IP协议之上,默认端口80
2.HTTP是无连接无状态
HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。
HTTP协议是以ASCII码传输
method request-URL version状态行
headers 请求头
entity-body 请求体
200 OK 客户端请求成功
301 Moved Permanently 请求永久重定向
302 Moved Temporarily 请求临时重定向
304 Not Modified 文件未修改,可以直接使用缓存的文件。
400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解。
401 Unauthorized 请求未经授权。这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因
404 Not Found 请求的资源不存在,例如,输入了错误的URL
500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求。
503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。
HTTP协议采用的是请求-应答的模式,当使用普通模式,非Keep-Alive模式,每个请求和应答客户端和服务器都要
重新建立一个连接,完成之后立即断开,HTTP1.1版本中,Keep-Alive功能使客户端到服务端的链接持续有效,当出现服务器后续请求时,Keep-Alive功能避免了建立或者重新建立链接
HTTP Keep-Alive
##4.会话跟踪
1.什么是会话?
客户端打开与服务器的链接发出请求到服务器响应客户端请求的全过程称之为会话 session
2.什么是会话跟踪?
对同一个用户对服务器的连续请求和接受响应的监视
3.为什么需要会话跟踪??
浏览器和服务器之间的通信是通过HTTP协议进行通信的,无状态的,不能保存用户信息,响应一次完成之后就断开了
下一次请求需要重新连接,这样就需要判断是否是同一个用户,才会有会话跟踪
方法1:URL重写
URL结尾添加一个附加数据以表示该会话,会把会话ID通过URL信息传递过去,来识别用户
方法2:Cookie 可以被禁止
Cookie是Web服务器发送给客户端的一小段信息,客户端请求时可以读取该信息发送到服务器,进而进行用户的识别
对于客户端的每次请求,服务器都会把Cookie发送到客户端,在客户端进行保存,以便下次使用
客户端有两种形式保存Cookie
方法3:Session
每个用户都有一个不同的session,各个用户之间不能共享,每个用户独享,在session中存放信息
在服务器创建一个session对象,产生一个sessionID来表示这个session对象,然后将sessionID放到Cookie中发送到客户端,下一次访问时,sessionID会发送到服务器,在服务器进行识别不同的用户
Session依赖于Cookie,Cookie被禁止,那么Session也失效了
##4.1 Session和Cookie
Cookie和Session
Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式
session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中,与此相对的,cookie需要将所有信息都保存在客户端。
##5.这几个之间的关系
在网络分层中,HTTP协议是基于TCP协议的,客户端向服务端发送一个HTTP请求时,需要先与服务端建立TCP连接,也就是经典的三次握手(通常对用户来说是很难察觉的),握手成功以后才能进行数据交互。HTTP是基于请求响应模式且无状态的协议,1.1之前只支持短连接,也就是请求响应一次以后连接中断,下次请求需要重新进行TCP连接,而1.1之后支持持长连接,即进行一次TCP连接以后,客户端可以发送多次的HTTP请求给服务器端。
小结:HTTP基于TCP
Socket是应用层与传输层之间的同一个抽象层,它是一套接口,所以Socket连接可以基于TCP连接,也有可能基于UDP。我们知道,TCP协议是可靠的,UDP协议是不可靠的,那么基于TCP协议的Socket连接同样是可靠的;基于UDP协议的Socket连接是不可靠的,QQ就是基于主要UDP辅助TCP实现的
登陆采用TCP协议和HTTP协议,你和好友之间发送消息,主要采用UDP协议,内网传文件采用了P2P技术。总来的说:
1.登陆过程,客户端client 采用TCP协议向服务器server发送信息,HTTP协议下载信息。登陆之后,会有一个TCP连接来保持在线状态。
2.和好友发消息,客户端client采用UDP协议,但是需要通过服务器转发。腾讯为了确保传输消息的可靠,采用上层协议来保证可靠传输。如果消息发送失败,客户端会提示消息发送失败,并可重新发送。
3.如果是在内网里面的两个客户端传文件,QQ采用的是P2P技术,不需要服务器中转
之所以会发生在客户端明明看到“消息发送失败”但对方又收到了这个消息的情况,就是因为客户端发出的消息服务器已经收到并转发成功,但客户端由于网络原因没有收到服务器的应答包引起的。
更恰当的方式应该是:两种通信协议同时使用,各有侧重。UDP用于保持大量终端的在线与控制,应用与业务则通过TCP去实现。
早期的时候,QQ还是主要使用TCP协议,而后来就转向了采用UDP的方式来保持在线,TCP的方式来上传和下载数据。
小结:Socket可基于TCP,亦可UDP
HTTP和TCP UDP一样,其实都是基于Socket的,Socket只是操作系统提供给网络应用程序之间进行通信的抽象API,他不决定是否促成长连接还是短连接,Socket确实是HTTP连接的一部分,而且是基于TCP的上层协议,但他确实短连接,1.1之后,有keep-alive属性进行辅助维持短暂的长连接,复用TCP,避免短时间内的多次三次握手和四次挥手
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。
理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。
断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)
建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
1、服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
2、客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。
为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
3、连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。
而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
##6.HTTP发出网络请求全部详细过程描述
从这里摘录
###1.首先输入或者app发出请求
###2.浏览器根据郁闷解析IP地址DNS查找过程:
1)浏览器缓存:浏览器会缓存DNS记录一段时间。 但操作系统没有告诉浏览器储存DNS记录的时间,这样不同浏览器会储存个自固定的一个时间(2分钟到30分钟不等)。
2)系统缓存:如果在浏览器缓存里没有找到需要的域名,浏览器会做一个系统调用(windows里是gethostbyname),这样便可获得系统缓存中的记录。
3)路由器缓存:如果系统缓存也没找到需要的域名,则会向路由器发送查询请求,它一般会有自己的DNS缓存。
4)ISP DNS缓存:如果依然没找到需要的域名,则最后要查的就是ISP缓存DNS的服务器。在这里一般都能找到相应的缓存记录。
###3.客户端和服务端通过三次握手建立TCP连接
###4.客户端给服务端发送一个http请求
###5.服务器永久重定向响应
服务器给浏览器响应一个301永久重定向响应,这样浏览器就会访问“http://www.facebook.com/” 而非“http://facebook.com/”。为什么服务器一定要重定向而不是直接发送用户想看的网页内容呢?其中一个原因跟搜索引擎排名有关。如果一个页面有两个地址,就像http://www.igoro.com/和http://igoro.com/,搜索引擎会认为它们是两个网站,结果造成每个搜索链接都减少从而降低排名。而搜索引擎知道301永久重定向是什么意思,这样就会把访问带www的和不带www的地址归到同一个网站排名下。还有就是用不同的地址会造成缓存友好性变差,当一个页面有好几个名字时,它可能会在缓存里出现好几次。
###6.浏览器跟踪重定向
现在浏览器知道了 “HTTP://www.facebook.com/”才是要访问的正确地址,所以它会发送另一个http请求。
###7.服务器处理请求
服务器接收到获取请求,然后处理并返回一个响应。这表面上看起来是一个顺向的任务,但其实这中间发生了很多有意思的东西,就像作者博客这样简单的网站,何况像facebook那样访问量大的网站呢!web服务器软件(像IIS和阿帕奇)接收到HTTP请求,然后确定执行某一请求处理来处理它。请求处理就是一个能够读懂请求并且能生成HTML来进行响应的程序(像ASP.NET,PHP,RUBY…)
若connection 模式为close,则服务器主动关闭TCP 连接,客户端被动关闭连接,释放TCP 连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
##7.为什么要启用HTTPS,为什么说他安全
###1)首先为什么说HTTP是不安全的 对称加密 AES
http协议属于明文传输协议,交互过程以及数据传输都没有进行加密,通信双方也没有进行任何认证,通信过程非常容易遭遇劫持、监听、篡改,严重情况下,会造成恶意的流量劫持等问题,甚至造成个人隐私泄露(比如银行卡卡号和密码泄露)等严重的安全问题。
可以把http通信比喻成寄送信件一样,A给B寄信,信件在寄送过程中,会经过很多的邮递员之手,他们可以拆开信读取里面的内容(因为http是明文传输的)。A的信件里面的任何内容(包括各类账号和密码)都会被轻易窃取。除此之外,邮递员们还可以伪造或者修改信件的内容,导致B接收到的信件内容是假的。
一般是采用一种叫做 AES 的算法来解决的。这种算法需要一个 密钥 key 来加密整个信息,加密和解密所需要使用的 key 是一样的,所以这种加密一般也被称为“对称加密”。AES 在数学上保证了,只要你使用的 key 足够足够足够足够的长,破解是几乎不可能的。
我们再回到这个教室,你接着要传小纸条,你把地址写上后,把要传输的内容用 AES 蹭蹭蹭加密了起来。刚准备传,问题来了。AES 不是有一个 key 吗?key 怎么给目的地啊?如果我把密钥直接写在纸条上,那么中间的人不依然可以解密吗?在现实中你可以通过一些其它方法来把密钥安全传输给目的地而不被其他人看见,但是在互联网上,要想这么做难度就很大了,毕竟传输终究要经过这些路由,所以要做加密,还得找一个更复杂的数学方法。
现在利用这种非对称加密的方法,我们来设想一个场景。你继续想要传纸条,但是传纸条之前你先准备把接下来通讯的对称加密密钥给传输过去。于是你用 RSA 技术生成了一对 k1、k2,你把 k1 用明文发送了出去,路经有人或许会截取,但是没有用,k1 加密的数据需要用 k2 才能解密。而此时,k2 在你自己的手里。k1 送达目的地后,目的地的人会去准备一个接下来用于对称加密传输的密钥 key,然后用收到的 k1 把 key 加密了,把加密好的数据传回来。路上的人就算截取到了,也解密不出 key。等到了你自己手上,你用手上的 k2 把用 k1 加密的 key 解出来,现在全教室就只有你和你的目的地拥有 key,你们就可以用 AES 算法进行对称加密的传输啦!这时候你和目的地的通讯将无法再被任何人窃听!
要解决http带来的问题,就要引入加密以及身份验证机制。
我们引入了非对称加解密的概念。在非对称加解密算法里,公钥加密的数据,有且只有唯一的私钥才能够解密
当建立SSL链接的时候,服务器把公钥用明文的形式传递给客户端,客户端和服务端可以用公钥私钥进行对称加密用的密钥进行数据的解密,从而保证安全性,但是还是有中间人攻击,具体的逻辑可以参考下面的文章
详细介绍
当然,这时候你可能会问两个问题。
既然 非对称加密 可以那么安全,为什么我们不直接用它来加密信息,而是去加密 对称加密 的密钥呢?
这是因为 非对称加密 的密码对生成和加密的消耗时间比较长,为了节省双方的计算时间,通常只用它来交换密钥,而非直接用来传输数据。然后再用非对称加密解出来的密码进行对称加密的解密
因此加入了CA证书来保证一定是指定服务器的交互
我们引入了数字证书的概念。服务器首先生成公私钥,将公钥提供给相关机构(CA),CA将公钥放入数字证书并将数字证书颁布给服务器,此时服务器就不是简单的把公钥给客户端,而是给客户端一个数字证书,数字证书中加入了一些数字签名的机制,保证了数字证书一定是服务器给客户端的。中间人发送的伪造证书,不能够获得CA的认证,此时,客户端和服务器就知道通信被劫持了。
非对称加密算法(公钥和私钥)交换对称密钥+数字证书验证身份(验证公钥是否是伪造的)+利用对称密钥加解密后续传输的数据=安全
两帧率之间没有算完,就出现了跳帧,因此需要进行性能优化
1.网络加载数据的时候提前计算好,在model里面多一个字段用来存储高度,cell加载的时候直接获取
2.[UIImage imageName:]; 通过一个单例类来根据url存储UIImage,当你第一次取的时候木有,直接IO获取,有的话直接从内存获取
3.文本的绘制会有一定的性能消耗,牛B的话直接用core text来做
4.iOS视图性能优化
5.性能测试之异步切圆角 通过模拟器查看UI DEBUG下的blended layer 是否出现红色,是的话就要优化,UILabel除外,异步绘制圆角
6.代码性能测试,通过单元测试里面的textPerform来测试时间 instruments的timePtofile和lead来测试
7.对于一些处理完之后没什么用的类可以用AutoreleasePool来进行及时处理
8.cell行高的缓存
9.不进行UI交互的控件能用CAlayer就用CAlayer
10.对象销毁在后台执行
NSArray*tem = self.array;
self.array = nil;
dispatch_async(queue,^{
[tem class];
捕获到对象在后台销毁
});
11.图片尽量别有透明的,不然增加GPU计算的损耗
12.不要动态创建子视图,要预先创建好,如果不需要显示就先hidden
13.所有的cell子视图都必须指定背景颜色,颜色别用alpha
14.异步绘制
self.layer.drawAsynchronously = YES;
15.栅格化,cell所有内容,生成一张独立的图像,屏幕滚动式只显示图像
必须指定分辨率 默认使用 x1生成图像
self.layer.shouldRasterize = YES;
self.layer.rasterzationScale = [UIScreen mainScreen].scale
这里和异步coretext绘制一个道理,最终绘制上去的控件用Image的方式绘制上cell展示
如果需要将当前视图的子视图栅格化,也就是将它的全部子视图与当前视图压缩成一个图层
16.重用大开销对象
一些objects的初始化很慢,比如NSDateFormatter和NSCalendar
可以添加到Category里面或者用单例/静态变量来实现 空间换时间
其实设置NSDateFormatter的速度差不多是和创建新的一样慢,因此,懒加载就可以
// in your .h or inside a class extension
@property (nonatomic, strong) NSDateFormatter *formatter;
// inside the implementation (.m)
// When you need, just use self.formatter
- (NSDateFormatter *)formatter {
if(!_formatter) {
_formatter = [[NSDateFormatter alloc] init];
_formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";// twitter date format
}
return _formatter;
}
设置一个NSDateFormatter
17.AutoreleasePool
如果循环很多次,你会发现你的内存一直在被吃掉,这个时候你可以在每次循环的时候用
@autorelease进行临时创建的释放
18.选择性缓存图片
第一种imageNamed 会缓存图片
第二种imageWithContentOfFile 仅加载图片
如果是大图片,一次性的,第二种就很优秀,如果反复加载的小图,需要缓存,那么久用第一种
19.读取图片的二进制文件的时候系统默认会先解压缩,因此如果非常卡的话需要强制解压缩在异步线程中,不然会卡主主线程
SDWebDecoder已经异步做了解压缩
20.异步绘制以及按需加载 按需加载cell,cell滚动很快时,只加载范围内的cell
按需加载优化tableView
21.使用局部更新(reloadSection进行局部更新)
22.不要实现无用的代理方法,tableView只遵守两个协议
23.不要做过多的绘制工作 在实现drawRect:的时候,它的rect参数就是需要绘制的区域,这个区域之外的不需要进行绘制。
例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否需要绘制image和text,然后再调用绘制方法。
24.预渲染图像 当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕;
25.多线程访问者锁的使用会影响性能,自旋锁虽然消耗CPU忙等,但是性能优于互斥锁
26.排版 绘制 UI操作,前两个全部放在异步线程操作,等Runloop监听到休眠时再统一绘制操作在主线程
ASDK原理
27.异步排版和渲染,Runtime周期刷新到主线程
27.inline函数,inline
28.用FaceBook底层的yoga来操作frame,不用Autolayout
1)dyld 开始将程序二进制文件初始化 (dyld(the dynamic link editor),Apple 的动态链接器,系统 kernel 做好启动程序的初始准备后,交给 dyld 负责)
2)交由ImageLoader 读取 image,其中包含了我们的类,方法等各种符号(Class、Protocol 、Selector、 IMP)
3)由于runtime 向dyld 绑定了回调,当image加载到内存后,dyld会通知runtime进行处理
4)runtime 接手后调用map_images做解析和处理
5)接下来load_images 中调用call_load_methods方法,遍历所有加载进来的Class,按继承层次依次调用Class的+load和其他Category的+load方法
6)至此,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,再这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)
7)最后dyld调用真正的main函数
注意:dyld会缓存上一次把信息加载内存的缓存,所以第二次比第一次启动快一点
1.整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存, 动态链接依赖库加载进内存,并由 runtime 负责加载成 objc 定义的结构,安层级调用Class的load方法,所有初始化工作结束后,dyld 调用真正的 main 函数。创建型:单利(单态)和 抽象工厂(Class方法 方便继承维护代码,父类暴露接口,各自子类的实现,子类各自形态表现不同),迭代器模式(for in)
结构型:MVC,MVVM,MVP,Category(装饰模式),门面设计模式(KVO,封装)
行为型:观察者(KVO,Notification),Delegate(代理模式)
工厂设计模式的简单示例
#import
@interface Animation : NSObject
- (void)startAnimate;
- (void)run;
@end
#import "Animation.h"
@implementation Animation
- (void)startAnimate{
NSLog(@"父类%s",__func__);
[self run];
}
- (void)run{
NSLog(@"父类%s",__func__);
}
@end
#import "Animation.h"
@interface Dog : Animation
@end
#import "Dog.h"
@implementation Dog
- (void)run{
NSLog(@"子类狗%s",__func__);
}
@end
参考1
参考2
“并发”指的是程序的结构,“并行”指的是程序运行时的状态
并发是能力,并行是状态
并行指物理上同时执行,并发指能够让多个任务在逻辑上交织执行的程序设计(cpu时间片轮转优先级调度)
instanceType最重要的作用是那些非关联返回类型的方法返回所在类的类型。
(1)id在编译的时候不能判断对象的真实类型
instancetype在编译的时候可以判断对象的真实类型
(2)id可以用来定义变量, 可以作为返回值, 可以作为形参
instancetype只能用于作为返回值
其实都是全局变量,相对于都是Static,只能在本文件使用,而前者就是能在所有文件使用。
只是FOUNDATION_EXPORT是用来兼容C++的
如果没有C++ 就可以用 FOUNDATION_EXTERN
如果你用extern也没问题 所以兼容性好的话用 FOUNDATION_EXPORT即可
Block C语言的闭包,带有局部变量的匿名函数,Block整体代码表现形式就是一个结构体,结构体内部可以捕获外部变量的值,如果加了__block修饰(变量就是对象,无论什么类型),也可以捕获外部变量整个结构体指针,而Block任务块就是内部C函数的实现,而Block结构体内部就会引用这个匿名函数,调用实际就是调用结构体内的函数指针。Block有全局,堆和栈三种,ARC下基本都是帮我们赋值到堆上保存,这也就是为什么能捕获变量,引用变量,Block存在让变量无法释放的原因,Block就是对象
Block和Delegate都可以通知外面。
Block更轻,使用更简单,能够直接访问上下文,调用和声明都在同一个地方,代码连贯;Delegate更重一些,需要实现接口,它的好处就是方法分离开来,没有Block连贯。
Block是后面才有的,个人感觉应该优先使用Block。以下两种情况可以用Delegate
1.有多个相关方法,比如一个网络请求,只有成功和失败,详情参见AFN上层封装,但是底层网络NSURLSession,会有很多情况回调,比如失败,成功,异常,进度,回调处理一系列切点方法,就应该优先采用Delegate
2.为了避免循环引用,也可以使用Delegate,使用Block的时候稍微不注意就会引起循环引用,导致对象释放不了,而delegate是分离开的,并不会引用上下文,因此更加安全
如果自己设计一个库,安全和方便使用,宁可麻烦点,使用Delegate更加友好一点。
将 block 简单分类,有三种情形。