iOS -- 问题杂记

3,340 阅读20分钟

本篇内容不作为任何题目的解答,仅仅是个人学习记录,如有错误还请指正。

iOS基础必备

1:讲讲你对noatomic & nonatomic的理解

atomicseter/getter内部实现是用了互斥锁来保证seter/getter在多线程中的安全,但atomic修饰的对象是自定义的,可能并没有加锁,在多线程中atomic修饰对象并不能保证线程安全。

nonatomicsetter/getter方法的实现并没有加互斥锁,所以nonatomic修饰的对象是非线程安全的,同时nonatomicsetter/getter方法也是非线程安全的,但也正因为没有互斥锁所以性能要比atomic好。

(举例:当多个线程同时调用同一属性的读取方法时,线程1会get到一个确定的值,但是get的值不可控,可能是线程2或者线程3...set之后的值,也可能是之前的值)

2:被 weak 修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable 么?里面的结构可以画出来么?

weak修饰的对象在被释放时候会被自动置为nil

runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash哈希表,Key是所指对象的地址,Valueweak指针的地址数组(这个地址的值是所指对象指针的地址)。

1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。

2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。

3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entryweak表中删除,最后清理对象的记录。

runtime内存空间中,SideTables是一个64个元素长度的hash数组,里面存储了SideTableSideTableshash键值就是一个对象objaddress。 因此可以说,一个obj,对应了一个SideTable。但是一个SideTable,会对应多个obj。因为SideTable的数量只有64个,所以会有很多obj共用同一个SideTable。而在一个SideTable中,又有两个成员,分别是RefcountMap refcnts(引用计数的 hash表,其keyobj的地址,而value,则是obj对象的引用计数)和weak_table_t weak_tableweak引用全局hash表),weak_table则存储了弱引用obj的指针的地址,其本质是一个以obj地址为key,弱引用obj的指针的地址作为valuehash表。hash表的节点类型是weak_entry_t

3:block 用什么修饰?strong 可以?

因为block变量默认是声明为栈变量的,为了能够在block的声明域外使用,所以要把block copy到堆,所以说为了block属性声明和实际的操作一致,blockcopy修饰。

对于strong修饰符:

首先,在OC中的三种类型的block

  • _NSConcreateGlobalBlock 全局的静态block,不会访问任何外部变量。
  • _NSConcreateStackBlock 保存在栈中的block,当函数返回时会被销毁。
  • _NSConcreateMallocBlock 保存在堆区的block,当引用计数为0时被销毁。

MRC环境下,block存放在全局区、堆区、栈区,ARC环境下只存放在全局区、堆区(栈区block,在ARC情况下会自动拷贝到堆区)。

所以在MRC环境下,如果block要访问外部变量,就必须用copy修饰,而在ARC环境下,如果block访问了外部变量,系统会自动将栈区block拷贝到堆区,所以也可以使用strong。只不过copyMRC遗留下来的,习惯而已

4:block 为什么能够捕获外界变量? __block做了什么事?

block中使用到的局部变量,都会在编译时动态创建的block结构体中创建一个与局部变量名称一样的实例变量,该实例变量存储着外部的局部变量的值,而当执行block时,再将这里存储下来的值取出来。

__block所起到的作用就是只要观察到该变量被block所持有,就将外部变量在栈中的内存地址拷贝堆中。__block将变量包装成对象,然后在把捕获到的变量封装在block的结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。

5:谈谈你对事件的传递链和响应链的理解

传递链:

(1)发生触摸事件后,系统会利用Runloop将该事件加入到一个由UIApplication管理的队列事件中.

(2)UIApplication会从事件队列中取出最前面的事件,并将事件分发下去处理,发送事件给应用程序的主窗口UIWindow.

(3)主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件

(4)遍历找到合适的视图控件后,就会调用视图控件的touches方法来处理事件:touchesBegin...此子控件就是我们需要找到的第一响应者

响应链:

(1)判断第一响应者能否响应事件,如果第一响应者能进行响应,则事件在响应链中的传递终止。如果第一响应者不能响应,则将事件传递给 nextResponder也就是通常的superview进行事件响应

(2)如果事件继续上传至UIWindow并且无法响应,它将会把事件继续上报给UIApplication

(3)如果事件继续上报至UIApplication并且也无法响应,它将会将事件上报给其Delegate

(4)如果最终事件依旧未被响应则会被系统抛弃

注: 穿透事件,扩大响应区域。

6:谈谈 KVC 以及 KVO 的理解?

KVC(key-value-coding)键值编码,是一种间接访问实例变量的方法。提供一种机制来间接访问对象的属性。

1、给私有变量赋值。

2、给控件的内部属性赋值(如自定义UITextFiledclearButton,或placeholder的颜色,一般可利用runtime获取控件的内部属性名,Ivar *ivar = class_getInstanceVariable获取实例成员变量)。

[textField setValue:[UIColor redColor] forKeyPath:@"placeholderLabel.textColor"];

3、结合Runtimemodel和字典的转换(setValuesForKeysWithDictionaryclass_copyIvarList获取指定类的Ivar成员列表)

KVO是一种基于KVC实现的观察者模式。当指定的被观察的对象的属性更改了,KVO会以自动或手动方式通知观察者。

监听 ScrollViewcontentOffSet属性

[scrollview addObserver:self forKeyPath:@"contentOffset"  options:NSKeyValueObservingOptionNew context:nil];

7:RunLoop 的作用是什么?它的内部工作机制了解么?

RunLoop基本作用:

(1).使程序一直运行并接受用户输入:

程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoopRunLoop保证主线程不会被销毁,也就保证了程序的持续运行。

(2)决定程序在何时应该处理哪些Event

比如:touches事件,timer事件,selector事件

(3)节省CPU时间 程序运行起来,没有任何事件源的时候,RunLoop会告诉CPU,要进入休眠状态,这时CPU就会将其资源释放出来去做其他的事情,当有事件源来的时候RunLoop就会被唤醒处理事件源。

8:苹果是如何实现 autoreleasepool的?

autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成: (1).objc_autoreleasepoolPush (2).objc_autoreleasepoolPop (3).objc_autorelease

通过函数名就可以知道,对autorelease分别执行push,和pop操作。销毁对象时执行release操作。

iOS -- Autorelease & AutoreleasePool

9:谈谈你对 FRP (函数响应式) 的理解

函数响应式编程--rxswift

10:平时开发有没有玩过 Instrument ?

Instruments里面工具很多,常用的有:

(1).Time Profiler:性能分析,用来检测应用CPU的使用情况.可以看到应用程序中各个方法正在消耗CPU时间。

(2).Zoombies:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能

(3).Allocations:用来检查内存,写算法的那批人也用这个来检查

(4).Leaks:检查内存,看是否有内存泄漏

(5).Core Animation:评估图形性能,这个选项检查了图片是否被缩放,以及像素是否对齐。被放缩的图片会被标记为黄色,像素不对齐则会标注为紫色。黄色、紫色越多,性能越差。

Runtime

1:什么是 isa,isa 的作用是什么?

2:一个实例对象的isa 指向什么?类对象指向什么?元类isa 指向什么?

是一个Class类型的指针.

每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。

3:objc 中类方法和实例方法有什么本质区别和联系?

类方法:

(1).类方法是属于类对象的

(2).类方法只能通过类对象调用

(3).类方法中的self是类对象

(4).类方法可以调用其他的类方法

(5).类方法中不能访问成员变量

(6).类方法中不定直接调用对象方法

实例方法:

(1).实例方法是属于实例对象的

(2).实例方法只能通过实例对象调用

(3).实例方法中的self是实例对象

(4).实例方法中可以访问成员变量

(5).实例方法中直接调用实例方法

(6).实例方法中也可以调用类方法(通过类名)

4:load 和 initialize 的区别?

共同点:

(1).+load+initialize会被自动调用,不能手动调用它们。

(2).子类实现了+load+initialize的话,会隐式调用父类的+load+initialize方法

(3).+load+initialize方法内部使用了锁,因此它们是线程安全的。

不同点:

+load方法是在runtime加载类、分类的时候调用(根据函数地址直接调用,默认执行),每个类,分类的+load方法都只会调用一次。

先执行父类的+load方法,再执行子类的+load方法,而分类的+load方法会在它的主类的+load方法之后执行。(分类按照编译顺序先后调用)

+initialize方法在第一次给某个类发送消息时调用(通过objc_msgSend调用),并且只会调用一次,是懒加载模式,此时所有的类都已经加载到了内存中,如果这个类一直没有使用,就不会调用+initialize方法。

先执行父类的+initialize方法,再执行子类的+initialize方法,如果子类没有实现+initialize方法时,会调用父类的+initialize方法,所以父类的+initialize方法会调用多次,如果分类实现了+initialize方法,就会'覆盖掉'类本身的+initialize方法。

+load方法一般是用来交换方法Method Swizzle+initialize方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作

5:_objc_msgForward 函数是做什么的?直接调用会发生什么问题?

_objc_msgForwardIMP 类型(函数指针),用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。

_objc_msgForward消息转发做的几件事:

(1).调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。

(2).调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil对象。否则返回nil,继续下面的动作。注意,这里不要返回self,否则会形成死循环。

(3).调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个NSlnvocation并传给forwardInvocation:

(4).调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation传入,如何处理就在这里面了,并返回非nil

(5).调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。

一旦调用_objc_msgForward,将跳过查找IMP 的过程,直接触发“消息转发”,如果调用了_objc_msgForward,即使这个对象确实已经实现了这个方法,也会告诉objc_msgSend没有找到这个方法的实现。最常见的场景是:你想获取某方法所对应的NSInvocation对象。

6:简述下 Objective-C 中调用方法的过程

先来看一下Method在头文件中的定义:

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name;
    char * method_types;
    IMP method_imp;
};

Method被定义为一个objc_method指针,在 objc_method结构体中,包含一个 SEL 和一个IMP ,其中,SEL是一个指向objc_selector 的指针,其实就是一个保存方法名的字符串。IMP是一个“函数指针”,就是用来找到函数地址,然后执行函数。所以,Method建立了SELIMP 的关联,当对一个对象发送消息时,会通过给出的SEL 去找到IMP,然后执行。

再加上继承的情况,可以总结出:当向一个对象发送消息时,会去这个类的方法缓存里寻找(cache methodLists)如果有缓存则跳到方法实现,否则继续在这个类的 methodLists中查找相应的SEL,如果查不到,则通过super_class 指针找到父类,再去父类的缓存列表和方法列表里查找,层层递进,直到找到基类为止。最后仍然找不到,才会走消息转发的流程。

7:能否想向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

不能向编译后得到的类中增加实例变量;能向运行时创建的类中添加实例变量;

因为编译后的类已经注册在runtime 中,类结构体中的objc_ivar_list实例变量的链表 和instance_size 实例变量的内存大小已经确定,同时runtime会调用 class_setIvarLayoutclass_setWeakIvarLayout来处理strong weak引用。所以不能向存在的类中添加实例变量;

运行时创建的类是可以添加实例变量,调用 class_addIvar函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair之前。

8:谈谈你对切面编程的理解

AOP: Aspect Oriented Programming 面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术 AOPOOP 的延续,函数式编程的一种衍生范型。(利用 Objective-C Runtime 的黑魔法Method Swizzling。)

优势:对业务逻辑的各个部分进行隔离,降低业务逻辑各部分之间的耦合度,提高程序的可重用性,提高了开发的效率。

漫谈iOS AOP编程之路

iOS面向切面编程AOP实践

网络&多线程

1:HTTP的缺陷是什么?

(1).通信过程中使用的是未加密的明文,内容会被取抓。

(2).对于服务器或者客户端来说,不会验证通信方的身份,因此有可能遭到中间人的伪装。

(3).无法验证所发送报文的完整性和安全性,而且报文的内容也有可能是中间人篡改之后发送过来的。

2:谈谈三次握手,四次挥手!为什么是三次握手,四次挥手?

iOS:为什么TCP连接要三次握手,四次挥手

3:socket 连接和 Http 连接的区别

HTTP协议是基于TCP连接的,是应用层协议,主要解决如何包装数据。HTTP连接是一种短连接,客户端向服务器发送一次请求,服务器响应后连接断开,节省资源,而且服务器不能主动给客户端响应。

Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。Socket连接是一种长连接,客户端跟服务器端直接使用Socket进行连接,没有规定连接后断开,因此客户端和服务器段保持连接通道,双方可以主动发送数据。Socket默认连接超时时间是30秒,默认大小是8K(理解为一个数据包大小)。

4:HTTPS,安全层除了SSL还有,最新的? 参数握手时首先客户端要发什么额外参数

5:HTTPS是什么?握手过程,SSL原理,非对称加密了解多少

HTTPS解析

6:什么时候POP网路,有了 Alamofire 封装网络 URLSession为什么还要用Moya ?

POP:面向协议编程 面向协议 = 协议 + 扩展 + 继承 通过协议、扩展做功能划分,降低模块间的耦合,增强代码的可扩展性。iOS中有一个不足之处就是多重继承,而协议正好能够解决多重继承的问题。在Swift中结构体变的更加强大了,不仅能定义属性,还能定义方法,还能多重继承协议,这是OC所不提供的。

如果已经使用Alamofire进行抽象URLSession,为什么不使用某些方法来抽象URL,参数等的实质呢,因此,Moya的基本思想是,我们需要一些网络抽象层,以充分封装实际直接调用Alamofire的层。

7:如何实现 dispatch_once

dispatch_once能保证任务只会被执行一次,即使同时多线程调用也是线程安全的。常用于创建单例、swizzeld method等功能。

@implementation ZHClass

+ (id)sharedInstance {
    static ZHClass *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

8:能否写一个读写锁?谈谈具体的分析

信号量方式:

- (void)viewDidLoad {
	[super viewDidLoad];
	// Do any additional setup after loading the view.
	self.semaphore = dispatch_semaphore_create(1);
	
	for (NSInteger i = 0; i < 10; i ++) {
		[[[NSThread alloc]initWithTarget:self selector:@selector(read) object:nil]start];
		[[[NSThread alloc]initWithTarget:self selector:@selector(write) object:nil]start];
	}
}

- (void)read{
	dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
	NSLog(@"%s",__func__);
	dispatch_semaphore_signal(self.semaphore);
}
- (void)write{
	dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
	NSLog(@"%s",__func__);
	dispatch_semaphore_signal(self.semaphore);
}

pthread_rwlock_t 方式:

@property (nonatomic,assign) pthread_rwlock_t rwlock;

//初始化读写锁
pthread_rwlock_init(&_rwlock, NULL);
	
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
	for (NSInteger i = 0; i < 3; i ++) {
		dispatch_async(queue, ^{
			[[[NSThread alloc]initWithTarget:self selector:@selector(readPthreadRWLock) object:nil]start];
			[[[NSThread alloc]initWithTarget:self selector:@selector(writePthreadRWLock) object:nil]start];
		});
	}
	
//读
- (void)readPthreadRWLock{
    pthread_rwlock_rdlock(&_rwlock);
    NSLog(@"读文件");
    sleep(1);
    pthread_rwlock_unlock(&_rwlock);
}
// 写
- (void)writePthreadRWLock{
    pthread_rwlock_wrlock(&_rwlock);
    NSLog(@" 写入文件");
    sleep(1);
    pthread_rwlock_unlock(&_rwlock);
}

//销毁锁
- (void)dealloc{
	pthread_rwlock_destroy(&_rwlock);
}

dispatch_barrier方式:

//异步队列
self.testqueue = dispatch_queue_create("rw.thread", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i ++) {
	dispatch_async(queue, ^{
		[self readBarryier];
		[self readBarryier];
		[self readBarryier];
		[self writeBarrier];
	});
}

- (void)readBarryier{
	dispatch_async(self.testqueue, ^{
		NSLog(@"读文件 %@",[NSThread currentThread]);
		sleep(2);
	});
}
- (void)writeBarrier{
	dispatch_barrier_async(self.testqueue, ^{
		NSLog(@"写入文件 %@",[NSThread currentThread]);
		sleep(1);
	});
}

9:什么时候会出现死锁?如何避免?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称。

死锁的产生满足一些特定条件:

(1).互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放。

(2).请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。

(3).不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用。

(4).循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

10:有哪几种锁?各自的原理?它们之间的区别是什么?最好可以结合使用场景来说

(1).NSLock实现了最基本的互斥锁,遵循了 NSLocking协议,通过lockunlock 来进行锁定和解锁。

(2).NSRecursiveLock递归锁,可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。只有当所有的锁被释放之后,其他线程才可以获得锁

(3).NSCondition 是一种特殊类型的锁,通过它可以实现不同线程的调度。一个线程被某一个条件所阻塞,直到另一个线程满足该条件从而发送信号给该线程使得该线程可以正确的执行。

(4).NSConditionLock 对象所定义的互斥锁可以在使得在某个条件下进行锁定和解锁。它和 NSCondition 很像,但实现方式是不同的。

(5).pthread_mutex,互斥锁是一种超级易用的互斥锁,使用的时候,只需要初始化一个pthread_mutex_tpthread_mutex_lock来锁定 pthread_mutex_unlock 来解锁,当使用完成后,记得调用 pthread_mutex_destroy 来销毁锁。

(6).pthread_rwlock,读写锁,在对文件进行操作的时候,写操作是排他的,一旦有多个线程对同一个文件进行写操作,后果不可估量,但读是可以的,多个线程读取时没有问题的。

当读写锁被一个线程以读模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程还可以继续进行。 当读写锁被一个线程以写模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞。

(7).dispatch_semaphore,信号量机制实现锁,等待信号,和发送信号,当有多个线程进行访问的时候,只要有一个获得了信号,其他线程的就必须等待该信号释放。

(8).@synchronized,一个便捷的创建互斥锁的方式,它做了其他互斥锁所做的所有的事情。

应当针对不同的操作使用不同的锁:

当进行文件读写的时候,使用pthread_rwlock 较好,文件读写通常会消耗大量资源,而使用互斥锁同时读文件的时候会阻塞其他读文件线程,而 pthread_rwlock不会。 当性能要求较高时候,可以使用pthread_mutex或者 dispath_semaphore,由于OSSpinLock 不能很好的保证线程安全,而在只有在iOS10中才有 os_unfair_lock ,所以,前两个是比较好的选择。既可以保证速度,又可以保证线程安全。 对于NSLock及其子类,速度来说NSLock < NSCondition < NSRecursiveLock < NSConditionLock

持续更新中..........