系统分配了16个字节给NSObject对象(通过malloc_size函数获得) 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得) 复制代码
instance对象的isa指向class对象 class对象的isa指向meta-class对象 meta-class对象的isa指向基类的meta-class对象 复制代码
对象方法、属性、成员变量、协议信息,存放在class对象中 类方法,存放在meta-class对象中 成员变量的具体值,存放在instance对象 复制代码
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类 - 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数 willChangeValueForKey: 父类原来的setter didChangeValueForKey: - 内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:) 复制代码
手动调用willChangeValueForKey:和didChangeValueForKey: // - (void)viewDidLoad { [super viewDidLoad]; Person *person = [[Person alloc]init];; [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; [p willChangeValueForKey:@"name"]; [p didChangeValueForKey:@"name"]; } -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"被观测对象:%@, 被观测的属性:%@, 值的改变: %@/n, 携带信息:%@", object, keyPath, change, context); } 复制代码
不会触发KVO 复制代码
会触发KVO KVC在赋值时候,内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:) 发送通知 复制代码
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性 调用 setValue:forKey: setKey,_setKey ->找到了则进行赋值,未找到调用 accessInstanceVarlableDirctly 是否运行 修改值,返回YES 调用_key, _isKey, key, isKey 进行赋值 复制代码
- 在不修改原有类代码的情况下,为类添对象方法或者类方法 - 或者为类关联新的属性 - 分解庞大的类文件 使用场合: - 添加实例方法 - 添加类方法 - 添加协议 - 添加属性 - 关联成员变量 复制代码
Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中) 复制代码
Class Extension在编译的时候,它的数据就已经包含在类信息中 Category是在运行时,才会将数据合并到类信息中 复制代码
- 有load方法 - load方法在runtime加载类、分类的时候调用 - load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用 复制代码
- 当类第一次收到消息的时候会调用类的initialize方法 - 是通过 runtime 的消息机制 objc_msgSend(obj,@selector()) 进行调用的 - 优先调用分类的 initialize, 如果没有分类会调用 子类的,如果子类未实现则调用 父类的 复制代码
- load 是类加载到内存时候调用, 优先父类->子类->分类 - initialize 是类第一次收到消息时候调用,优先分类->子类->父类 - 同级别和编译顺序有关系 - load 方法是在 main 函数之前调用的 复制代码
不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果 Category是发生在运行时,编译完毕,类的内存布局已经确定,无法添加成员变量(Category的底层数据结构也没有成员变量的结构) 可以通过 runtime 动态的关联属性 复制代码
block 本质其实是OC对象 block 内部封装了函数调用以及调用环境 复制代码
如果需要在 block 内部修改外部的 局部变量的值,就需要使用__block 修饰(全局变量和静态变量不需要加__block 可以修改) __block 修饰以后,局部变量的数据结构就会发生改变,底层会变成一个结构体的对象,结构内部会声明 一个 __block修饰变量的成员, 并且将 __block修饰变量的地址保存到堆内存中. 后面如果修改 这个变量的值,可以通过 isa 指针找到这个结构体,进来修改 这个变量的值; 可以在 block 内部修改 变量的值 复制代码
block 一旦没有进行copy操作,就不会在堆上 使用注意:循环引用问题 (外部使用__weak 解决) 复制代码
如果是操作 NSMutableArray 对象不需要,因为 block 内部拷贝了 NSMutableArray对象的内存地址,实际是通过内存地址操作的 如果 NSMutableArray 对象要重新赋值,就需要加__block 复制代码
通过查看Block 源码,可以发现, block 内部如果单纯使用 外部变量, 会在 block 内部创建同样的一个变量,并且将 外部变量的值引用过来..(只是将外部变量值拷贝到 block 内部), 内部这个变量和外部 实际已经没关系了 从另一方面分析,block 本质也是一个 函数指针, 外部的变量也是一个局部变量,很有可能 block 在使用这个变量时候,外部变量已经释放了,会造成错误 加了__block 以后, 会将外部变量的内存拷贝到堆中, 内存由 block 去管理. 复制代码
OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名) objc_msgSend底层有3大阶段 消息发送(当前类、父类中查找)、动态方法解析、消息转发 复制代码
当我们的一个 receiver(实例对象)收到消息的时候, 会通过 isa 指针找到 他的类对象, 然后在类对象方法列表中查找 对应的方法实现,如果 未找到,则会通过 superClass 指针找到其父类的类对象, 找到则返回,未找打则会一级一级往上查到,最终到NSObject 对象, 如果还是未找到就会进行动态方法解析 类方法调用同上,只不过 isa 指针找到元类对象; 复制代码
当我们发送消息未找到方法实现,就会进入第二步,动态方法解析: 代码实现如下 // 动态方法绑定- 实例法法调用 + (BOOL)resolveInstanceMethod:(SEL)sel{ if (sel == @selector(run)) { Method method = class_getInstanceMethod(self, @selector(test)); class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method)); return YES; } return [super resolveInstanceMethod:sel]; } // 类方法调用 +(BOOL) resolveClassMethod:(SEL)sel.... 复制代码
未找到动态方法绑定,就会进行消息转发阶段 // 快速消息转发- 指定消息处理对象 - (id)forwardingTargetForSelector:(SEL)aSelector{ if (aSelector == @selector(run)) { return [Student new]; } return [super forwardingTargetForSelector:aSelector]; } // 标准消息转发-消息签名 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ if(aSelector == @selector(run)) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation{ //内部逻辑自己处理 } 复制代码
Objective-C runtime是一个`运行时`库,它为Objective-C语言的动态特性提供支持,我们所写的OC代码在运行时都转成了runtime相关的代码,类转换成C语言对应的结构体,方法转化为C语言对应的函数,发消息转成了C语言对应的函数调用。通过了解runtime以及源码,可以更加深入的了解OC其特性和原理 OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行 OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数 平时编写的OC代码,底层都是转换成了Runtime API进行调用 复制代码
利用关联对象(AssociatedObject)给分类添加属性 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档) 交换方法实现(交换系统的方法) 利用消息转发机制解决方法找不到的异常问题 复制代码
[self class] 和 [super class] 都是给当前类返送消息,spuer 表示在父类中查找 [self superClass] 和 [super superclass] 也是也当前类发消息,返回父类 第一个打印: MJStudent / MJStudent/ MJerson / MJPerson isKindOfClass 表示对象是否为当前类或者子类的 类型 isMemberOfClass 表示是否为当前类的的类型 isMemberOfClass 分为- 对象方法 和+ 类方法2中 - (bool)isMemberOfClass; 比较的是类对象 + (bool)isMemberOfClass; 比较的是元类 第二个打印: 1 ,0, 0, 0 复制代码
打印结果: <ViewController: 0x7f9396c16300> 复制代码
runloop运行循环,保证程序一直运行,主线程默认开启 用于处理线程上的各种事件,定时器等 可以提高程序性能,节约CPU资源,有事情做就做,没事情做就让线程休眠 应用范畴: 定时器,事件响应,手势识别,界面刷新,以及autoreleasePool 等等 复制代码
实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。 复制代码
每条线程都有唯一的一个与之对应的RunLoop对象 RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建 RunLoop会在线程结束时销毁 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop 复制代码
timer 定时器,是基于 runloop 来实现的, runloop 在运行循环当中,监听到了定制器 就会执行;所以 timer 需要添加到 runloop 中去, 注意子线程的 runloop 默认是不开启的,如果在子线程执行 timer 需要手动开启 runloop 复制代码
将 timer 对象添加到 runloop 中,并修改 runloop 的运行 mode NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:nil]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 复制代码
不明白问题想问什么? 复制代码
添加Observer监听RunLoop的所有状态
runloop 只能在一种 mode 下运行, 做不同的事情,runloop 会切换到对应的 model 下来执行,默认是 kCFRunLoopDefaultMode 如果视图滑动再回切换到 UITrackingRunLoopMode,如果需要在多种 mode 下运行则需要手动设置 kCFRunLoopCommonModes; 1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行 2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode 4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到 5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode 复制代码
同一时间,CPU 只能处理理一条线程, 只有一条线程在⼯工作 多线程并发执行,其实是 CPU 快速的在多条线程之间调度(切换) 如果 CPU 调度线程的时间⾜足够快, 就造成了多线程并发执⾏的假象 优势 充分发挥多核处理器的优势,将不同线程任务分配给不同的处理器,真正进入“⾏行 计算”状态 弊端 新线程会消耗内存控件和cpu时间,线程太多会降低系统行性能。 复制代码
倾向于GCD ,简单灵活,使用方便 复制代码
使用过 GCD中有2个用来执行任务的函数 用同步的方式执行任务 dispatch_sync(dispatch_queue_t queue, dispatch_block_t block); queue:队列 block:任务 用异步的方式执行任务 dispatch_async(dispatch_queue_t queue, dispatch_block_t block); 复制代码
GCD的队列可以分为2大类型 并发队列(Concurrent Dispatch Queue) 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务) 并发功能只有在异步(dispatch_async)函数下才有效 串行队列(Serial Dispatch Queue) 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务) 复制代码
1> GCD是纯C语⾔言的API,NSOperationQueue是基于GCD的OC版本封装 2> GCD只⽀支持FIFO的队列列,NSOperationQueue可以很⽅方便便地调整执⾏行行顺 序、设 置最⼤大并发数量量 3> NSOperationQueue可以在轻松在Operation间设置依赖关系,⽽而GCD 需要写很 多的代码才能实现 4> NSOperationQueue⽀支持KVO,可以监测operation是否正在执⾏行行 (isExecuted)、 是否结束(isFinished),是否取消(isCanceld) 5> GCD的执⾏行行速度⽐比NSOperationQueue快 任务之间不不太互相依赖:GCD 任务之间 有依赖/或者要监听任务的执⾏行行情况:NSOperationQueue 复制代码
1.加锁 2.同步执行 复制代码
os_unfair_lock ios10 开始 OSSpanLock ios10 废弃 dispatch_semaphore 建议使用,性能也比较好 dispatch_mutex dispatch_queue 串行 NSLock 对 mutex 封装 @synchronized 性能最差 复制代码
什么情况使用自旋锁比较划算? 预计线程等待锁的时间很短 加锁的代码(临界区)经常被调用,但竞争情况很少发生 CPU资源不紧张 多核处理器 什么情况使用互斥锁比较划算? 预计线程等待锁的时间较长 单核处理器 临界区有IO操作 临界区代码复杂或者循环量大 临界区竞争非常激烈 复制代码
注意死锁 在串行队列使用同步,容易造成死锁 复制代码
两种锁的加锁原理: 互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。 自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位, 复制代码
打印 1,3 performSelector after 是基于 timer 定制器,定时器又是基于 runloop 实现的 任务2在子线程中,子线程默认 runloop 是不开启的,所以不执行2 复制代码
打印1 start 执行完,线程就销毁了.任务 test 没法执行了 复制代码
CADisplayLink 保证调用频率和刷帧频率一直,60FPS, 不用设置时间间隔,每秒钟60次 可以使用 proxy 代理解决循环引用 CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用 复制代码
低地址-> 高地址 保留->代码段->数据段(字符串常量,已初始化全局数据,未初始化数据)>堆->栈内存-> 内核区域 代码段: 编译之后的代码 数据段: 字符串常量,已经初始化的全局变量,或者静态变量,未初始化的全局变量,静态变量 堆 (低>高) 通过 alloc malloc calloc 动态分配的内存 栈 (高地址 从 低地址) 函数调用开销() 复制代码
在iOS中,使用引用计数来管理OC对象的内存 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1 内存管理的经验总结 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1 可以通过以下私有函数来查看自动释放池的情况 extern void _objc_autoreleasePoolPrint(void); 复制代码
LLVM + Runtime 会为我们代码自动插入 retain 和 release 以及 autorelease等代码,不需要我们手动管理 复制代码
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。 runtime对注册的类, 会进行布局,对于weak对象会放入一个hash表中。 用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键, 在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。 复制代码
iOS在主线程的Runloop中注册了2个Observer -第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush() -第2个Observer 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush() 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop() objc_autoreleasePoolPop()调用时候回给 pool 中的对象发送一次 release 消息 复制代码
如果是普通的 局部对象 会立即释放 如果是放在了 autoreleasePool 自动释放吃,则会等runloop 循环,进入休眠前释放 复制代码
第一个内存会暴涨,self.name 会不行的创建 第二个内存固定,会使用 Tagged Pointer 将值存在地址中 复制代码
内存优化可以从 内存泄漏 和 内存开销 2方面入口 - 减少内存泄露 可以使用静态分析以及instruments的leaks 分析 注意 NStimer 以及 block ,delegate 等的使用,避免循环引用 - 降低内存使用峰值 1. 关于图片加载占用内存问题:imageNamed: 方法会在内存中缓存图片,用于常用的图片。 imageWithContentsOfFile: 方法在视图销毁的时候会释放图片占用的内存,适合不常用的大图等。 2.tableView cell 尽量使用重用机制,减少额外的开销 3.tableView 列表图片展示尽量使用缩略图 4.延迟加载 对象,节约内存开销 5.避免短时间大量创建对象,配合 autoreleasePool 减少内存峰值 6.重用大开销对象,比如: NSDateFormatter和NSCalendar 7.加载 html 尽量使用 wkwebView 8.单例使用不易过多 9.线程最大并发数 复制代码
卡顿优化 启动优化 耗电量优化 app 瘦身 CPU 占用率、 内存使用情况、网络状况监控、启动时闪退、卡顿、FPS、使用时崩溃、耗电量监控、流量监控.... 复制代码
1.最常用的就是cell的重用, 注册重用标识符 如果不重用cell时,每当一个cell显示到屏幕上时,就会重新创建一个新的cell; 如果有很多数据的时候,就会堆积很多cell。 如果重用cell,为cell创建一个ID,每当需要显示cell 的时候,都会先去缓冲池中寻找可循环利用的cell,如果没有再重新创建cell 2.避免cell的重新布局 cell的布局填充等操作 比较耗时,一般创建时就布局好 如可以将cell单独放到一个自定义类,初始化时就布局好 3.提前计算并缓存cell的属性及内容 当我们创建cell的数据源方法时,编译器并不是先创建cell 再定cell的高度 而是先根据内容一次确定每一个cell的高度,高度确定后,再创建要显示的cell,滚动时,每当cell进入凭虚都会计算高度,提前估算高度告诉编译器,编译器知道高度后,紧接着就会创建cell,这时再调用高度的具体计算方法,这样可以方式浪费时间去计算显示以外的cell 4.减少cell中控件的数量 尽量使cell得布局大致相同,不同风格的cell可以使用不用的重用标识符,初始化时添加控件, 不适用的可以先隐藏 5.不要使用ClearColor,无背景色,透明度也不要设置为0 渲染耗时比较长 6.使用局部更新 如果只是更新某组的话,使用reloadSection进行局部更新 7.加载网络数据,下载图片,使用异步加载,并缓存 8.少使用addView 给cell动态添加view 9.按需加载cell,cell滚动很快时,只加载范围内的cell 10.不要实现无用的代理方法,tableView只遵守两个协议 11.缓存行高:estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这两者同时存在才会出现“窜动”的bug。所以我的建议是:只要是固定行高就写预估行高来减少行高调用次数提升性能。如果是动态行高就不要写预估方法了,用一个行高的缓存字典来减少代码的调用次数即可 12.不要做多余的绘制工作。在实现drawRect:的时候,它的rect参数就是需要绘制的区域,这个区域之外的不需要进行绘制。例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否需要绘制image和text,然后再调用绘制方法。 13.预渲染图像。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕; 14.使用正确的数据结构来存储数据。 复制代码
1/. pre-main 之前 * 排查无用的动态库(定期清理) * 减少ObjC类(项目中不适用的的库,废弃的代码等)、方法(selector)、分类(category)的数量、无用的库 * 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize1. 2/. main 函数之后的 didFinishLaunchingWithOptions 加载完之前 * 不影响用户体验的操作,做延迟加载,不要全部放在 didFinishLaunchingWithOptions中去做 * 版本更新,一些三方初始化,不需要在 didFinishLaunchingWithOptions 初始化的放到,界面展示完以后再初始化 * 一些网络请求延迟 请求.. * 一些业务逻辑延迟 加载 * 初始化第三方 SDK * 配置 APP 运行需要的环境 * 自己的一些工具类的初始化 复制代码
1.不要频繁的刷新页面,能刷新1行cell最好只刷新一行,尽量不要使用reloadData. 2.选择正确的集合 NSArray,使用index来查找很快(插入和删除很慢) 字典,使用键来查找很快 NSSets,是无序的,用键查找很快,插入/删除很快 3.少用运算获得圆角,必须要用圆角的话,不如把图片本身就做成圆角 4.懒加载,不要一次性创建所有的subview,而是需要时才创建. 5.重用机制 6.图片处理 图片与imageView相同大小,避免多余运算 可以使用整副的图片,增加应用体积,但是节省CPU 可调大小的图片,可以省去一些不必要的空间 CALayer,CoreGraphics,甚至OpenGL来绘制,消耗CPU 7.cache,cache,cache(缓存所有需要的) 服务器相应结果的缓存(图片) 复杂计算结果的缓存(UITableView的行高) 8.尽量少用透明或半透明,会产生额外的运算. 9.使用ARC减少内存失误,dealloc需要重写并对属性置为nil 10.避免庞大的xib,storyBoard,尽量使用纯代码开发 CPU层面 1.Timer的时间间隔不宜太短,满足需求即可 2.线程适量,不宜过多,不要阻塞主线程 3.优化算法,减少循环次数 4.定位和蓝牙按需取用,定位之后要关闭或降低定位频率 5.一些硬件的使用,不使用就关掉 复制代码
MVC Model-view-controller 数据-视图-控制器 一般控制器用于管理数据和视图, 数据和视图交互都是通过控制器来进行的.视图和数据进行了解耦, 但是我们日常使用经常会将模型绑定给视图.模型封装在视图内部,外部不用管理视图内部业务逻辑,这数据 mvc 的变种, 控制器只给视图模型数据就好了. 缺点是视图和 模型有耦合; MVVM Model-view-viewModel 模型-视图-视图模型 view 和 model 的交互通过viewmodel 来进行交互,实现数据的双向绑定 MVP Model-view - Presenter 模型-视图-主持人 view 和 model 的交互通过Presenter,controller通过Presenter来管理 model 和 View 复制代码