setNeedsLayout、layoutIfNeeded、layoutSubviews
setNeedsLayout等待下一个周期更新视图(一般下一个周期会马上到来,runloop刷新机制)
layoutIfNeeded立即更新。
set就是性能更好一些
使用在:自定义视图大小更改之后相对于子视图的边界已经失效了,在重新了layoutsubview情况下需要更新一下子视图的约束
- layoutSubviews:延迟调用,布局子视图
注:layoutSubviews:改变x,y时不调用,改变width和height时调用
- setNeedsLayout
标记为需要重新布局,异步调用layoutIfNeeded
刷新布局,不立即刷新,在下一轮runloop结束前刷新,对于这一轮runloop
之内的所有布局和UI上的更新只会刷新一次,layoutSubviews
一定会被调用。 - layoutIfNeeded
如果有需要刷新的标记,立即调用layoutSubviews
进行布局(如果没有标记,不会调用layoutSubviews
)。
layoutIfNeeded不一定会调用layoutSubviews方法。
setNeedsLayout一定会调用layoutSubviews方法(有延迟,在下一轮runloop结束前)。
如果想在当前runloop中立即刷新,调用顺序应该是
[self setNeedsLayout];
[self layoutIfNeeded];
Weak的底层实现原理?
Runtime会维护一个weak表,用于维护指向对象的所有weak指针。weak表是一个哈希表,其key为所指对象的指针,value为weak指针的地址数组。
具体过程如下:
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数,更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
CALayer和UIView
UIView是iOS系统中界面元素的基础,所有的界面元素都是继承自它。它本身完全是由Core Animation来实现的。它真正的绘图部分,是由一个CALayer类来管理。UIView本身更像是一个CALayer的管理器,访问它的跟绘图和跟坐标有关的属性,例如frame,bounds等,实际上内部都是在访问它所包含的CALayer的相关属性
- 首先UIView可以响应事件,Layer不可以
- View中frame getter方法,bounds和center,UIView并没有做什么工作;它只是简单的各自调用它底层的CALayer的frame,bounds和position方法。
- Layer 比 View 多了个AnchorPoint 在 View显示的时候
沙盒
所有的非代码文件都要保存在此,例如属性文件plist、文本文件、图像、图标、媒体资源等
沙盒中相关路径
-
AppName.app
应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以不能在运行时对这个目录中的内容进行修改,否则会导致应用程序无法启动。 -
Documents/
保存应用程序的重要数据文件和用户数据文件等。用户数据基本上都放在这个位置(例如从网上下载的图片或音乐文件),该文件夹在应用程序更新时会自动备份,在连接iTunes时也可以自动同步备份其中的数据。 -
Library:
这个目录下有两个子目录,可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份.Library/Caches:
保存应用程序使用时产生的支持文件和缓存文件(保存应用程序再次启动过程中需要的信息),还有日志文件最好也放在这个目录。iTunes 同步时不会备份该目录并且可能被其他工具清理掉其中的数据。
Library/Preferences:
保存应用程序的偏好设置文件。NSUserDefaults类创建的数据和plist文件都放在这里。会被iTunes备份。 -
tmp/:
保存应用运行时所需要的临时数据。不会被iTunes备份。iPhone重启时,会被清空。
解档归档
如果模型很复杂,解析不方便,或者不利于存储数据库,那么归档则是个不错的方式。归档后,模型会以NSDate类型被写进文件中;解档后,这个模型又会被读取出来
实现 归解档
-
首先,自定义类要遵循协议 <NSCoding>
-
实现自定义类中归档方法:
-(void)encodeWithCoder:(NSCoder *)aCoder;
- 实现自定义类中解档方法:
- (instancetype )initWithCoder:(NSCoder *)aDecoder;
如果自定义模型里面有很多个属性,那么归解档方法里面是不是就会疯狂的写
[aCoderencodeObject:obj forKey:key] 或者 [aDecoder decodeObjectForKey:key]
方法;
所以最简单的实现就是通过运行时取获取当前类里面所有的属性,循环遍历类中所有的属性,并且 根据属性名和属性值依次调用 归解档方法;这里在对属性的赋值与取值时是用的kvc,间接调用可以避免 基础数据类型 不能通过反射机制调用set或者get方法;
KVC
KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的。
KVC的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject的类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的,因为没有继承NSObject),下面是KVC最为重要的四个方法:
WeChat530785d1da608c9eed59b19e77a56bda.pngKVO
KVO
的实现依赖于 Objective-C
强大的 Runtime
,当观察某对象 A
时,KVO
机制动态创建一个对象A
当前类的子类,并为这个新的子类重写了被观察属性 keyPath
的 setter
方法。setter
方法随后负责通知观察对象属性的改变状况。
Apple
使用了 isa-swizzling
来实现 KVO
。当观察对象A
时,KVO
机制动态创建一个新的名为:NSKVONotifying_A
的新类,该类继承自对象A
的本类,且KVO
为NSKVONotifying_A
重写观察属性的 setter
方法,setter
方法会负责在调用原 setter
方法之前和之后,通知所有观察对象属性值的更改情况
- 子类setter方法剖析
KVO
的键值观察通知依赖于NSObject
的两个方法:willChangeValueForKey:
和didChangeValueForKey:
,在存取数值的前后分别调用 2 个方法:
被观察属性发生改变之前,willChangeValueForKey:
被调用,通知系统该keyPath
的属性值即将变更;
当改变发生后,didChangeValueForKey:
被调用,通知系统该keyPath
的属性值已经变更;之后,observeValueForKey:ofObject:change:context:
也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
- (void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; //KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO 在调用存取方法之后总调用
}
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
每当监听的keyPath发生变化了,就会在这个函数中回调。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
单例
1.单例的优点(主要优点)
单例可以保证系统中该类有且仅有一个实例,所以很便于外界访问.
因为其上面的特点,对于项目中的个别场景的传值,存储状态等等更加方便
2.单例的缺点(主要缺点)
单例实例一旦创建,对象指针是保存在静态区的,那么在堆区分配空间只有在应用程序终止后才会被释放
-
对于一个实例,我们一般并不能保证他一定会在单线程模式下使用,所以我们得适配多线程情况。在多线程情况下,上面的单例创建方式可能会出现问题。如果两个线程同时调用shareInstance,可能会创建出2个single来。所以对于多线程情况下,我们需要使用@synchronized来加锁。
@implementationSingleton + (instancetype)shareInstance{ staticSingleton* single; @synchronized(self){ if(!single) { single = [[Singleton alloc] init]; } } return single; } @end
-
dispatch_once单例
使用@synchronized虽然解决了多线程的问题,但是并不完美。因为只有在single未创建时,我们加锁才是有必要的。如果single已经创建.这时候锁不仅没有好处,而且还会影响到程序执行的性能(多个线程执行@synchronized中的代码时,只有一个线程执行,其他线程需要等待)。那么有没有方法既可以解决问题,又不影响性能呢?
这个方法就是GCD中的dispatch_once+ (instancetype)shareInstance{ static Singleton* single; static dispatch_once_t onceToken; //①onceToken = 0; dispatch_once(&onceToken, ^{ NSLog(@"%ld",onceToken); //②onceToken = 140734731430192 single = [[Singleton alloc] init]; }); NSLog(@"%ld",onceToken); //③onceToken = -1; return single; } }
打印结果如下:
140734605830464 -1
-
dispatch_once为什么能做到既解决同步多线程问题又不影响性能呢?
下面我们来看看dispatch_once的原理:
dispatch_once主要是根据onceToken的值来决定怎么去执行代码。
当onceToken= 0时,线程执行dispatch_once的block中代码
当onceToken= -1时,线程跳过dispatch_once的block中代码不执行
当onceToken为其他值时,线程被线程被阻塞,等待onceToken值改变
当线程首先调用shareInstance,某一线程要执行block中的代码时,首先需要改变onceToken的值,再去执行block中的代码。这里onceToken的值变为了140734605830464。
这样当其他线程再获取onceToken的值时,值已经变为140734605830464。其他线程被阻塞。
当block线程执行完block之后。onceToken变为-1。其他线程不再阻塞,跳过block。
下次再调用shareInstance时,block已经为-1。直接跳过block。
这样dispatch_once在首次调用时同步阻塞线程,生成单例之后,不再阻塞线程。dispatch_once是创建单例的最优方案 -
总结:
单例模式是一个很好的设计模式,他就像一个全局变量一样,可以让我们在任何地方都使用同一个实例。
如果要自己创建单例模式,最好使用dispatch_once方法,这样即可解决多线程问题,又能达到高效的目的
GCD信号量(dispatch_semaphore_t)
dispatch_semaphore_create(long value); // 创建信号量
dispatch_semaphore_signal(dispatch_semaphore_t deem); // 发送信号量
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等待信号量
dispatch_semaphore_create(long value);
和GCD的group等用法一致,这个函数是创建一个dispatch_semaphore_类型的信号量,并且创建的时候需要指定信号量的大小。
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
等待信号量。该函数会对信号量进行减1操作。如果减1后信号量小于0(即减1前信号量值为0),那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。
dispatch_semaphore_signal(dispatch_semaphore_t deem);
发送信号量。该函数会对信号量的值进行加1操作。
一般使用信号量+异步组
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_group_t grp = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(grp, queue, ^{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSLog(@"task1 begin : %@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"task1 finish : %@",[NSThread currentThread]);
dispatch_semaphore_signal(sema);
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
});
dispatch_group_async(grp, queue, ^{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSLog(@"task2 begin : %@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"task2 finish : %@",[NSThread currentThread]);
dispatch_semaphore_signal(sema);
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
});
dispatch_group_notify(grp, dispatch_get_main_queue(), ^{
NSLog(@"refresh UI");
});
}
执行结果
image.png
这样就能在异步的组线程分别执行的异步线程都结束之后再执行操作
NSOperation
Operation:
- 我们使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,
或者自定义子类来封装操作。
Operation Queues:
- 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。等于1就是串行,大于1就是并行,创建多少的线程由系统决定
- NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。
使用时,首先创建操作,然后创建队列,将操作添加到队列中
[queue addOperationWithBlock:^{具体实现}];
NSOperation 操作依赖
-
- (void)addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作 op 的完成。 -
- (void)removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作 op 的依赖。 -
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
在当前操作开始执行之前完成执行的所有操作对象数组。
NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。
NSOperation、NSOperationQueue 线程安全通过加锁的方式
点击事件传递及响应
当点击了屏幕会产生一个触摸事件,消息循环(runloop)会接收到触摸事件放到消息队列里,UIApplication 会从消息队列里取事件分发下去,接着需要找到去响应这个事件的最佳视图,也就是 Responder,所以开始的第一步应该是找到 Responder
WechatIMG7.png
- 事件的传递是从上到下(父控件到子控件),UIApplication->UIWindow->UIView
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
该方法判断触摸点是否在控件身上, 是则返回YES, 否则返回NO - 只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法寻找合适的View
注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,
事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
- 事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件)
Masonry布局之后获取frame
Masonry原理:
首先你要知道autolayout和frame的关系,autolayout最终也是转成frame,masonry是建立在autolayout之上的。你没获取到正确的值,那是因为约束还没布局完成。相当于就是我们给一定的约束,系统内部自己去根据约束条件转成对应的frame,而这需要一个过程。想要拿到正确的frame最好的就是让autolayout完成之后,什么时候完成呢?那就是在layoutsubviews for view or didlayoutsubviews for controller 里获取,当然在控制器的viewdidappear里也拿得到,但是正确做法和最佳做法还是在控制器里的viewdidlayout里获取最好~因为autolayout会根据约束,不停的去改变frame,这方法里最后拿到的frame就是最终姿势.
[self.view layoutIfNeeded];
就可以获取frame
NSTimer的循环引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self
selector:@selector(runTest) userInfo:nil repeats:YES];
此方法会造成循环引用,因为:
- ViewController 有一个强引用引用着定时器
- 定时器会对target产生一个强引用
- 产生了一个循环引用,两个都无法释放了
解决办法1:iOS10提供的Block的形式的定时器
解决方法2:使用一个中间介来做消息转发
内存
- Autoreleasepool 参考
其中对象s会被加入到自动释放池,当ARC下代码执行到右大括号时(相当于MRC执行代码[pool drain]
)会对池中所有对象依次执行一次release
操作
- RunLoop释放池
有没有想过我们直接调用autorelease方法就可以把释放对象的任务交给Autoreleasepool对象,Autoreleasepool对象从哪里来?Autoreleasepool对象又会在何时调用[pool drain]方法?
要解答以上两个问题,首先要了解NSRunLoop
NSRunLoop : App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
对于每一个Runloop运行循环,系统会隐式创建一个Autoreleasepool
对象,+ (instancetype)student中执行autorelease
的操作就会将具体创建的对象添加到这个系统隐式创建的
Autoreleasepool
对象中——这回答了Autoreleasepool
对象从哪里来
当Runloop执行完一系列动作没有更多事情要它做时,它会进入休眠状态,避免一直占用大量系统资源,或者Runloop要退出时会触发执行_objc_autoreleasePoolPop()
方法相当于让Autoreleasepool
对象执行一次drain
方法,Autoreleasepool
对象会对自动释放池中所有的对象依次执行依次release
操作——这回答了Autoreleasepool
对象又会在何时调用[pool drain]
方法
- 子线程上的Autorelease
子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?
在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage ),并调用
page->add(obj)
将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!
- 局部释放池的应用
for (int i = 0; i < largeNumber; i++) {
NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
str = [str stringByAppendingString:@" - world"];
}
/**
[NSString stringWithFormat:@"hello -%04d", i]方法创建的对象会加入到自动释放池里,
对象的释放权交给了RunLoop 的释放池
RunLoop 的释放池会等待Runloop即将进入睡眠或者即将退出的时候释放一次
for循环中线程一直在做事情,Runloop不会进入睡眠
*/
解决办法,每次循环时都手动创建一个局部释放池,及时创建,及时释放,这样NSString对象就会及时得到释放
for (int i = 0; i < largeNumber; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
str = [str stringByAppendingString:@" - world"];
}
}
Runloop和Autorelease pool
对于每一个Runloop, 系统会隐式创建一个Autorelease pool,这样所有的release pool会构成一个象CallStack一样的一个栈式结构,在每一个Runloop结束时,当前栈顶的Autorelease pool会被销毁,这样这个pool里的每个Object会被release
mrc和arc
- mrc 人工引用计数
需要开发者手动释放对象,或者autorelease
延迟释放
自己生成的对象,自己持有
非自己生成的对象,自己也能持有
不需要自己持有的对象时释放
无法释放非自己持有的对象
- 为什么要进行内存管理呢
继承了NSObject的对象的存储在操作系统的堆里边。
操作系统的堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表
非OC对象一般放在操作系统的栈里面
操作系统的栈:由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进后出)
- autorelease的本质
autorelease实际上只是把对release的调用延迟了,对于每一个autorelease,系统只是把该对象放入了当前的autorelease pool中,当该pool被释放时,该pool中的所有对象会被调用release。
当对象的引用计数器为0时,对象占用的内存就会被系统回收`
- ARC 自动引用计数
以alloc/new/copy/mutableCopy
生成的对象,这种对象会被当前的变量所持有,引用计数会加1
不是用被持有的方式生成对象,比如{ id obj = [NSMutableArray array]; }
这种方式生成的对象不会被obj持有,通常情况下会被注册到autoreleasepool中.但也有特殊情况,上面的代码可以转换成如下代码
{
// 消息转发
id obj = objc_msgSend(NSMutableArray,@selector(array));
// 调用objc_retainAutoreleasedReturnValue函数
objc_retainAutoreleasedReturnValue(obj);
// 编译器在obj作用域结束时自动插入release
objc_release(obj);
}
weak
修饰符想必大家都非常熟悉,它有一个众所周知的特性:用weak修饰的对象在销毁后会被自动置为nil
.另外还补充一点:凡是用weak修饰过的对象,必定是注册到autoreleasepool中的对象.
{
// obj默认有__strong修饰
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
}
实际过程如下:
{
// 省略obj的实现
id obj1;
// 通过objc_initWeak初始化变量
objc_initWeak(&obj1,obj);
// 通过objc_destroyWeak释放变量
objc_destroyWeak(&obj1);
}
-
objc_initWeak()
函数的作用是将obj1初始化为0,然后将obj作为参数传递到这个函数中objc_storeWeak(&obj1,obj)
-
objc_destroyWeak()
函数则将0作为参数来调用:objc_storeWeak(&obj1,0)
-
objc_storeWeak()
函数的作用是以第二个参数(obj || 0)
作为key,第一个参数(&obj1)
作为value,将第一个参数的地址注册到weak表中.当key为0,即从weak表中删除变量地址.
那么weak表中的对象是如何被释放的呢?
- 从weak表中获取废弃对象的键值记录.
- 将记录中所有包含__weak的变量地址,赋值为nil.
- 从weak表中删除该记录.
- 从引用计数表中删除对应的记录.
这就是__weak修饰的变量会在释放后自动置为nil的原因.同时,因为weak修饰之后涉及到注册到weak表等相关操作,如果大量使用weak可能会造成不必要的CPU资源浪费,所以书里指出尽量在循环引用中使用weak.
如果只是用__weak声明一个变量
id __weak obj1 = obj;
{
id obj1;
objc_initWeak(&obj1,obj);
//从weak表中取出weak修饰的对象,所以tmp会对这个取出的对象进行一次强引用,为了让这个对象在当前变量作用域结束前都可以使用
id tmp = objc_loadWeakRetained(&obj1);
//将tmp注册到了`autoreleasepool `中
//所以当大量使用weak对象的时候,注册到autoreleasepool的对象会大量增加.解决方案就是用一个__strong修饰的临时变量来使用.
objc_autorelease(tmp);
objc_destroyWeak(&obj1);
}
- ARC,MRC区别
说了这么多总结一下ARC,MRC的区别吧。MRC类似于c++中的普通指针,程序猿要手动的进行生成持有释放废弃操作,但是可以将其注册到autoreleasepool中(类似c++的作用域),在作用域消失时也就是autoreleasepool对象释放时会释放所有注册在它里面的对象。而ARC则是类似于c++的智能指针,不需要显示的对对象实例进行释放,出现了strong,weak等修饰符,采用自动引用计数的方法,使得当一个对象实例在强引用计数为0时,则废弃这个对象实例,释放其所占的内存块。
runtime的消息机制(一个消息从调用到报错)
OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。
- Runtime消息传递
一个对象的方法像这样[obj foo]
,编译器转成消息发送objc_msgSend(obj, foo)
,Runtime时执行的流程是这样的:
- 首先,通过
obj
的isa
指针找到它的class
; - 在
class
的method list
找foo
; - 如果
class
中没到foo
,继续往它的superclass
中找 ; - 一旦找到
foo
这个函数,就去执行它的实现IMP
。
在找到
foo
之后,把foo
的method_name
作为key
,method_imp
作为value
给存起来。当再次收到foo
消息的时候,可以直接在cache
里找到,避免去遍历objc_method_list
。从前面的源代码可以看到objc_cache
是存在objc_class
结构体中的。
因为objc_method中的SEL(objc_selector)
selector
是方法选择器,只记了method
的name
没有参数,所以没法区分不同的method
。所以oc不可以进行方法的重载
-
Runtime消息转发
消息转发图示
- 动态方法解析(消息动态解析)
首先,Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数(
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
)实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。
如果resolve
方法返回NO
,运行时就会移到下一步:forwardingTargetForSelector
。
- 备用接受者(消息接受者重定向)
如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
- 完整消息转发(消息重定向)
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
首先它会发送-methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:
返回nil
,Runtime
则会发出-doesNotRecognizeSelector:
消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime
就会创建一个NSInvocation
对象并发送-forwardInvocation:
消息给目标对象。
- 为什么runtime在子类方法中找不到会去父类中找
当调用方法[person test],就是给person对象发送test消息,然后通过isa->superclass-> superclass......寻找类对象,找到类对象之后,还会先通过@selector(test)&_mask生成索引,通过索引直接在cache里的散列表里面取方法,如果cache里面没有方法,再遍历methods数组......
//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY
#if !__OBJC2__
Class super_class
...
为什么主线程更新ui
- UIKit是一个线程不安全的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度。
- 另一方面因为整个程序的起点UIApplication是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应。
- 因为整个程序的起点UIApplication是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应。而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上 同时 更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。
分类和类扩展区别
- Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
- Class Extension在编译的时候,它的数据就已经包含在类信息中
Category是在运行时,才会将数据合并到类信息中
分类为什么不能添加成员变量却可以添加属性
- 从结构体可以知道,有属性列表,所以分类可以声明属性,但是分类只会生成该属性对应的get和set的声明,没有去实现该方法。
- 结构体没有成员变量列表,所以不能声明成员变量。
Category的加载处理过程
- 1.通过Runtime加载某个类的所有Category数据
- 2.把所有Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面
- 3.将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
atomic 和nonatomic的区别
- 线程安全(默认):atomic修饰的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。
注:atomic只能保证set/get读写的安全(给set/get方法加锁),不能保证线程的安全
- 线程不安全:nonatomic修饰的属性,不做保持getter完整性保证,但在运行速度上要比atomic快
Block
- block本质上也是一个OC对象,它内部也有个isa指针
- block是封装了函数调用以及函数调用环境的OC对象
- block是封装函数及其上下文的OC对象
使用copy关键字是从栈中复制到堆中,以便进行对象的释放
self 和super区别
其实不管是self还是super真正调用的对象都是一样的,只是查找方法的位置不一样,self
是从当前类结构中开始查找,super
是从父类中查找,但方法真正的接受者都是当前类或者当前类的对象
UIViewController的声明周期
1、 alloc
创建对象,分配空间
2、init (initWithNibName|initWithCoder)
初始化对象,初始化数据
3、awakeFromNib
所有视图的outlet和action已经连接,但还没有被确定。
4、loadView
完成一些关键view的初始化工作,加载view。
5、viewDidLoad
载入完成,可以进行自定义数据以及动态创建其他控件
6、viewWillAppear
视图将出现在屏幕之前
7、viewWillLayoutSubviews
将要对子视图进行调整
8、viewDidLayoutSubviews
对子视图进行调整完毕
9、viewDidAppear
视图已在屏幕上渲染完成
10、viewWillDisappear
视图将被从屏幕上移除
11、viewDidDisappear
视图已经被从屏幕上移除
12、dealloc
视图被销毁,此处需要对你在init和viewDidLoad中创建的对象进行释放
13、didReceiveMemoryWarning
内存警告
runloop的几个mode和source
-
UITrackingRunLoopMode (UI模式,只能被UI事件触发 优先级非常高)
-
NSDefaultRunLoopMode (默认模式,处理时钟网络事件)
-
NSRunLoopCommonModes (占位模式,以上两种模式都添加)
-
App启动时第一个模式,runloop会处理启动事件,这个模式只运行一次(苹果不开放)
-
系统事件的内部模式(苹果不开放)
source(事件源source0:非内核事件由开发者产生如:点击屏幕触发事件,source1:系统内核事件由系统所产生. source0基于souce1,因为触摸屏幕等事件最终都会通知系统去处理) - observe(runloop的观察者) - timer(时钟事件)
swift高阶函数
Map,flatMap,filter,reduce
gcd栅栏函数
-
dispatch_barrier_sync
和dispatch_barrier_async
都可以实现需求; -
dispatch_barrier_async
只是改变了指定任务的执行顺序,但是并不阻塞线程;而dispatch_barrier_sync
不仅改变了任务的执行顺序,而且还会阻塞后续任务的执行,无论是放在那个队列中,后续任务需要等待同步栅栏函数返回才能执行.
load 和initialize
- load:
- +load方法是一定会在runtime中被调用的,只要类被添加到runtime中了,就会调用+load方法,即只要是在Compile Sources中出现的文件总是会被装载,与这个类是否被用到无关,因此+load方法总是在main函数之前调用。所以我们可以自己实现+laod方法来在这个时候执行一些行为。
- +load方法不会覆盖(因为由于+load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用)。
+[Person load]
+[Man load]
注:在main函数之前调用,不发送消息也会调用,因为load是根据方法地址调用,先调用父类然后子类然后分类中的load方法,分类中不分顺序
- initialize
+initialize方法会在类第一次接收到消息时调用。
调用顺序:先调用父类的+initialize,再调用子类的+initialize,每个类只会初始化1次
+[Person initialize]
+[Man initialize]
+initialize和+load的最大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点:
- 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
- 如果分类实现了+initialize,就覆盖类本身的+initialize调用
注:+initialize 在main函数之后调用,在第一次进行消息转发时调用,仅调用一次。走的是消息转发,所以如果分类中实现了会覆盖本类的方法,子类中未实现会调用父类中的方法,先调用父类的再子类
1、相同点
1).load和initialize会被自动调用,不能手动调用它们。
2).子类实现了load和initialize的话,会隐式调用父类的load和initialize方法。
3).load和initialize方法内部使用了锁,因此它们是线程安全的。
网友评论