# iOS技术研究项目 **Repository Path**: wubinnjj/technical-research-project ## Basic Information - **Project Name**: iOS技术研究项目 - **Description**: 研究iOS技术知识点,每个知识点打一个分支,切换分支查看对应知识点运行 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-08-08 - **Last Updated**: 2025-05-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: IOS, oc, Objective-c ## README # iOS技术知识点 #### 1、weak指针的原理? ``` 当一个对象要释放时,会自动调用dealloc,接下来的调用轨迹是 dealloc _objc_rootDealloc rootDealloc object_dispose objc_destructInstance(obj->clearDeallocation 将弱引用对象置为nil)、free 答案: 将弱引用对象存到一个哈希表里,当这个对象要销毁,取出当前对象对应的弱引用表,把弱引用表里存储的弱引用到清除掉 ``` #### 2、ARC都帮我们做了什么? ``` ARC是LLVM编译器(Xcode架构编译器的框架系统,c++语言) 和 Runtime 互相协作的一个结果 ARC利用LLVM编译器在大括号快结束的时候自动生成retain、release、autorelease代码 ARC利用Runtime在程序运行中处理弱应用等相关操作 ``` #### 3、iOS中的常见多线程方案? | 技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 | | :-----: | :---- | :----: | :----: | :----: | | pthread | 1、 一套通用的多线程API
2、 适用于Unix\Linux\Windows等系统
3、跨平台\可移植
4、使用难度大 | C | 程序员管理 | 几乎不用 | | NSThread | 1、 使用更加面向对象
2、 简单易用、可直接操作线程对象 | OC | 程序员管理 | 偶尔使用 | | GCD | 1、 旨在替代NSThread等线程技术
2、 充分利用设备的多核 | C | 自动管理 | ***经常使用*** | | NSOperation | 1、 基于GCD(底层是GCD)
2、比GCG多了一些简单实用的功能
3、使用更加面向对象 | OC | 自动管理 | ***经常使用*** | #### 4、iOS NSThread GCD NSOperation的优缺点? ``` ** NSthread: ** 优点:比其他两种轻量级。 缺点:需要自己管理线程的生命周期,线程同步。 线程同步对数据的加锁会有一定的开销。 ** Operation、GCD: ** 优点:不需要关心线程管理,数据同步的事情。 两者区别:NSOperationQueue可以方便的管理并发、NSOperation之间的优先级。GCD主要与block结合使用。代码简洁高效 1. 性能:GCD更接近底层,而NSOperationQueue则更高级抽象,所以GCD在追求性能的底层操作来说,是速度最快的。这取决于使用Instruments进行代码性能分析,如有必要的话 2. 从异步操作之间的事务性,顺序行,依赖关系。GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持 3. 如果异步操作的过程需要更多的被交互和UI呈现出来,NSOperationQueue会是一个更好的选择。底层代码中,任务之间不太互相依赖,而需要更高的并发能力,GCD则更有优势 个人总结:我们应该忘记微小的性能提升。过早优化是万恶之源。只有Instruments显示有真正的性能提升时才有必要用低级的GCD。 ``` #### 5、多线程容易混淆的术语 ``` 同步和异步主要影响:能不能开启新的线程 同步:在当前线程中执行任务,不具备开启新线程的能力 异步:在新的线程中执行任务,具备开启新线程的能力 并发和串行主要影响:任务的执行方式(不能决定能不能开启线程) 并发队列:多个任务并发(同时)执行 串行队列:一个任务执行完毕后,再执行下个任务 主队列:异步主队列不能创建新的线程,任务会在主线程上执行 dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_async(queue, ^{ NSLog(@"当前任务 - %@",[NSThread currentThread]); }); 注意:sync往当前的串行队列添加任务就会产生死锁!!!(新任务等旧任务,旧任务等新任务) ``` #### 6、多线程安全隐患的解决方案 ``` 解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行) 常见的线程同步技术是:加锁 iOS中的线程同步方案: 1、OSSpinLock:叫做“自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源 目前已经不再安全,可能会出出现优先级反转的问题 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁 需要导入头文件 #import 2、os_unfair_lock: os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等 需要导入头文件 #import 3、pthread_mutex: 叫做‘互斥锁’,等待锁的线程会处于休眠状态 需要导入头文件 #import 4、dispatch_semaphore:semaphore叫做‘信号量’ 信号量的初始值,可以用来控制线程的并发访问的最大数量 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步 5、dispatch_queue(DISPATCH_QUEUE_SERIAL):直接使用GCD的串行队列,也是可以实现线程同步的 6、NSLock:是对mutex普通锁的封装 7、NSRecursiveLock:也是对mutex递归锁的封装,API跟NSLock基本一致 8、NSCondition:是对mutex和cont的封装 9、NSConditionLock 10、@synchronized:是对mutex递归锁的封装 性能从高到低排序:一般推荐使用dispatch_semaphore和pthread_mutex 1、os_unfair_lock(iOS10开始支持) 2、OSSpinLock 3、dispatch_semaphore 4、pthread_mutex 5、dispatch_queue(DISPATCH_QUEUE_SERIAL) 6、NSLock 7、NSCondition 8、pthread_mutex(recursive) 9、NSRecursiveLock 10、NSConditionLock 11、synchronized ``` #### 7、nonatomic和atomic ``` atom:原子 atomic:原子性 给属性加上atomic修饰,可以保证属性的setter和getter都是原子性操作(给setter和getter方法加锁),也就是保证setter和getter内部是线程同步的 缺点:频繁加锁,太耗性能 ``` #### 8、iOS多线程之读写安全方案(多读单写) ``` 如何实现以下场景? 同一时间,只能有1个线程进行写的操作 同一时间,允许有多个线程进行读的操作 同一时间,不允许既有写的操作,又有读的操作 经常用于文件等数据的读写操作,iOS中的实现方案有 1、pthread_rwlock: 2、dispatch_barrier ``` #### 9、CADisplayLink、NSTimer使用注意 ``` CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用 解决方案1:使用block方式 案例: __weak typeof(self) weakSelf = self; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { [weakSelf timerTest]; }]; 解决方案2:增加一个中间对象,它的类对象弱引用target即可 ViewController强引用timer NSTimer强引用target NSProxy弱引用target 继承NSProxy效率更高 因为NSProxy专门用来做消息转发的,会少了从父类查找方法和动态方法解析流程,这样效率更高,推荐使用 案例: @implementation WBProxy + (instancetype)proxyWithTarget:(id)target { // NSProxy对象不需要调用init,因为它本来就没有init方法 WBProxy *proxy = [WBProxy alloc]; proxy.target = target; return proxy; } //消息转发代码 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { [invocation invokeWithTarget:self.target]; } @end 在ViewController使用 self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[WBProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES]; ``` #### 10、GCD定时器 ``` NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时 而GCD的定时器会更加准时 ``` #### 11、iOS程序的内存布局 ``` 低地址 ---> 高地址 保留 代码段(__TEXT) 数据段(__DATA) 堆(heap) 栈(stack) 内核区 字符串常量 已初始化数据 未初始化数据 代码段:编译之后的代码 数据段:字符串常量:比如NSString *str = @“123” 已初始化数据:已初始化的全局变量,静态变量等 未初始化数据:未初始化的全局变量,静态变量等 堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大 栈:函数调用开销,比如局部变量,分配的内存空间地址越来越小 ``` #### 12、Tagged Pointer ``` 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储 在没有使用Tagged Pointer之前,NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中 当对象指针的最低有效位是1,则该指针为Tagged Pointer objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销 如何判断一个指针是否位Tagged Pointer ? iOS平台,最高有效位是1(第64位) MAC平台,最低有效位是1 ``` #### 13、ARC模式 ``` ARC模式: 编译器直接生成成员变量、get、set方法,并实现get、set方法 assgin修饰基本数据类型直接赋值 @property (nonatomic, assign) int age; eg: - (void)setAge:(int)age { _age = age; } - (int)age { return _age; } retain修饰先判断对象是否相同,若不相同则release,再retain;若相同则什么都不处理 @property (nonatomic, retain) WBDog *dog; eg: - (void)setDog:(WBDog *)dog { //引用计数+1 if (_dog != dog) { [_dog release]; _dog = [dog retain]; } } - (WBDog *)dog { return _dog; } ``` #### 14、copy ``` iOS提供了2个拷贝的方法 1、copy,不可变拷贝,产生不可变副本 2、mutableCopy,可变拷贝,产生可变副本 浅拷贝和深拷贝 1.深拷贝:内容拷贝,产生新的对象 2.浅拷贝:指针拷贝,没有产生新的对象 ``` #### 15、copy策略内部实现原理 ``` @property (copy, nonatomic) NSArray *data; //NSMutableArray不要这样写 使用copy策略会在set方法重新copy成不可变数据,导致添加元素崩溃 //@property (copy, nonatomic) NSMutableArray *data; //copy策略内部实现 - (void)setData:(NSArray *)data { if (_data != data) { [_data release]; _data = [data copy]; } } ``` #### 16、自定义对象的copy ``` //自定义对象的copy必须要要实现NSCopying的copyWithZone协议 - (id)copyWithZone:(NSZone *)zone { WBPerson *person = [[WBPerson allocWithZone:zone] init]; person.age = self.age; //可选择属性copy // person.weight = self.weight; return person; } ``` #### 17、block的变量捕获(capture) ``` 为了保证block内部能够正常访问外部变量,block有个变量捕获机制 ``` | 变量类型 | 捕获到block内部 | 访问方式 | | :-----: | :----: | :----: | | auto局部变量 | 是 | 值传递 | | static局部变量 | 是 | 指针传递 | | 全局变量 | 否 | 直接访问 | #### 18、block的类型 ``` block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型 __NSGlobalBlock__ __NSStackBlock__ __NSMallocBlock__ ``` #### 19、RunLoop的运行逻辑 ``` 1、Source0 触摸事件处理 performSelector:onThread: 2、Source1 基于Port的线程通信 系统事件捕捉 3、Times NSTimer performSelector:withObject:aferdelay: 4、Observers 用于监听RunLoop的状态 UI刷新(BeforeWaiting) Autorelease pool ``` #### 20、RunLoop在实际开发中的应用 ``` 1、控制线程生命周期(线程保活) 核心代码: //往RunLoop里面添加Source\Timer\Observer [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; while (weakSelf && !weakSelf.isStoped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } 2、解决NSTimer在滑动时停止工作的问题 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"%d",++count); }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 3、监控应用卡顿 4、性能优化 ``` #### 21、objc_msgSend执行流程 ``` OC中的方法调用,其实都是转换为objc_msgSend函数的调用 objc_msgSend的执行流程可以分为3大阶段 1、消息发送 a.首先判断receiver是否为nil,如果是nil,则退出; b.否则从reveiverClass的cache中查找方法,如果找到了方法,调用方法,查找结束; c.否则从reveiverClass的class_rw_t中查找方法,如果找到了方法,调用方法,结束查找,并将方法缓存到reveiverClass的cache中; d.否则从superClass的cache中查找方法,如果找到了方法,调用方法,结束查找,并将方法缓存到reveiverClass的cache中; e.否则从superClass的class_rw_t中查找方法,如果找到了方法,调用方法,结束查找,并将方法缓存到reveiverClass的cache中; f.否则继续查看上层是否还有superClass,如果有,则继续重复d的操作,直到找到方法为止; g.否则说明整个消息继承体系没有方法,消息发送阶段结束,进入动态方法解析阶段. 2、动态方法解析 a.是否曾经有动态解析,如果是,进入消息转发; b.否则调用: 实例方法: + (BOOL)resolveInstanceMethod:(SEL)sel 类方法: + (BOOL)resolveClassMethod:(SEL)sel c.标记为已经动态方法解析; d.消息发送; 没写动态方法解析,则进入消息转发阶段! 3、消息转发 a.调用forwardingTargetForSelector:方法,如果返回值不为nil,则进入objc_msgSend(返回值对象,SEL); b.否则返回值为nil,则调用methodSignatureForSelector:方法,如果返回值为nil,则调用doesNotRecognizeSelector:方法; c.方法签名方法返回值不为nil,则调用forwardInvocation:方法. ``` #### 22、self、super、class、superClass ``` NSObject -> Person -> Student | 方法调用 | 结果 | | :-----: | :---- : | | [self class] | Student | | [self superClass] | Person | | [super class] | Student | | [super superClass] | Person | super 是从父类开始查找方法;消息的接收者还是self; class 方法底层实现object_getClass(self); superClass 底层实现 class_getSuperclass(object_getClass(self)) 获取对象的父类 ``` #### 23、isKindOfClass和isMemberOfClass的区别 ``` 1、isMemberOfClass底层实现: + (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; } - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; } 判断当前对象是否属于传入的类 2、isKindOfClass底层实现: + (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls -> superclass) { if (tcls == cls) return YES; } return NO; } - (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls -> superclass) { if (tcls == cls) return YES; } return NO; } 判断当前对象是否属于传入的类的子类 ``` #### 24、Runtime的应用 ``` 什么是Runtime? 平时项目中有用过吗? OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时进行 OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数 平时编写的OC代码,底层都是转换成了Runtime API进行调用 具体应用: 1、利用关联对象(AssocitedObject)给分类添加属性 2、遍历类的所有成员变量(修改textField的占位文字颜色、字典转模型、自动归档解档) 3、交换方法实现(交换系统的方法) Method runMethod = class_getInstanceMethod([WBPerson class], @selector(run)); Method testMethod = class_getInstanceMethod([WBPerson class], @selector(test)); method_exchangeImplementations(runMethod, testMethod); 主要应用: 1、统计按钮点击的次数 · 2、数组、字典的判空处理 3、批量修改字体大小 ...... 4、利用消息转发机制解决方法找不到的异常问题 ```