学习笔记
1. Runtime
runtime
是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数,平时我们编写的OC代码,底层都是基于runtime
实现的runtime
有什么作用:
能动态产生一个类,一个成员变量,一个方法;能动态修改一个类,一个成员变量,一个方法;能动态删除一个类,一个成员变量,一个方法- 常用头文件:
#import <objc/runtime.h> //包含对类、成员变量、属性、方法的操作
#import <objc/message.h> //包含消息机制
- 常用方法
class_copyIvarList()//返回一个指向类的成员变量数组的指针
class_copyPropertyList()//返回一个指向类的属性数组的指针
runtime
在开发中的用途:
1. 动态的遍历一个类的所有成员变量,用于字典转模型,归档解档操作。例如:自动解析开源库MJExtension
就是用runtime
实现的
2. 交换方法:通过runtime
的方法,可以进行交换方法的实现;一般用自己写的方法(常用在自己写的框架中,添加某些防错措施)来替换系统的方法实现,常用的地方有:在数组中,越界访问程序会崩,可以用自己的方法添加判断防止程序出现崩溃数组或字典中不能添加nil
,如果添加程序会崩,用自己的方法替换系统防止系统崩溃
3. 关联对象:有一个问题:“如何给NSArray
添加一个属性(不能使用继承)”,不能用继承,难道用分类(Category)
,但是分类只能添加方法不能添加属性啊,这个时候就要用到runtime
了. 关联对象是指某个OC对象通过一个唯一的key连接到一个类的实例上。例如:xiaoming
是Person
类的一个实例,他的dog
(一个OC对象)通过一个绳子(key)被他牵着散步,这可以说xiaoming
和dog
是关联起来的,当然xiaoming
可以牵着多个dog
4. 关联KVO观察者:有时候我们在分类中使用KVO,推荐使用关联的对象作为观察者,尽量避免对象观察自身
5. 运行时动态创建一个类动态特性:
动态加载:动态加载就是根据需求加载所需要的资源。有一个典型的例子,就是iPhone 会根据机型的不同加载不同的图片(xxx.png、xxx@2x.png、xxx@3x.png)。
动态类型:即运行时再决定对象的类型。动态类型这个特性在日常开发中非常的常见,最简单的就是id类型。 动态类型有利有弊,有了动态类型,我们可以在运行时根据对象的类型不同执行不同的逻辑代码;但是也导致一些错误不能及时的发现。比如,我们经常会遇到的这类错误:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_NSZeroData count]: unrecognized selector sent to instance 0x7f8632ed7ab0'
这是错误的示例代码:
NSArray *data = [[NSData alloc] init];
if (data.count > 0) {
NSLog(@“%@”, data.count);
} // 这就是我们错误的将一个NSData 对象赋值给了NSArray 实例,然后又调用数组的count 方法。
动态绑定:基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性和响应的消息也被完全确定,这就是动态绑定。
刚开始这个实例对象就像白纸一样干净,不知道它的具体类型,也没有属性和方法。然后在动态类型阶段,确定它的实际类型。再经过动态绑定,才会为其绑定相应的属性和方法,这时候这个对象才算完整了。
2. RunLoop
RunLoop
:其实就是一个while
的死循环。一个线程一次只能执行一个任务,执行完成后线程就会退出。RunLoop
可以让线程能随时处理事件并不退出。让线程在没有处理消息时休眠以避免资源占用,在有消息到来时立刻被唤醒。- 要让
RunLoop
跑起来,必须要给其添加一个有内容的mode
, 而且必须要让他Run
。RunLoop
跑起来后相当于是一个while
的死循环,后面的代码不会执行。RunLoop
是在程序的入口main
函数中开启 RunLoop
作用:
1. 保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop
,RunLoop
保证主线程不会被销毁,也就保证了程序的持续运行
2. 处理APP中的各种事件,比如:触摸事件(手势、UIScrollView
的滑动)、定时器事件、selector
事件
3. 节省CPU资源,提高程序性能。程序运行起来时,当什么操作都没有做的时候,RunLoop
会进入休眠状态,当有事情做的时候RunLoop
会被唤起RunLoop
和线程的关系:
1. 每条线程都有唯一的一个与之对应的RunLoop
对象
2. 主线程的RunLoop
已经自动创建好了,子线程的RunLoop
需要主动创建
3.RunLoop
在第一次获取时创建,在线程结束时销毁
4. 子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程添加操作,或者再次开启。如何让子线程永远活着,这时就要用到常驻线程:给子线程开启一个RunLoop
5. 主线程的RunLoop
里有两个预置的Mode
:kCFRunLoopDefaultMode
和UITrackingRunLoopMode
。这两个Mode
都已经被标记为"Common"
属性。DefaultMode
是App平时所处的状态,TrackingRunLoopMode
是追踪ScrollView
滑动时的状态。RunLoop
的退出:
1. 主线程销毁RunLoop
退出
2.Mode
中有一些Timer
、Source
、Observer
,这些保证Mode
不为空时保证RunLoop
没有空转并且是在运行的,当Mode
中为空的时候,RunLoop
会立刻退出
3. 我们在启动RunLoop
的时候可以设置什么时候停止自动释放池
RunLoop
内部有一个自动释放池,当RunLoop
开启时,就会自动创建一个自动释放池,当RunLoop
在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池。只有主线程的RunLoop
会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。“`
@autorelease {
// 执行代码
}
* 代码事例:可以通过下面这段代码进行阻塞某块代码的执行,又不阻塞主线程
```
while (!done){
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
1. `[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];` 这个是单次获取消息事件的方法,两个参数代表的意思是,获取 `default` 对应的事件消息,以及超时的时间
2. 主线程中使用 `while` 死循环,意味着阻塞主线程,此时主线程的 `Runloop` 处于不工作状态;如果 `RunLoop` 不工作,程序将无任何响应(卡页面);可以通过自定义 `observer` 来观察活动状态
3. 既想阻塞某块代码的执行,又不想影响主线程的其他操作,那么就需要手动轮训调用 `RunLoop` 来获取事件消息;保证程序可以正常运行
RunLoop的应用场景:
1、保证线程长久存活,如果程序中,需要经常在子线程中执行任务,频繁的创建和销毁线程,会造成资源的浪费。这时候我们就可以使用
RunLoop
来让该线程长时间存活而不被销毁。创建一个子线程,当子线程启动后,启动runloop
。2、让
timer
正常运转(NSTimer
在视图滑动时,依然能正常运转),在构造timer
时,我们只需要在添加timer
时,将mode
设置为NSRunLoopCommonModes
即可。
例如:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[timer fire];
// NSRunLoopCommonModes并不是一种Mode,而是一种特殊的标记,关联的有一个set ,默认包含NSDefaultRunLoopMode、NSModalPanelRunLoopMode、NSEventTrackingRunLoopMode,添加到NSRunLoopCommonModes中的还没有执行的任务,会在mode切换时,再次添加到当前的mode中,这样就能保证不管当前runloop切换到哪一个mode,任务都能正常执行。
注意:把timer加入到当前runloop后,必须让runloop 运行起来,否则timer仅执行一次。
总结:
(1)、如果是在主线程中运行 timer
,想要 timer
在某界面有视图滚动时,依然能正常运转,那么将 timer
添加到 RunLoop
中时,就需要设置 mode
为 NSRunLoopCommonModes
。
(2)、如果是在子线程中运行 timer
,那么将 timer
添加到 RunLoop
中后, Mode
设置为 NSDefaultRunLoopMode
或 NSRunLoopCommonModes
均可,但是需要保证 RunLoop
在运行,且其中有任务。
3、滚动视图流畅性优化(会影响 UITableView、UICollectionView
等视图滑动流畅的因素)
影响UITableView滑动性能的因素:1、在主线程中做耗时操作; 2、动态计算cell的高度,时间过久; 3、界面中背景色透明的视图过多; 4、主线程RunLoop切换到UITrackingRunLoopMode时,视图有过多的修改;应避免在主线程RunLoop切换到UITrackingRunLoopMode时修改视图。
4、App卡顿检测(使用 RunLoop
检测主线程的卡顿,并将卡顿时的线程堆栈信息保存下来,下次上传到服务器): 我们只需要在主线程的 RunLoop
中添加一个 observer
,检测从 kCFRunLoopBeforeSources
到 kCFRunLoopBeforeWaiting
花费的时间 是否过长。如果花费的时间大于某一个阙值,我们就认为有卡顿,并把当前的线程堆栈转储到文件中,并在以后某个合适的时间,将卡顿信息文件上传到服务器。
5、阻止App崩溃一次(在获取异常信息的回调函数里,可以获取到当前的 RunLoop
,然后获取该 RunLoop
中的所有 Mode
,手动运行一遍);
- 苹果不允许直接创建
RunLoop
,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。
线程刚创建时并没有RunLoop
,如果你不主动获取,那它一直都不会有。RunLoop
的创建是发生在第一次获取时,RunLoop
的销毁是发生在线程结束时。
每次调用RunLoop
的主函数时,只能指定其中一个Mode
,这个Mode
被称作CurrentMode
。如果需要切换Mode
,只能退出Loop
,再重新指定一个Mode
进入。
3. 属性和成员变量
- 在iOS5之前,定义一个属性:成员变量(大括号下的变量)+
@property
+@synthesize
成员变量三个步骤 在iOS5之后,苹果默认将编译器从GCC转换为LLVM,不需要为属性声明实例变量了。编译器在编译过程中发现没有新的实例变量后,就会生成一个带下划线开头的实例变量。
@property
声明的属性不仅生成一个带下划线的成员变量,同时也会生成setter/getter
方法。在.m文件中,可以直接实用_myString
实力变量,也可由通过属性self.myString
都是一样的@interface CustomView { NSString *name } @end
这段代码只是声明一个成员变量,并没有
setter/getter
方法。所以访问成员变量时,可以直接访问name,也可以像C++一样用self->name
来访问,但不能用self.name
来访问- 属性对成员变量扩充了存取方法
- 属性可以用一些关键字进行修饰,在必要的地方用不同的关键字进行修饰
4. 类方法、实例方法、静态方法
- 类方法:也称静态方法,指的是用
static
关键字修饰的方法。此方法属于类本身的方法,不属于类的某一个实例(对象) - 当你给一个类写一个方法,如果该方法需要访问某个实例的成员变量时,那么就将该方法定义成实例方法。静态方法正好相反,它不需要访问某个实例的成员变量,它不需要去改变某个实例的状态。我们就把该方法定义成静态方法