iOS笔记。oc语法、runtime、runloop、多线程、内

2023-02-14  本文已影响0人  lym不解释

github链接


1. OC语法

iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

KVO利用runtime的API生成一个子类,当instance对象修改属性时,会调用Fondation的_NSSetVauleAndNotfity函数,
willchnagevauleforkey,
父类原来的setter、
didvaulechangeforkey,
内部触发监听器observe的监听方法(observerVauleForyKeyPath:ofObject:change:context)

简述一下KVC?

key vaule coding,在iOS开发中允许直接用key名修改对象的属性,或者给对象的属性赋值,不需要调用明确存取方法。
这样可以在运行时动态的访问和修改对象的属性,而不是在编译时就确定,这也是iOS黑魔法之一,像JSON解析model和其它开发技巧都是通过kvc实现的。

KVC的赋值和取值过程是怎样的?原理是什么?

graph TB
    A(setValue:forKey:) --> B[按照setKey _setKey顺序查找方法]
    B[按照setKey _setKey顺序查找方法] --> C{找到了?}
    C{找到了?} -- 找到了 --> e(传递参数/调用方法直接赋值)
    C{找到了?} -- 没有找到 --> F[查看accessInstanceVariablesDirectly方法的返回值] --> G{默认YES}
    G{默认YES} -- NO -->  H(调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException没有找到成员变量)
    G{默认YES} -- YES --> K[按照_key _isKey key isKey顺序查找成员变量]
    K[按照_key _isKey key isKey顺序查找成员变量] --> Z(直接赋值)
graph TB
    A(valueForKey:) --> B[按照getKey key isKey _key顺序查找方法]
    B[按照getKey key isKey _key顺序查找方法] --> C{找到了?}
    C{找到了?} -- 找到了 --> e(调用方法)
    C{找到了?} -- 没有找到 --> F[查看accessInstanceVariablesDirectly方法的返回值] --> G{默认YES}
    G{默认YES} -- NO -->  H(调用valueForUndefinedKey:并抛出异常<br>NSUnknownKeyException<br>没有找到成员变量)
    G{默认YES} -- YES --> K[按照_key _isKey key isKey顺序查找成员变量]
    K[按照_key _isKey key isKey顺序查找成员变量] --> Z(直接取值)

Category的使用场合是什么?Category的实现原理?

Category和Class Extension的区别是什么?

6.Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

Category能否添加成员变量?如果可以,如何给Category添加成员变量?

不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果。

添加关联对象
void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)

获得关联对象
id objc_getAssociatedObject(id object, const void * key)

移除所有的关联对象
void objc_removeAssociatedObjects(id object)

load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?

例如:compile sources中的文件顺序如下:SubB、SubA、A、B,load的调用顺序是:B、SubB、A、SubA。

分析:SubB是排在compile sources中的第一个,所以应当第一个被调用,但是SubB继承自B,所以按照优先调用父类的原则,B先被调用,然后是SubB,A、SubA。

第二种情况:compile sources中的文件顺序如下:B、SubA、SubB、A,load调用顺序是:B、A、SubA、SubB

讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?

从property看安全隐患

@property(nonatomic,strong) NSString* userName;
@property(nonatomic,assign) int age;

// 对象类型,userName属于TagPointer,在栈上,不存在线程安全问题
self.userName = @"xxx";

// 对象类型, userName属于指针类型,执行堆,多线程下可能有线程安全问题
self.userName = @"123234dfsdfasdfasdfad";

// 栈上,不存在线程安全问题
self.age = 19;

OC对象的分类

isa、superclass

Block的本质

变量类型 捕获到block内部 访问方式
局部变量 auto 值传递
局部变量 static 指针传递
全局变量 直接访问
block的类型 内存区域 复制效果
NSGlobalBlock 程序的数据区域 .data区 什么也不做
NSStackBlock 从栈复制到堆
NSMallocBlock 引用计数增加

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
- block作为函数返回值时
- 将block赋值给__strong指针时
- block作为Cocoa API中方法名含有usingBlock的方法参数时
- block作为GCD API的方法参数时

MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

__forwarding指针这里的作用就是针对堆的Block,把原来__forwarding指针指向自己,换成指向_NSConcreteMallocBlock上复制之后的__block自己。然后堆上的变量的__forwarding再指向自己。这样不管__block怎么复制到堆上,还是在栈上,都可以通过(i->__forwarding->i)来访问到变量值。

__weak、__strong的实现原理

在ARC环境下,id类型和对象类型和C语言其他类型不同,类型前必须加上所有权的修饰符。
所有权修饰符总共有4种:
1.__strong修饰符
2.__weak修饰符
3.__unsafe_unretained修饰符
4.__autoreleasing修饰符

id obj ;
objc_initWeak(&obj,strongObj);
objc_destoryWeak(&obj);

objc_initWeak的实现其实是这样的: 会把传入的object变成0或者nil,然后执行objc_storeWeak函数。

id objc_initWeak(id *object, id value) {   
    *object = nil; 
    return objc_storeWeak(object, value);
}

objc_destoryWeak函数的实现:也是会去调用objc_storeWeak函数。objc_initWeak和objc_destroyWeak函数都会去调用objc_storeWeak函数,唯一不同的是调用的入参不同,一个是value,一个是nil。

void objc_destroyWeak(id *object) { 
    objc_storeWeak(object, nil);
}

objc_storeWeak函数的用途就很明显了。由于weak表也是用Hash table实现的,所以objc_storeWeak函数就把第一个入参的变量地址注册到weak表中,然后根据第二个入参来决定是否移除。如果第二个参数为0,那么就把__weak变量从weak表中删除记录,并从引用计数表中删除对应的键值记录。

所以如果__weak引用的原对象如果被释放了,那么对应的__weak对象就会被指为nil。原来就是通过objc_storeWeak函数这些函数来实现的。

为什么iOS的Masonry中的self不会循环引用?

关于 Masonry ,它内部根本没有捕获变量 self,进入block的是testButton,所以执行完毕后,block会被销毁,没有形成环。所以,没有引起循环依赖。


UIButton *testButton = [[UIButton alloc] init];
[self.view addSubview:testButton];
testButton.backgroundColor = [UIColor redColor];
[testButton mas_makeConstraints:^(MASConstraintMaker *make) {
    make.width.equalTo(@100);
    make.height.equalTo(@100);
    make.left.equalTo(self.view.mas_left);
    make.top.equalTo(self.view.mas_top);
}];
[testButton bk_addEventHandler:^(id sender) {
    [self dismissViewControllerAnimated:YES completion:nil];
} forControlEvents:UIControlEventTouchUpInside];

iOS 闭包中的[weak self]在什么情况下需要使用,什么情况下可以不加?

只有当block直接或间接的被self持有时,才需要weak self。

为什么 block 里面还需要写一个 strong self,如果不写会怎么样?

在 block 中先写一个 strong self,其实是为了避免在 block 的执行过程中,突然出现 self 被释放的尴尬情况。
通常情况下,如果不这么做的话,还是很容易出现一些奇怪的逻辑,甚至闪退 野指针。

iOS block 为什么用copy修饰

首先, block是一个对象, 所以block理论上是可以retain/release的.
但是block在创建的时候它的内存是默认是分配在栈(stack)上, 而不是堆(heap)上的.
所以它的作用域仅限创建时候的当前上下文(函数, 方法...), 当你在该作用域外调用该block时, 程序就会崩溃.

  1. 一般情况下你不需要自行调用copy或者retain一个block. 只有当你需要在block定义域以外的地方使用时才需要copy. Copy将block从内存栈区移到堆区.
  2. 其实block使用copy是MRC留下来的, 在MRC下, 如上述, 在方法中的block创建在栈区, 使用copy就能把他放到堆区, 这样在作用域外调用该block程序就不会崩溃.
  3. 但在ARC下, 使用copy与strong其实都一样, 因为block的retain就是用copy来实现的。

2. Runtime

什么是Runtime?平时项目中有用过么?

OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
平时编写的OC代码,底层都是转换成了Runtime API进行调用

具体应用:
利用关联对象(AssociatedObject)给分类添加属性
遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
交换方法实现(交换系统的方法)
利用消息转发机制解决方法找不到的异常问题

OC 的消息机制

OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
objc_msgSend底层有3大阶段: 消息发送、动态方法解析、消息转发

  1. 消息发送
graph TB
    A(消息发送objc_msgSend) --> B[检查receiver是否为nil]
    B[检查receiver是否为nil] -- nil --> C(退出)
    B[检查receiver是否为nil] -- 不为nil --> D[从reveiverClass的cache中查找方法]
    D[从reveiverClass的cache中查找方法] -- 找到了 -->  E(调用方法<br>结束查找)
    D[从reveiverClass的cache中查找方法] -- 没有找到 --> F[从reveiverClass的class_rw_t中查找方法]
    F[从reveiverClass的class_rw_t中查找方法] -- 找到了 --> G(调用方法/结束查找/并将方法缓存到reveiverClass的cache中)
    F[从reveiverClass的class_rw_t中查找方法] -- 没有找到找到了 --> H(从superClass的cache中查找方法)
    H(从superClass的cache中查找方法) -- 找到了 --> G[调用方法<br>结束查找<br>并将方法缓存到reveiverClass的cache中]
    H(从superClass的cache中查找方法) -- 没有找到 --> J(从superClass的class_rw_t中查找方法)
    J(从superClass的class_rw_t中查找方法) -- 找到了 --> G[调用方法<br>结束查找<br>并将方法缓存到reveiverClass的cache中]
    J(从superClass的class_rw_t中查找方法) -- 没有找到 --> K(上层是否还有superClass)
    K(上层是否还有superClass) -- 否 --> L(动态方法解析)
  1. 动态方法解析
graph TB
    A[动态方法解析] --> B{是否曾经有动态解析}
    B{是否曾经有动态解析} -- 没有 --> D[调用+resolveInstanceMethod:<br>或者<br>+resolveClassMethod:<br>方法来动态解析方法] --> F[标记为已经动态解析] --> G(消息发送)
    B{是否曾经有动态解析} -- 有 --> C(消息转发)
  1. 消息转发
graph TB
    A[消息转发] --> B[调用<br>forwardingTargetForSelector:<br>方法]
    B[调用<br>forwardingTargetForSelector:方法] -- 返回值不为nil --> C(objc_msgSend) 
    B[调用<br>forwardingTargetForSelector:方法] -- 返回值为nil --> D[调用<br>methodSignatureForSelector:方法] 
    D[调用<br>methodSignatureForSelector:方法] -- 返回值为nil --> F(调用doesNotRecognizeSelector:方法) 
    D[调用<br>methodSignatureForSelector:方法] -- 返回值不为nil --> H(调用forwardInvocation:方法<br>开发者可以在这个方法中自定义任何逻辑) 

3. RunLoop

什么是RunLoop?

RunLoop是一个事件循环机制,用于管理线程中的事件和消息。它允许线程在没有任务的情况下休眠,并在有任务需要处理时唤醒线程。
RunLoop主要负责以下几个方面:

RunLoop与线程

RunLoop的运行逻辑?

01、通知Observers:进入Loop

02、通知Observers:即将处理Timers

03、通知Observers:即将处理Sources

04、处理Blocks

05、处理Source0(可能会再次处理Blocks)

06、如果存在Source1,就跳转到第8步

07、通知Observers:开始休眠(等待消息唤醒)

08、通知Observers:结束休眠(被某个消息唤醒):1> 处理Timer,2> 处理GCD Async To Main Queue,3> 处理Source1

09、处理Blocks

10、根据前面的执行结果,决定如何操作:01> 回到第02步, 02> 退出Loop

11、通知Observers:退出Loop

RunLoop在实际开中的应用?

4. 多线程

线程安全的本质是什么?为什么会出现多线程不安全?

线程安全不是指线程的安全,而是指内存的安全。每个进程的内存空间中都有一块特殊的公共区域,通常称为堆。当多个线程同时访问该区域,就会照成线程不安全的本质原因。

针对一块内存区域,我们有读和写两种操作,读和写同时发生在同一块内存区域时,就有可能发生多线程不安全。

在 iOS 中,常用的多线程方案有几种?

  1. NSThread:NSThread 是 Objective-C 对 POSIX 线程(pthread)的封装,可以直接创建一个线程,并进行启动、停止等操作。但是,由于需要自己管理线程的生命周期,使用起来比较繁琐。

  2. GCD(Grand Central Dispatch):GCD 是一个高效的多线程编程方案,通过队列和任务的概念来实现线程的管理和调度,使用起来非常方便。

  3. NSOperationQueue:NSOperationQueue 是一个基于 GCD 的高级抽象,使用 NSOperation 和 NSOperationQueue 可以实现更加复杂的任务管理。

  4. pthread:pthread 是 POSIX 线程库,使用起来比较底层,但是可以实现更加细粒度的线程控制。

  5. performSelectorInBackground:这是 NSObject 提供的一个方法,可以在后台线程执行一个方法,使用起来非常简单,但是灵活性不如前面几种方案。

iOS中的线程锁有哪些?

自旋锁、互斥锁、递归锁、条件锁

  1. OSSpinLock:OSSpinLock 是一种自旋锁,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源;目前已经不再安全,可能会出现优先级反转问题,如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。

  2. os_unfair_lock 是苹果推出的一种线程锁,也是一种自旋锁,可以在锁定期间不断进行忙等待,从而避免线程的切换。与 OSSpinLock 不同的是,os_unfair_lock 采用了更加公平的锁定策略,避免了优先级反转等问题。在 iOS 10 和 macOS 10.12 中,苹果已经推荐使用 os_unfair_lock 代替 OSSpinLock,并且将其作为一种新的官方推荐的锁机制。os_unfair_lock 的使用方式与其他锁机制类似,通常需要先进行初始化,然后使用 os_unfair_lock_lock、os_unfair_lock_unlock 等方法来进行加锁、解锁等操作。与其他锁机制不同的是,os_unfair_lock 不能被递归加锁,即同一线程多次加锁会导致死锁。因此,在使用 os_unfair_lock 时需要注意避免这种情况的发生。

  3. pthread_mutex:”互斥锁”,等待锁的线程会处于休眠状态。通过 pthread_mutex_lock 和 pthread_mutex_unlock 来实现加锁和解锁操作。pthread_mutex 还提供了 PTHREAD_MUTEX_RECURSIVE 和 PTHREAD_MUTEX_ERRORCHECK 等不同的类型,用于满足不同的需求。

  4. @synchronized:@synchronized是对mutex递归锁的封装。

  5. NSLock:NSLock是对mutex普通锁的封装,可以通过 lock 和 unlock 方法来控制加锁和解锁。NSLock 还提供了 tryLock 和 lockBeforeDate 等方法,可以方便地进行加锁尝试和超时控制。

  6. NSRecursiveLock:NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致。不同之处在于 NSRecursiveLock 是一种递归锁,可以被同一线程多次加锁而不会造成死锁。

  7. NSConditionLock:"条件锁",是对NSCondition的进一步封装,可以设置具体的条件值。NSCondition是对mutex和cond的封装。用于控制线程的执行顺序。NSCondition 提供了 wait、signal 和 broadcast 等方法,可以实现等待、唤醒和广播等操作。

  8. dispatch_semaphore:dispatch_semaphore 是 GCD 中的一种信号量,可以通过信号量的值来控制线程的并发数量。当信号量为 1 时,可以实现互斥锁的效果。

  9. dispatch_group:dispatch_group 是 GCD 中的一种机制,可以将一组任务进行分组,以便于统一管理和控制。通过 dispatch_group_notify 方法,可以在任务组执行完毕后执行指定的任务。

iOS线程同步方案性能比较

性能从高到低排序:

自旋锁、互斥锁比较

在iOS中,自旋锁(Spin Lock)和互斥锁(Mutex Lock)是常用的两种线程锁。它们的主要区别在于:

另外,自旋锁和互斥锁的实现方式也不同。自旋锁在锁定过程中不涉及系统调用,只需要进行原子性操作,因此可以使用 CPU 提供的原子性操作指令实现,比较轻量级。而互斥锁需要涉及系统调用,需要进行用户态和内核态之间的上下文切换,开销相对较大。

综上所述,自旋锁适用于锁定时间短、竞争轻度的场景,可以避免线程切换的开销,提高效率。而互斥锁适用于锁定时间长、竞争激烈的场景,可以避免空转的开销,保证公平性。在实际使用中,应根据具体场景选择适合的锁机制,以保证程序的正确性和稳定性。

NSOperationQueue 和 GCD 的区别,以及各自的优势

NSOperationQueue 是基于 GCD 构建的高层抽象,本质上仍然使用了 GCD 的底层机制。二者的区别主要在以下几个方面:

  1. 抽象程度:OperationQueue 提供了更高层次的抽象,使用 NSOperation 和 NSOperationQueue 可以将任务抽象为操作,并进行依赖关系的设置。而 GCD 只能处理简单的任务队列,缺少更高层次的抽象。

  2. 线程调度:OperationQueue 可以将操作分发到不同的线程中执行,而 GCD 只能将任务分配到全局的线程池中执行,无法精确控制线程的数量。

  3. 优先级设置:OperationQueue 可以为操作设置优先级,而 GCD 只能使用 dispatch_group_notify 和 dispatch_group_wait 等方法来实现类似的功能。

  4. 取消操作:OperationQueue 可以随时取消某个操作的执行,而 GCD 只能使用 dispatch_suspend 和 dispatch_resume 等方法来控制队列的执行状态。

总的来说,OperationQueue 提供了更高层次的抽象和更精细的控制,可以实现更复杂的任务调度和管理。而 GCD 则更加简单直观,适合处理简单的任务队列。在实际开发中,应根据具体的场景和需求选择适合的方案。

如何设计一个线程安全的可变数组?

线程安全的设计原则:

在 iOS 中,实现线程安全的数组有以下几种方法:

  1. 使用@synchronized关键字,这种方法简单易懂,但是效率相对较低。
  2. 使用 GCD 的读写锁,通过 dispatch_barrier_async 和 dispatch_sync 方法可以实现数组的读写安全。
  3. 使用 pthread 的读写锁,通过 pthread_rwlock_rdlock 和 pthread_rwlock_wrlock 方法可以实现数组的读写安全。
  4. 使用信号量 dispatch_semaphore,通过dispatch_semaphore_wait 和 dispatch_semaphore_signal 方法可以实现数组的读写安全。
  5. 使用 NSLock,通过lock 和 unlock 方法可以实现数组的读写安全。
  6. 使用 NSRecursiveLock,这种锁可以被同一线程多次获取,因此可以用于递归调用。
  7. 使用 NSConditionLock,这种锁可以通过设置不同的条件变量,控制线程的执行。

按照性能从高到低排序,可以得到以下结果:

  1. pthread 的读写锁,是性能最高的实现方式之一。
  2. GCD 的读写锁,性能略低于 pthread 读写锁。
  3. NSLock 和 NSRecursiveLock,性能比 GCD 稍低。
  4. 信号量 dispatch_semaphore,性能比 NSLock 和 NSRecursiveLock 稍低。
  5. @synchronized 关键字,性能相对较低,不建议在大规模数据操作时使用。

实现方案:

读写锁pthread_rwlock
@interface ThreadSafeArray : NSObject

@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, assign) pthread_rwlock_t rwlock;

@end

@implementation ThreadSafeArray

- (instancetype)init
{
    self = [super init];
    if (self) {
        _array = [NSMutableArray array];
        pthread_rwlock_init(&_rwlock, NULL);
    }
    return self;
}

- (void)addObject:(id)object
{
    pthread_rwlock_wrlock(&_rwlock);
    [self.array addObject:object];
    pthread_rwlock_unlock(&_rwlock);
}

- (void)removeObjectAtIndex:(NSUInteger)index
{
    pthread_rwlock_wrlock(&_rwlock);
    [self.array removeObjectAtIndex:index];
    pthread_rwlock_unlock(&_rwlock);
}

- (id)objectAtIndex:(NSUInteger)index
{
    id object;
    pthread_rwlock_rdlock(&_rwlock);
    object = self.array[index];
    pthread_rwlock_unlock(&_rwlock);
    return object;
}

- (void)dealloc
{
    pthread_rwlock_destroy(&_rwlock);
}

@end

在上面的代码中,addObject: 和 removeObjectAtIndex: 方法都使用了 pthread_rwlock_wrlock 来保证在执行这些操作时,其他线程无法对数组进行读写操作。而 objectAtIndex: 方法则使用了 pthread_rwlock_rdlock 来保证在获取数组元素时,其他线程可以进行读操作,但不能进行写操作。这样可以避免读写冲突,保证线程安全。注意,需要在对象销毁时调用 pthread_rwlock_destroy 来释放读写锁资源。

栅栏+并发队列 (dispatch_barrier)
@interface ThreadSafeArray : NSObject

@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong) dispatch_queue_t queue;

@end

@implementation ThreadSafeArray

- (instancetype)init
{
    self = [super init];
    if (self) {
        _array = [NSMutableArray array];
        _queue = dispatch_queue_create("com.example.arrayQueue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (void)addObject:(id)object
{
    dispatch_barrier_async(self.queue, ^{
        [self.array addObject:object];
    });
}

- (void)removeObjectAtIndex:(NSUInteger)index
{
    dispatch_barrier_async(self.queue, ^{
        [self.array removeObjectAtIndex:index];
    });
}

- (id)objectAtIndex:(NSUInteger)index
{
    __block id object;
    dispatch_sync(self.queue, ^{
        object = self.array[index];
    });
    return object;
}

@end

在上面的代码中,addObject: 和 removeObjectAtIndex: 方法都使用了 dispatch_barrier_async 来保证在执行这些操作时,其他线程无法对数组进行读写操作。而 objectAtIndex: 方法则使用了 dispatch_sync 来保证在获取数组元素时,其他线程无法对数组进行写操作,但可以进行读操作。这样可以避免读写冲突,保证线程安全。

dispatch_semaphore_t信号量
@interface ThreadSafeArray : NSObject

@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;

@end

@implementation ThreadSafeArray

- (instancetype)init
{
    self = [super init];
    if (self) {
        _array = [NSMutableArray array];
        _semaphore = dispatch_semaphore_create(1);
    }
    return self;
}

- (void)addObject:(id)object
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    [self.array addObject:object];
    dispatch_semaphore_signal(self.semaphore);
}

- (void)removeObjectAtIndex:(NSUInteger)index
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    [self.array removeObjectAtIndex:index];
    dispatch_semaphore_signal(self.semaphore);
}

- (id)objectAtIndex:(NSUInteger)index
{
    id object;
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    object = self.array[index];
    dispatch_semaphore_signal(self.semaphore);
    return object;
}

@end

在上面的代码中,addObject:、removeObjectAtIndex: 和 objectAtIndex: 方法都使用了 dispatch_semaphore_wait 和 dispatch_semaphore_signal 来保证在执行这些操作时,其他线程无法对数组进行读写操作。使用信号量时,每次读写操作都需要获取信号量,以阻止其他线程进行操作。在操作完成后,需要释放信号量,允许其他线程进行读写操作。这样可以避免读写冲突,保证线程安全。

5. 内存管理

使用CADisplayLink、NSTimer有什么注意点?

CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。
解决方案:

NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时,而GCD的定时器会更加准时。

 // 获取一个全局的线程来运行计时器
 dispatch_queue_t queue = dispatch_get_global_queu(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 // 创建一个计时器
 dispatch_source_t timer = dispatch_source_creat(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
 // 设置计时器, 这里是每10毫秒执行一次
 dispatch_source_set_timer(timer, dispatch_walltime(nil, 0),10*NSEC_PER_MSEC, 0);
 // 设置计时器的里操作事件
 dispatch_source_set_event_handler(timer, ^{
     //do you want....
 });
 // 启动定时器
 dispatch_resume(timer);

iOS程序的内存布局

Tagged Pointer

从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

如何判断一个指针是否为Tagged Pointer?
iOS平台,最高有效位是1(第64bit)
Mac平台,最低有效位是1

当字符串的长度为10个以内时,字符串的类型都是NSTaggedPointerString类型,当超过10个时,字符串的类型才是__NSCFString

// 对象类型,userName属于TagPointer,在栈上,不存在线程安全问题
self.userName = @"xxx";

// 对象类型, userName属于指针类型,执行堆,多线程下可能有线程安全问题
self.userName = @"123234dfsdfasdfasdfad";

OC对象的内存管理

在iOS中,使用引用计数来管理OC对象的内存

一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间

调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

内存管理的经验总结

引用计数的存储

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

dealloc

当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是

自动释放池

自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的
当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息。

自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的

POOL_SENTINEL(哨兵对象)

POOL_SENTINEL 只是 nil 的别名。#define POOL_SENTINEL nil

在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。

objc_autoreleasePoolPush 方法
objc_autoreleasePoolPush

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

它调用 AutoreleasePoolPage 的类方法 push,也非常简单:

static inline void *push() {
   return autoreleaseFast(POOL_SENTINEL);
}

在这里会进入一个比较关键的方法 autoreleaseFast,并传入哨兵对象 POOL_SENTINEL:

static inline id *autoreleaseFast(id obj)
{
   AutoreleasePoolPage *page = hotPage();
   if (page && !page->full()) {
       return page->add(obj);
   } else if (page) {
       return autoreleaseFullPage(obj, page);
   } else {
       return autoreleaseNoPage(obj);
   }
}

hotPage 可以理解为当前正在使用的 AutoreleasePoolPage。

objc_autoreleasePoolPop 方法

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

该静态方法总共做了三件事情:

  1. 使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPage
  2. 调用 releaseUntil 方法释放栈中的对象,直到 stop
  3. 调用 child 的 kill 方法

autorelease 方法
我们已经对自动释放池生命周期有一个比较好的了解,最后需要了解的话题就是 autorelease 方法的实现,先来看一下方法的调用栈:

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

Runloop和Autorelease

iOS在主线程的Runloop中注册了2个Observer:

6.性能优化

卡顿优化 - CPU

卡顿优化 - GPU

离屏渲染

在OpenGL中,GPU有2种渲染方式
On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作

离屏渲染消耗性能的原因
需要创建新的缓冲区
离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕

哪些操作会触发离屏渲染?

卡顿检测

平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作
可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的

耗电的主要来源

耗电优化

APP的启动

APP的启动优化

按照不同的阶段

安装包瘦身

7. 设计模式、架构

设计模式(Design Pattern)

是一套被反复使用、代码设计经验的总结
使用设计模式的好处是:可重用代码、让代码更容易被他人理解、保证代码可靠性
一般与编程语言无关,是一套比较成熟的编程思想

设计模式可以分为三大类

项目管理

  1. 开发和构建环境
    通过 Ruby ⼯具链为整个项⽬搭建⼀致的开发和构建环境,我们通过 Xcode、rbenv、RubyGems 和 Bundler 搭建⼀个统⼀的 iOS 开发和构建环境.
  1. 项目配置
    xcconfig也叫作 Build configuration file(构建配置⽂件),我们可以使⽤它来为 Project 或 Target 定义⼀组 Build Setting。由于它是⼀个纯⽂本⽂件,我们可以使⽤ Xcode 以外的其他⽂本编辑器来修改,⽽且可以保 存到 Git 进⾏统⼀管理。通过 Build Configuration、 Xcode Scheme 以及 xcconfig 配置⽂件来统⼀项⽬的构建配置,从⽽搭建出多个不同环境。

  2. 代码管理

    • 遵循swift-style-guide代码风格。
    • 在提交代码前,必须使用SwiftFormat对代码进行格式化。
    • SwiftLint
上一篇 下一篇

猜你喜欢

热点阅读