一些iOS的面试记录
如何访问并修改一个类的私有属性
- KVC
- runtime
创建一个Father类,声明一个私有属性name,并重写description打印name的值,在另外一个类中通过runtime来获取并修改Father中的属性
#import <objc/runtime.h>
@interface Father : NSObject
@property(nonatomic, copy) NSString *name;
@end
@implementation Father
- (NSString *)description {
return [NSString stringWithFormat:@"name: %@", _name];
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Father *father = [Father new];
father.name = @"hey";
NSLog(@"**%@", [father description]);
//count记录变量数量,IVar是runtime声明的一个宏
unsigned int count = 0;
//获取类所有的变量
Ivar *members = class_copyIvarList([Father class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = members[i];
//将Ivar变量转化为字符串,这里获得属性名
const char *memberName = ivar_getName(ivar);
NSLog(@"==%s", memberName);
Ivar m_name = members[0];
//修改属性值
object_setIvar(father, m_name, @"zhangsan");
NSLog(@"[[%@", [father description]);
}
}
@end
- 结果
2020-08-11 22:50:52.448854+0800 Test[6178:13104159] **name: hey
2020-08-11 22:50:52.449109+0800 Test[6178:13104159] ==_name
2020-08-11 22:50:52.449319+0800 Test[6178:13104159] [[name: zhangsan
+load和+initialize的区别
- +(void)load;
- 当类对象被引入项目时,runtime会向每一个类对象发送load消息,load方法会在每一个类甚至分类被引入时仅调用一次,调用的顺序:父类优先于子类,子类优先于分类
- load方法不会被类自动继承
- +(void)initialize;
- 也是第一次使用这个类的时候会调用这个方法
meta-class
是Class对象的类,为这个Class类存储类方法,当一个类发送消息时,就去那个类对应的meta-class中查找那个消息
layoutSubviews&drawRects
layoutSubviews在以下情况下会被调用(师徒位置变化是触发):
- init不会
- addSubview会
- 设置view的frame会,一定要有变化
- 滚动UIScrollView会
- 旋转screen会触发父UIView上的layoutSubviews
- 改变一个UIView的大小也会触发父UIView上的layoutsubviews
- 直接调用setlayoutsubviews。drawrect在以下情况下会被调用:
- uiview初始化时没有设置rect大小,将直接导致drawrect不被自动调用。
- drawrect实在controller的loadview,viewdidload两方法之后调用的。
- 该方法在调用sizetofit后被调用,所以可以先调用sizetofit计算出size。然后系统自动调用drawrect方法。
- 通过设置contentmode为UIViewContentModeRedraw。那么将在每次设置或者更改frame的时候自动调用drawrect。
- 直接调用setneedsdisplay,或者setneedsdisplayinrect触发drawrect,但是有个前提条件是rect不能为0
UIView和CALayer之间的关系
- UIView显示在屏幕上归功于CALayer,通过调用drawRect方法来渲染自身的内容,调节CALayer属性可以调整UIView的外观,UIView继承UIResponder,CALayer不可以响应用户事件
- UIView是iOS系统中界面元素的基础,所有界面元素都继承自它。它内部是由Core Animation来实现的,它真正绘图部分,是由一个叫CALayer(Core Animation Layer)的类来管理。UIView本身,更像一个CALayer的管理器,访问它的根绘图和坐标有关的属性,如frame,bounds等,实际上内部都是访问它所在CALayer的相关属性
- UIView有个layer属性,可以返回它的主CALayer实例,UIView有一个layerClass方法,返回主layer所使用的类,UIView的子类,可以通过重载这个方法,来让UiView使用不同的CALayer来显示
Push Notification是如何工作的
- 推送分为两种,本地推送和远程推送
- 本地推送:不需要联网,是开发人员在app内设定特定的时间来提醒用户干什么
- 远程推送:需要联网,用户的设备会于苹果APNS服务器形成一个长连接,用户设备会发送uuid和bundle id给苹果服务器,苹果服务器会加密生成一个deviceToken给用户设备,然后设备会将deviceToken发送给app服务器,服务器会将deviceToken存进他们的数据库,这时候如果有人发送消息给我,服务器端就会去查询我的deviceToken,然后将deviceToken和要发送的消息发送给苹果服务器,苹果服务器通过deviceToken找到我的设备并将消息推送到我的设备上,这里还有个情况是如果app在线,那么app服务器会于app产生一个长连接,这时候app服务器会直接通过deviceToken将消息推送到设备上
runloop
是一个与线程相关的机制,可以理解为一个循环,在这个循环里面等待事件然后处理事件。而这个循环是基于线程的,在Cocoa中每个线程都有它的runloop,通过这样的机制,线程可以在没有事件要处理的时候休息,有时间运行,减轻cpu压力。
- 非主线程通常来说就是为了执行某一任务的,执行完毕就需要归还资源,因此默认是不运行runloop的
- 每一个线程都有其对应的runloop,只是默认只有主线程的runloop是启动的,七天子线程的runloop默认是不启动的,若要启动则需要手动启动
- 在一个单独线程中,如果需要处理完某个任务后不退出,继续等待接收事件,则需要启用runloop
- 实质上,对于子线程的runloop默认是不存在的,因为苹果采用了
lazy load
。如果我们没有手动调用[NSRunLoop currentRunLoop]
的话,就不会查询是否存在当前线程的runloop,也不会去加载,创建了
runtime
概述
<objc/objc.h>
/// A pointer to an instance of a class.
typedef struct objc_object *id;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
在OC里面,每一个类的isa
指针都指向它的元类,最终指向NSObject
,NSObject
的元类是它自己。而NSObject
的父类则是nil。这张图很好的说明了isa
和super_class
的区别:
OC消息发送
OC的方法调用底层是给某个对象发送某个方法。并且,在编译的时候并没有确定具体调用哪个方法,只有在运行时才能确定。
打开终端,cd到main.m
文件所在的文件夹下,执行clang -rewrite-objc main.m
这时候,就可以在文件夹里面看到一个main.cpp
文件,打开,拉到最下面,就可以看到这一段代码:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
}
return 0;
}
研究一下这个objc_msgSend
。要使用它,得首先导入#import <objc/message.h>
,然后就可以使用了。但是,我们打出来这个发现没有任何参数提示。但是可以在Build Setting里面,找到
Enable Strict Checking of objc_msgSend Calls
改为No,再回去敲如objc_msgSend,既可以看到提示了。
具体的参数含义是:
id _Nullable self
:id
类型我们前面知道,它可以指向任意OC对象,这地方就代表着给谁发消息,也就是调用谁的方法。
...
:三个点代表参数列表/可扩展参数。
SEL _Nonnull op
:SEL
又是什么呢?到这里,我们就得提一下OC里面的方法了。老规矩,我们先去找定义,在<objc/runtime.h>
中:
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
IMP
其实就是一个指向方法实现的指针,而SEL
则是一个objc_selector
结构体,源码中我们找不到SEL
的定义,经过查阅资料得知,它完全可以理解为一个char *
,也就是说,其实它就是方法名的字符串,也就是一个方法的标签。
知道这些以后,我们就可以用消息发送来改写以前的OC代码,比如:
前面的Person
类的run
方法的调用:
Person *p = objc_msgSend([Person class], @selector(alloc));
p = objc_msgSend(p,@selector(init));
获取类名:
objc_getClass(char * _Nonnull name);
获取SEL
:
sel_registerName(const char * _Nonnull str);
所以:
id p = objc_msgSend(objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")),sel_registerName("init"));
objc_msgSend(p,sel_registerName("run"));;
运行OK。
我们甚至不需要导入Person.h
头文件,就可以直接获取创建它的实例,并且执行方法,完成了解耦。
OC的消息转发(message forwarding)
发送消息时,都直接是一些字符串参数,如果给一个不存在的方法发消息会怎样?
CRASH!
调用方法时,如果在方法在对象的类继承体系中没有找到,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出 unrecognized selector sent to …类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。这就涉及到以下4个方法:
- 动态方法解析(dynamic method resolution)
首先会调用+ resolveInstanceMethod:
(对应实例方法)或+ resolveClassMethod:
(对应类方法)方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。
我们这里测试一下,增加一个wahaha
方法的实现,看看是否可以顺利运行,首先,我们在Person.m
中导入#import <objc/message.h>
,然后,利用
class_addMethod(Class _Nullable __unsafe_unretained cls,SEL _Nonnull name,IMP _Nonnull imp, const char * _Nullable types)
来增加一个方法及实现,其中,第一个参数填self
,第二个参数填wahaha
的SEL,可以用@selector(wahaha)
,也可以用之前用过的sel_registerName("wahaha")
,第三个参数需要一个imp,我们知道IMP是指向方法实现的指针,这里我们可以用imp_implementationWithBlock(id _Nonnull block)
来实现,最后一个参数我们之前也见过,就是方法定义里面的的method_types,这个东西该怎么写呢?我们先去查一下官方文档:
types An array of characters that describe the types of the arguments to >the method. For possible values, see Objective-C Runtime >Programming Guide > Type Encodings. >Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).
这里面说明这个是一组描述方法的参数类型的字符数组,并且,每个方法都有两个被隐含的参数,一个是self
(代表当前对象),一个是_cmd
(代表当前对象的SEL),所以第二个和第三个字符必须是@
和:
,而第一个字符是返回值,所以一个没有参数的方法,它的types
就是"v@:"
。至于什么类型对应什么字符,可以去上面的链接中找。所以我们这里可以直接用"v@:"
。
//如果增加了方法并返回YES,就会重新发送消息并处理,返回NO,则进入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == sel_registerName("wahaha")) {
class_addMethod(self, sel_registerName("wahaha"), imp_implementationWithBlock(^(){
NSLog(@"wahaha");
}), "v@:");
}
return YES;
}
如果上面返回NO,则会进入完整的消息转发机制(full forwarding mechanism),这里又分为两个步骤:
- 快速消息转发(Fast Forwarding)
这个时候,如果实现了- forwardingTargetForSelector:
方法,系统就会进入该方法继续处理消息,这个方法的作用是把之前没办法处理的消息转发给别的对象去处理:
//返回一个对象继续处理消息
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == sel_registerName("wahaha")) {
return [Dog new];
}
return nil;
}
- 普通消息转发(Normal Forwarding)
如果上一步也没有对消息进行处理,则会进入最后一步,这里涉及到两个方法。它首先调用methodSignatureForSelector:
方法来获取函数的参数和返回值,如果返回为nil
,程序会Crash掉,并抛出unrecognized selector sent to instance
异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation
对象并调用-forwardInvocation:
方法。我们同样在这里对之前的消息进行处理一次:
//返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == sel_registerName("wahaha")) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
//转发消息
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Dog *dog = [Dog new];
if ([dog respondsToSelector:sel]) {
[anInvocation invokeWithTarget:dog];
}
}
以上就是OC消息传递过程中发生的事情,利用这些我们可以在很多地方对一个消息做处理。但是我们该怎么选择呢?
- 动态方法解析:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
- 快速消息转发:其他对象,使用范围更广,不只是限于原来的对象。
- 普通消息转发:它一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。
同时需要注意的是,消息转发过程中,步骤越往后,处理消息的代价就越大,最好能在第一步就处理完,这样的话,运行期系统可以将此方法缓存。如果这个类的实例还会再接收到同名选择子,那么根本无须再次启动消息转发流程。
- Runtime可以做什么
- 在程序运行时动态添加一个类
- 在程序运行时动态修改一个类的属性和方法
- 在程序运行时遍历一个类的所有属性
Runtime有很多方法,可以在文档中一一查看,不同功能的方法通过前缀区分,比如说class_
就是对类的操作,objc_
就是对对象的操作,等等,都比较好理解。
杂项
UITableViewCell上有个UILabel,显示NSTimer实现的秒表时间,手指滚动cell过程中,label是否刷新,为什么?
这是否刷新取决于timer加入到Run Loop中的Mode是什么。Mode主要是用来指定事件在运行循环中的优先级的,分为:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
:默认,空闲状态UITrackingRunLoopMode
:ScrollView滑动时会切换到该ModeUIInitializationRunLoopMode
:run loop启动时,会切换到该modeNSRunLoopCommonModes(kCFRunLoopCommonModes)
:Mode集合 苹果公开提供的Mode有两个:NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
NSRunLoopCommonModes(kCFRunLoopCommonModes)
- 在编程中:如果我们把一个
NSTimer
对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer
将不再被调度。当我们滚动的时候,也希望不调度,那就应该使用默认模式。但是,如果希望在滚动时,定时器也要回调,那就应该使用common mode。
TCP三次握手
- 第一次握手:客户端发送syn=i的包到服务器,并进入SYN_SEND状态,等待服务器确认
- 第二次握手:服务器收到syn包,必须确认客户的syn(ack=i+1),同时自己也发送一个syn包,即syn+ack包,此时服务器进入syn+recv状态
- 第三次握手:客户端收到服务器的syn+ack包,向服务器发送确认包ack(ack=k+1),此发送完毕,客户端和服3务器进入establised状态,完成三次状态。
Socket连接和http连接的区别
- http协议是基于tcp连接的,是应用层协议,主要解决如何包装数据。socket是对tcp/ip协议的封装,socket本身并不是协议,而是一个调用接口,通过socket,我们才能使用tcp/ip协议。
- http连接:短连接,客户端向服务器发送一次请求,服务器响应后连接断开,节省资源。服务器不能主动给客户端响应(除非采用http长连接技术),iPhone主要使用类NSURLConnection。
- socket连接:长连接,客户端跟服务器端直接使用socket进行连接,没有规定连接后断开,因此客户端和服务器段保持连接通道,双方可以主动发送数据,一般多用于游戏。socket默认连接超时时间是30秒,默认大小是8k(理解为一个数据包大小)。
HTTP协议的特点,GET和POST的区别
-
get和post的区别
- http超文本传输协议,是短连接,是客户端主动发送请求,服务器做出响应,服务器响应之后,链接断开。http是一个属于应用层面向对象的协议,http有两类报文:请求报文和响应报文。
- 请求报文:一个http请求报文由请求行,请求头部,空行和请求数据4部分组成。
- 响应报文:状态行,消息报头,响应正文。
- get请求:参数在地址后拼接,没有请求数据,不安全(因为所有参数都拼接在地址后面),不适合传输大量数据(长度有限制,为1024个字节)。
GET提交、请求的数据会附在URL之后,即把数据放置在HTTP协议头<requestline>中。 以?分割URL和传输数据,多个参数用&连接。如果数据是英文字母或数字,原样发送, 如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密。
- post请求:参数在请求数据区放着,相对get请求更安全,并且数据大小没有限制。把提交的数据放置在http包的
request-body
中 -
get提交的数据会在地址栏显示出来,而post提交,地址栏不会改变
- 传输数据的大小 + get提交时,传输数据会受到url长度限制,post由于不是通过url传值,理论上不受限制。
-
安全性
- post安全性比get高
- 通过get提交数据,用户名和密码将明文出现在url上,比如登录界面有可能会被浏览器缓存。
- https:基于http开发,使用安全套接字层(ssi)进行信息交换
UIViewController的完整生命周期
-[ViewController initWithNibName:bundle:];
-[ViewController init];
-[ViewController loadView];
-[ViewController viewDidLoad];
-[ViewController viewWillAppear:];
-[ViewController viewWillLayoutSubviews:];
-[ViewController viewDidLayoutSubviews:];
-[ViewController viewDidAppear:];
-[ViewController viewWillDisappear:];
-[ViewController viewDidDisappear:];
-[ViewController viewWillUnload:];
-[ViewController viewDidUnload:];
性能测试
profile->instruments->time profiler