面试总结

 

一些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在以下情况下会被调用(师徒位置变化是触发):

  1. init不会
  2. addSubview会
  3. 设置view的frame会,一定要有变化
  4. 滚动UIScrollView会
  5. 旋转screen会触发父UIView上的layoutSubviews
  6. 改变一个UIView的大小也会触发父UIView上的layoutsubviews
  7. 直接调用setlayoutsubviews。drawrect在以下情况下会被调用:
    1. uiview初始化时没有设置rect大小,将直接导致drawrect不被自动调用。
    2. drawrect实在controller的loadview,viewdidload两方法之后调用的。
    3. 该方法在调用sizetofit后被调用,所以可以先调用sizetofit计算出size。然后系统自动调用drawrect方法。
    4. 通过设置contentmode为UIViewContentModeRedraw。那么将在每次设置或者更改frame的时候自动调用drawrect。
    5. 直接调用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指针都指向它的元类,最终指向NSObjectNSObject的元类是它自己。而NSObject的父类则是nil。这张图很好的说明了isasuper_class的区别:

ios_interview

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 selfid类型我们前面知道,它可以指向任意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消息传递过程中发生的事情,利用这些我们可以在很多地方对一个消息做处理。但是我们该怎么选择呢?

  1. 动态方法解析:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
  2. 快速消息转发:其他对象,使用范围更广,不只是限于原来的对象。
  3. 普通消息转发:它一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。

同时需要注意的是,消息转发过程中,步骤越往后,处理消息的代价就越大,最好能在第一步就处理完,这样的话,运行期系统可以将此方法缓存。如果这个类的实例还会再接收到同名选择子,那么根本无须再次启动消息转发流程。

  • Runtime可以做什么
    1. 在程序运行时动态添加一个类
    2. 在程序运行时动态修改一个类的属性和方法
    3. 在程序运行时遍历一个类的所有属性

Runtime有很多方法,可以在文档中一一查看,不同功能的方法通过前缀区分,比如说class_就是对类的操作,objc_就是对对象的操作,等等,都比较好理解。

杂项

UITableViewCell上有个UILabel,显示NSTimer实现的秒表时间,手指滚动cell过程中,label是否刷新,为什么?

这是否刷新取决于timer加入到Run Loop中的Mode是什么。Mode主要是用来指定事件在运行循环中的优先级的,分为:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
  • UITrackingRunLoopMode:ScrollView滑动时会切换到该Mode
  • UIInitializationRunLoopMode:run loop启动时,会切换到该mode
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合 苹果公开提供的Mode有两个:
  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
  • NSRunLoopCommonModes(kCFRunLoopCommonModes)
  • 在编程中:如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。当我们滚动的时候,也希望不调度,那就应该使用默认模式。但是,如果希望在滚动时,定时器也要回调,那就应该使用common mode。

TCP三次握手

  1. 第一次握手:客户端发送syn=i的包到服务器,并进入SYN_SEND状态,等待服务器确认
  2. 第二次握手:服务器收到syn包,必须确认客户的syn(ack=i+1),同时自己也发送一个syn包,即syn+ack包,此时服务器进入syn+recv状态
  3. 第三次握手:客户端收到服务器的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)进行信息交换

ios_interview

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