InterView一个靠谱的iOS开发(一)

1,782 阅读30分钟

1. setNeedsLayout与layoutIfNeeded的区别

  • setNeedsLayout
    • 标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,在下一轮runloop结束前刷新,对于这一轮runloop之内的所有布局和UI上的更新只会刷新一次,layoutSubviews一定会被调用。
    • 为什么不是立即调用呢?因为渲染毕竟比较消耗性能,特别是视图层级复杂的时候。这种机制下任何UI控件布局上的变动不会立即生效,而是每次间隔一个周期,所有UI控件在布局上的变动统一生效并且在视图上更新,苹果通过这种高性能的机制保障了视图渲染的流畅性。
    • runloopobserver回调=>CoreAnimation渲染引擎一次事务的提交=>CoreAnimation递归查询图层是否有布局上的更新=>CALayer layoutSublayers=>UIView layoutSubviews 这样一个调用的流程。从这里也可以看到UIView其实就是相当于CALayer的代理。
  • layoutIfNeeded:
    • 如果有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)。

2. UIView的bounds和frame和center的关系。

  • frame:描述当前视图在其父视图中的位置和大小。
  • bounds:描述当前视图在其自身坐标系统中的位置和大小。
  • center:描述当前视图的中心点在其父视图中的位置。
  • View B是View A的子视图,那么ViewB的frame属性为origin(200,100),size(200,250),而View B的bounds属性为origin(0,0),size(200,250)。center属性则用CGPoint表示矩形中心点在其父视图中的位置,View B的center属性为(300,200)。

3. Objective-c中:isKindOfClass,isMemberOfClass,isSubclassOfClass的区别。;isEqual和isEqualToString和==三者的区别;

  • - (BOOL)isKindOfClass:(Class)aClass:返回一个BOOL类型的值,表示调用该方法的类是否是参数类或者继承于参数类;
  • - (BOOL)isMemberOfClass:(Class)aClass:返回一个BOOL类型的值,表示调用该方法的类是否是参数类;
  • + (BOOL)isSubclassOfClass:(Class)aClass:返回一个BOOL类型的值,表示调用该方法的类是不是参数类的一个子类或者是这个类的本身。
  • ==:对于基本类型,==运算符比较的是值;对于对象类型,==运算符比较的是对象的地址(即是否为同一对象);
  • isEqual:NSObject方法,返回一个bool值判断两个对象是否相等。如果两个对象是相等的,那么他们必须有相同的哈希值
  • isEqualToString:NSString方法,而NSString是继承自NSObject的,所以isEqualToString应该是isEqual的衍生方法,是对isEqual的细分。速度效率上优于isEqual。相似的还有isEqualToArray等。

4. loadinitialize方法的区别是什么?

  • 调用方式
    • load是:根据函数地址直接调用
    • initialize是:通过objc_msgSend调用
  • 调用时刻
    • load是:runtime加载类、分类的时候调用(只会调用一次)
    • initialize是:类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
  • 调用顺序
    • load是:先调用类的load(先编译的类,有限调用load。调用子类的load之前,会先调用父类的load);再调用分类的load(先编译的分类,优先调用``load)。 * initialize是:先初始化父类,再初始化子类(可能最终调用的是父类的initialize`方法)。

5. 宏定义来获取数组元素的个数

#define SIZE_ARRAY(a) (sizeof(a) / sizeof((a)[0]))
sizeof函数是求对象空间大小的函数。 arry是整个数组,arry[0]是数组中第一个元素。

6. KVO的底层原理

我之前的OC底层知识点里有写。 juejin.cn/post/684490…

7. 消息调用的过程

我之前的OC底层知识点里有写。 juejin.cn/post/684490…

8. http有哪些部分

HTTP 是基于 TCP/IP协议来传输信息的应用层协议,基于请求与响应,无状态的。它不涉及数据包(packet)传输,主要规定了客户端和服务器之间的通信格式,默认使用80端口。

  • HTTP 的请求报文分为三个部分:请求行、请求头、请求体
    • 请求行(Request line)分为三个部分:请求方法、请求地址和协议版本。
    • 请求头可用于传递一些附加信息,格式为:键: 值,注意冒号后面有一个空格:
    • 请求体(又叫请求正文)是 post 请求方式中的请求参数,以 key = value 形式进行存储,多个请求参数之间用&连接,如果请求当中请求体,那么在请求头当中的 Content-Length 属性记录的就是该请求体的长度。
  • HTTP 请求头包含的字段和含义
    • Accept:浏览器可接受的MIME类型。(application/json,multipart/form-data,audio/mpeg)
    • Accept-Charset:浏览器可接受的字符集。
    • Accept-Encoding:浏览器能够进行解码的数据编码方式,比如gzip。
    • Accept-Language:浏览器所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到。
    • Authorization:授权信息,通常出现在对服务器发送的WWW-Authenticate头的应答中。
    • Connection:表示是否需要持久连接。如果Servlet看到这里的值为“Keep-Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接),它就可以利用持久连接的优点,当页面包含多个元素时(例如Applet,图片),显著地减少下载所需要的时间。要实现这一点,Servlet需要在应答中发送一个Content-Length头,最简单的实现方法是:先把内容写入ByteArrayOutputStream,然后在正式写出内容之前计算它的大小。
    • Content-Length:表示请求消息正文的长度。
    • Cookie:设置cookie,这是最重要的请求头信息之一
    • From:请求发送者的email地址,由一些特殊的Web客户程序使用,浏览器不会用到它
    • Host:初始URL中的主机和端口。
    • If-Modified-Since:只有当所请求的内容在指定的日期之后又经过修改才返回它,否则返回304“Not Modified”应答。
    • Pragma:指定“no-cache”值表示服务器必须返回一个刷新后的文档,即使它是代理服务器而且已经有了页面的本地拷贝。
    • Referer:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面。
    • User-Agent:浏览器类型,如果Servlet返回的内容与浏览器类型有关则该值非常有用。
    • UA-Pixels,UA-Color,UA-OS,UA-CPU:由某些版本的IE浏览器所发送的非标准的请求头,表示屏幕大小、颜色深度、操作系统和CPU
  • HTTP 响应的格式上除响应状态行(第一行)与请求报文的请求行不一样之外,其他的就格式而言是一样的。
    • 响应状态行:
      • 1XX 消息 表示请求已经被接收了,服务器正在进行处理
      • 2XX 成功 表示请求已经成功接收且被处理
      • 3XX 重定向 表示需要客户端采取进一步的操作才能完成请求
      • 4XX 请求错误 表示客户端错误,可能是请求的语法有错或该请求无法实现
      • 5XX 服务器错误 表示服务器错误,服务器端无法实现发出的合理请求
    • 响应头同样可用于传递一些附加信息
    • 响应体也就是网页的正文内容,一般在响应头中会用 Content-Length 来明确响应体的长度,便于浏览器接收,对于大数据量的正文信息,也会使用 chunked 的编码方式。
  • 三次握手四次挥手
    • 第一次握手:浏览器准备向服务器建立连接,发送同步序列编号(SYN)到服务器,等待服务器确认
    • 第二次握手:服务器确认该同步序列编号(SYN),自己也发送一个同步序列编号(SYN+ACK)给浏览器
    • 第三次握手:浏览器收到服务器的同步序列编号(SYN+ACK),向服务器发送确认包(ACK),客户端和服务器进入 ESTABLISHED(TCP 连接成功)状态,完成三次握手。
    • 挥手的过程和握手过程区别在第二次,当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步挥手。
  • HTTPS

身披SSL外壳的HTTP,基于HTTP协议,通过SSL或TLS提供加密处理数据、验证对方身份以及数据完整性保护。通过抓包可以看到数据不是明文传输的。

  • TLS/SSL是一种加密通道的规范

    • SSL:(Secure Socket Layer,安全套接字层),SSL是基于HTTP之下TCP之上的一个协议层,是基于HTTP标准并对TCP传输数据时进行加密,所以HPPTS是HTTP+SSL/TCP的简称。位于TCP/IP协议与各种应用层协议之间一项标准技术,可确保互联网连接安全,保护两个系统之间发送的任何敏感数据。采用了RC4、MD5以及RSA等加密算法,使用40 位的密钥,适用于商业信息的加密。默认使用443端口。该协议由两层组成:SSL记录协议和SSL握手协议。
    • TLS:(Transport Layer Security,传输层安全协议),更为安全可靠的升级版 SSL(消息认证使用密钥散列法等,更安全的MAC算法,更严密的警报,规范定义更明确)。用于两个应用程序之间提供保密性和数据完整性。该协议由两层组成:TLS记录协议和TLS握手协议。
  • https发起的请求,会对内容采用混合加密技术,中间者无法直接查看明文内容;通过证书认证客户端访问的是自己的服务器;防止传输的内容被中间人冒充或者篡改。

  • 工作流程大致可以分为三个阶段:

    • 认证服务器。
    • 协商会话密钥。
    • 加密通讯。
  • 在传统的三次握手建立连接之后,HTTPS还会有其他一系列的步骤进行SSL握手协商建立连接。

    • 客户端通过发送Client Hello报文开始SSL通信,报文中包含客户端支持的SSL的指定版本、加密组件列表(所使用的加密算法及密钥长度等)。
    • 服务器可进行SSL通信时,会以Server Hello报文作为应答。和客户端一样,在报文中包含SSL版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。
    • 之后服务器发送Certificate报文。报文中包含公开密钥证书。
    • 最后服务器发送Server Hello Done 报文通知客户端,最初阶段的SSL握手协商部分结束。
    • SSL第一次握手结束之后,客户端以Client Key Exchange报文作为回应。报文中包含通信加密中使用的一种被称为Pre-master secret的随机密码串。该报文已用步骤3中的公开密钥进行加密。
    • 接着客户端继续发送Change Cipher Spec报文。该报文会提示服务器,在此报文之后的通信会采用Pre-master secret密钥加密。
    • 客户端发送Finished报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确解密该报文作为判定标准。
    • 服务器同样发送Change Cipher Spec报文。
    • 服务器同样发送Finshed报文。
    • 服务器和客户端的Finished报文交换完毕之后,SSL连接就算建立完成。当然,通信会受到SSL的保护。从此处开始进行应用层协议的通信,即发送HTTP请求。
    • 应用层协议通信,即发送HTTP响应。
    • 最后由客户端断开连接。断开连接时,发送close_notify报文。这步之后再发送TCP FIN报文来关闭与TCP的通信。
  • HTTP/2

    • 二进制协议:
      HTTP/1.1版的头信息肯定是文本(ASCII编码),数据体可以是文本,也可以是二进制。HTTP/2则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧"(frame):头信息帧和数据帧。
    • 多工:
      HTTP/2复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了"队头堵塞"。
    • 数据流:
      HTTP/2的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。HTTP/2 将每个请求或回应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流。另外还规定,客户端发出的数据流,ID一律为奇数,服务器发出的,ID为偶数。
    • 头信息压缩:
      HTTP协议不带有状态,每次请求都必须附上所有信息。头信息使用gzip或compress压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
    • 服务器推送:
      HTTP/2允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送(server push)。

9. get和post的区别,tcp和udp的区别,七层网络模型

  • HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。
  • GET产生一个TCP数据包;POST产生两个TCP数据包。(对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据))
  • TCP和UDP的区别
    • TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前无需建立连接。
    • TCP提供可靠的服务。也就是说通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
    • TCP 面向字节流,实际上是TCP把数据看成是一连串无结构的字节流。UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)。
    • UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
    • 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。
    • TCP对系统资源要求较多,UDP对系统资源要求较少。
  • 客户端实现TCP编程一般步骤
    • 创建一个socket,用函数socket();
    • 设置socket属性,用函数setsockopt();可选
    • 绑定IP地址、端口等信息到socket上,用函数bind();可选
    • 设置要连接的对方的IP地址和端口等属性;
    • 连接服务器,用函数connect();
    • 收发数据,用函数send()和recv(),或者read()和write();
    • 关闭网络连接;
  • 客户端实现UDP编程一般步骤
    • 创建一个socket,用函数socket();
    • 设置socket属性,用函数setsockopt();可选
    • 绑定IP地址、端口等信息到socket上,用函数bind();可选
    • 设置对方的IP地址和端口等属性;
    • 发送数据,用函数sendto();
    • 关闭网络连接;
  • OSI七层网络模型:是一个标准,而非实现。
    • 物理层:底层数据传输,如网线;网卡标准。
    • 数据链路层:定义数据的基本格式,如何传输,如何标识;如网卡MAC地址。
    • 网络层:定义IP编址,定义路由功能;如不同设备的数据转发。
    • 传输层:端到端传输数据的基本功能;如 TCP、UDP。
    • 会话层:控制应用程序之间会话能力;如不同软件数据分发给不同软件。
    • 标识层:数据格式标识,基本压缩加密功能。
    • 应用层:各种应用软件,包括 Web 应用。
  • TCP/IP简化四层网络模型
    • 网络访问层:物理层,数据链路层。
    • 网络层
    • 传输层
    • 应用层:会话层,标识层,应用层

10.发送http请求的过程:

  • url解析,dns解析,tcp三次握手,发送http请求,服务器响应,浏览器解析,tcp四次挥手。
  • 过程完善:页面发送http请求前,先检查本地是否有缓存,缓存是否过期,另外就是发送的请求询问服务器资源是否过期,过期就请求新的资源。http有个option字段,做预检请求的,在请求之前都要先询问服务器,当前网页所在域名是否在服务器的许可名单中,预检通过再发送真正的请求。

11. 传递响应链

  • 事件的产生
    • 发生触摸事件后,系统会将事件加入到一个UIApplication管理的队列中,先产生的事件先处理。
    • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常先发送事件给应用程序的主窗口(keyWindow)。
    • 主窗口(keyWindow)会在视图层次结构中找到一个最合适的视图来处理触摸事件,寻找最合适的视图的关键就是hitTest:withEvent:方法。
  • 事件的传递(找到合适的view,hitTest:withEvent:)过程。事件的传递是自上到下的顺序,即UIApplication->window->处理事件最合适的view。
    • 首先判断主窗口(keyWindow)自己是否能接受触摸事件。判断触摸点是否在自己身上。
    • 子控件数组中从后往前遍历子控件,重复上一步的步骤。
    • 通过前面的步骤找到了fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的所有子控件,直到没有更合适的View。
    • 如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。userInteractionEnabled = NO,hiden = YES,alpha<=0.01都不能接收事件。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    // 1.判断下窗口能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil;
    // 2.判断下点在不在窗口上
    // 不在窗口上
    if ([self pointInside:point withEvent:event] == NO) return nil;
    // 3.从后往前遍历子控件数组
    int count = (int)self.subviews.count;
    for (int i = count - 1; i >= 0; i--)     {
        // 获取子控件
        UIView *childView = self.subviews[i];
        // 坐标系的转换,把窗口上的点转换为子控件上的点
        // 把自己控件上的点转换成子控件上的点
        CGPoint childP = [self convertPoint:point toView:childView];
        UIView *fitView = [childView hitTest:childP withEvent:event];
        if (fitView) {
            // 如果能找到最合适的view
            return fitView;
        }
    }
    // 4.没有找到更合适的view,也就是没有比自己更合适的view
    return self;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
//- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
//{
// return NO;
//}
  • 事件的响应(responderchain过程)
    • 我的理解hitTest的过程是将响应者对象(继承自UIResponder的对象)找出来并入栈的过程。将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理 (即调用super的touches方法),这是一个出栈执行的过程,从最后入栈的最合适的view开始执行。
    • 判断当前是否是控制器的View,如果是控制器的View,上一个响应者就是控制器
    • 如果不是控制器的View,上一个响应者就是父控件
    • 找到最合适的view会调用touches方法处理事件
    • 判断最合适的view是否处理事件(是否实现touches方法)–>没有实现默认会将事件传递给上一个响应者–>找到上一个响应者–>最终到keyWindow找不到方法作废。
//只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
// 上一个响应者可能是父控件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event]; 
// 注意不是调用父控件的touches方法,而是调用父类的touches方法
// super是父类 superview是父控件 
}

12. 通过一个view查找它所在的viewController

通过响应者链条机制找到当前view所属的控制器

#import "UIView+Tool.h"

@implementation UIView (Tool)
//通过响应者链条获取view所在的控制器
- (UIViewController *)parentController
{
    UIResponder *responder = [self nextResponder];
    while (responder) {
        if ([responder isKindOfClass:[UIViewController class]]) {
            return (UIViewController *)responder;
        }
        responder = [responder nextResponder];
    }
    return nil;
}
@end

13. 通过NSObject对象,获取当前控制器对象

#import "NSObject+Tool.h"
#import "UIView+Tool.h"
@implementation NSObject (Tool)
//通过展示window的布局视图可以获取到控制器实例对象    modal的展现方式需要取到控制器的根视图
- (UIViewController *)currentViewController
{
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    // modal展现方式的底层视图不同
    // 取到第一层时,取到的是UITransitionView,通过这个view拿不到控制器
    UIView *firstView = [keyWindow.subviews firstObject];
    UIView *secondView = [firstView.subviews firstObject];
    UIViewController *vc = [secondView parentController];
    
    if ([vc isKindOfClass:[UITabBarController class]]) {
        UITabBarController *tab = (UITabBarController *)vc;
        if ([tab.selectedViewController isKindOfClass:[UINavigationController class]]) {
            UINavigationController *nav = (UINavigationController *)tab.selectedViewController;
            return [nav.viewControllers lastObject];
        } else {
            return tab.selectedViewController;
        }
    } else if ([vc isKindOfClass:[UINavigationController class]]) {
        UINavigationController *nav = (UINavigationController *)vc;
        return [nav.viewControllers lastObject];
    } else {
        return vc;
    }
    return nil;
}
@end

14. 如何扩大view的响应范围

// 在view中重写以下方法,其中self.button就是那个希望被触发点击事件的按钮
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [super hitTest:point withEvent:event];
    if (view == nil) {
        // 转换坐标系
        CGPoint newPoint = [self.button convertPoint:point fromView:self];
        // 判断触摸点是否在button上
        if (CGRectContainsPoint(self.button.bounds, newPoint)) {
            view = self.deleteButton;
        }
    }
    return view;
}

15. 线程之间的通信方式

  • GCD实现线程通信
    //开启一个全局队列的子线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
            //1. 开始请求数据
            //...
            // 2. 数据请求完毕
            //我们知道UI的更新必须在主线程操作,所以我们要从子线程回调到主线程
        dispatch_async(dispatch_get_main_queue(), ^{

                //我已经回到主线程更新
        });

    });
    
        //线程延迟调用 通信
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSLog(@"## 在主线程延迟5秒调用 ##");
    });
  • perfermselecter选择器实现线程通信
    //数据请求完毕回调到主线程,更新UI资源信息  waitUntilDone    设置YES ,代表等待当前线程执行完毕
 [self performSelectorOnMainThread:@selector(dothing:) withObject:@[@"1"] waitUntilDone:YES];
  //将当前的逻辑转到后台线程去执行
[self performSelectorInBackground:@selector(dothing:) withObject:@[@"2"]];
        //当我们需要在特定的线程内去执行某一些数据的时候,我们需要指定某一个线程操作
    [self performSelector:@selector(dothing:) onThread:thread withObject:nil waitUntilDone:YES];
  • NSOperation实现线程通信
 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            self.imageView.image = image;
        }];

16. app进程之间的通信方式

  • URL Scheme:ios最常用的app通信方式,通过openURL方式进行跳转,可以携带参数
  • Keychain:系统地Keychain是一个安全的存储容器,它本质上就是一个sqllite数据库,它的位置存储在/private/var/Keychains/keychain-2.db,不过它所保存的所有数据都是经过加密的,可以用来为不同的app保存敏感信息。
  • UIPasteboard:剪切板功能
  • UIDocumentInteractionController:主要是用来实现同设备上app之间的共享文档,以及文档预览、打印、发邮件和复制等功能
  • local socket:一个App1在本地的端口port1234进行TCP的bind和listen,另外一个App2在同一个端口port1234发起TCP的connect连接,这样就可以建立正常的TCP连接,进行TCP通信了,那么就想传什么数据就可以传什么数据了。
  • AirDrop:通过AirDrop实现不同设备的App之间文档和数据的分享
  • UIActivityViewController:iOSSDK中封装好的类在App之间发送数据、分享数据和操作数据
  • App Groups:同一个开发团队开发的App之间,包括App和Extension之间共享同一份读写空间,进行数据共享。

17. 两个进程分别指向同一个地址空间并初始化一个值,分别输出是什么

物理地址:内存单元所看到的地址。逻辑地址(虚拟地址):CPU所生成的地址。每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)而值不同没什么奇怪。

18. 判断一个字符串是否所有的大写字母都在小写字母前面

循环对照ASCALL码表,AZ为65~90,az为97~122。从前到后遍历字符是否在‘A’~‘Z’区间,如果是继续执行循环。如果不是,判断是否在'a'~'z'中:如果不是,继续循环;如果是,标记临界点,继续循环,判断之后的字符是否在‘A’~‘Z’区间,是返回false,否则返回true。

19. 提高编译速度,减小编译生成的app的大小的方法。

项目瘦身

编译速度优化

  • 正确使用.pch文件
    • pch文件中尽量不要导入宏定义,我们都直到宏定义是去匹配的机制,如果全局匹配,无疑很耗费时间。最好写到每一部分头文件中。
    • pch文件主要进行加载的是一些比较大的文件。但是如果是我们自己写的一些比较大的文件,尽量不要导入太多,对编译速度本身是不好的。而且pch中的文件代码在多个项目中的可复用并不好。比较建议一些系统级常用框架。
  • 正确的import操作
    • 如B类用到A类,有时可能需要将A类在.h文件中声明一个属性,这个时候会在BClass.h中直接#import “AClass.h” ,这样做法是不合理的,因为编译时是不需要把AClass中的全部信息编译尽量,只需要直到该类被引入即可。所以此时使用·@class AClass
    • 另外如果引用链A -> B -> C -> D... ,如编译链中最后一个类发生更改,那么整个从A开始的相关类都要重新编译。
  • 打包静态链接库
    • 日常开发可以将我们不会修改或已经成熟的业务模块打包成.a静态库链接。打包成静态链接库后,生成的就是对应CPU架构下的二进制文件,编译时不会占用编译时间,运行时直接写入内存。当然打包静态库涉及release和debug版本,真机和模拟器兼容版本。
    • 采用cocoapod管理维护.a静态链接库。
  • 借助CCache工具:ccache是一个可以把编译中间产物缓存起来的工具,目前可以支持C、C++、Objective-C、Objective-C++。brew安装,xcode配置。在第一次启用ccache编译时因为所有文件都没有做过编译缓存,从第二次开始编译速度就会有所提升。
  • 控制换行,空白行的数量,控制方法的数量,目录深度不要太深等等。(倒觉得这些没必要,影响有限反而会波及到项目管理和代码规范)。

20. 程序执行的过程

  • 预处理(Prepressing) 预处理的过程,其实,主要是处理那些源代码中以#开始的预编译指令,hello.c->hello.i。比如#inclode,#define等,处理过程如下:
    • 将所有的#define删除,并且展开所有的宏定义。
    • 处理所有的条件预编译指令,比如#if,#ifdef,#elif,#else,#endif等。
    • 处理#include``#import预编译指令,将被包含的文件插入到该预编译指令的位置。在这个插入的过程,是递归进行的,也就是说被包含的文件,可能还包含其他文件。
    • 删除所有注释///***/
    • 添加行号和文件标识,以便编译时产生调试的行号及编译错误报警行号
    • 保留所有的#pragma 编译器指令,因为编译器需要使用他们。
  • 编译(Compilation)
    编译过程可分为6步:词法分析、语法分析、语义分析、源代码优化、代码生成、目标代码优化。hello.i -> hello.a
    • 词法分析:扫描器(Scanner)将源代码的字符序列分割成一系列的记号(Token)。
      注:lex工具,可实现按照用户描述的词法规则将输入的字符串分割为一个一个记号。
    • 语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。
      注:yacc工具(yacc: Yet Another Compiler Compiler)可实现语法分析,根据用户给定的语法规则对输入的记号序列进行解析,从而构建一个语法树,所以它也被称为“编译器编译器(Compiler Compiler)”。
    • 语义分析:编译器所分析的语义是静态语义,所谓静态语义就是指在编译期可以确定的语义,通常包括声明,和类型的匹配,类型的转换。
      注:与之对于的为动态语义分析,只有在运行期才能确定的语义。
    • 源代码优化:源代码优化器(Source Code Optimizer),在源码级别进行优化,例如(2+6)这个表达式,其值在编译期就可以确定。
    • 目标代码生成:代码生成器(Code Generator)。
    • 目标代码优化:目标代码优化器(Target Code Optimizer)。

最后的俩个步骤十分依赖与目标机器,因为不同的机器有不同的字长,寄存器,整数数据类型和浮点数据类型等。

  • 汇编(Assembly)

汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,所以根据汇编指令和机器指令的对照表一一翻译即可。汇编过程可以通过以下方式完成。-> hello.o。

  • 链接(Linking)

    • 静态链接
      把一个程序分成多个模块,把一个程序分割为多个模块,然后通过某种方式组合形成一个单一的程序,这就是链接。

      hello.o文件,既目标文件,是以分段的形式组织在一起的。其简单来说,把程序运行的地址划分为了一段一段的片段,有的片段是用来存放代码,叫代码段,这样,可以给这个段加个只读的权限,防止程序被修改;有的片段用来存放数据,叫数据段,数据经常修改,所以可读写;有的片段用来存放标识符的名字,比如某个变量 ,某个函数,叫符号表;等等。由于有这么多段,所以为了方便管理,所以又引入了一个段,叫段表,方便查找每个段的位置。

      当文件之间相互需要链接的时候,就把相同的段合并,然后把函数,变量地址修改到正确的地址上。这就是静态链接。

      • 假如有多个程序都链接一个相同的静态库,这样程序运行起来后,就有了多个副本。对于计算机的内存和磁盘的空间浪费比较严重。
      • 程序的更新,部署,和发布会带来很多麻烦。一旦程序任何位置有一个小小的改动,都会导致重新下载整个程序。
    • 动态链接
      我们的想法很简单,就是当第一个例子在运行时,在内存中只有一个副本;第二个例子在发生时,只需要下载更新后的lib,然后链接,就好了。那么其实,这就是动态链接的基本思想了:把链接这个过程推迟到运行的时候在进行。在运行的时候动态的选择加载各种程序模块,这个优点,就是后来被人们用来制作程序的插件(Plug-in)。

      动态链接器。它会在程序运行的时候,把程序中所有未定义的符号(比如调了动态库的一个函数,或者访问了一个变量)绑定到动态链接库中。

      可能有的人,就要问了,多个程序应用一个库不会有问题么?变量冲突?是这样的。动态链接文件,把那些需要修改的部分分离了出来,与数据放在了一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这种方案就是目前被称为地址无关代码(PIC,Position-independent Code)的技术。

    • 静态库

    一组相应目标文件的集合,我们称它为库。在Linux平台上,常以.a或者.o为拓展名的文件,我们最常用的C语言静态库,就位于/usr/lib/libc.a;而在Windows平台上,常以.lib为拓展名的文件,比如Visual C++附带的多个版本C/C++运行库,在VC安装的目录下的lib\目录。

    • 动态库

    一组相应目标文件的集合,我们称它为库。在Linux平台上,动态链接文件为称为动态共享对象(DSO,Dynamic Shared Objects),简称共享对象。他们一般常以.so为拓展名的文件;而在Windows平台上,动态链接文件被称为动态链接库(DLL,Dynamical Linking Library),通常就是我们常见的.dll为拓展名的文件。

  • 装载(Loading)
    其实目标文件,内部结构上来说和可执行文件的结构几乎是一样的,所以一般跟可执行文件格式一起用一种格式进行存储。总的来说,装载做了以下三件事情

    • 创建虚拟地址空间。
    • 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。
    • 将CPU的指令寄存器设置为运行库的初始函数(初始函数不止一个,第一个启动函数为:_start),初始了main()函数的环境,然后指向可执行文件的入口。

针对C/C++的GCC编译过程

21. 算法奇数排在前面,偶数排在后面

我之前的排序算法知识点里有写。juejin.cn/post/684490…

22. 下图是一颗四则运算树:x=(1+2)+((5*6)-7)+(3/4),请翻译成代码,并对树求值。

提示:可用对象描述每一个节点。

  • 运算树,首先想到通过递归实现,递归数据建模如下。
- (void)testTreeArithmetic{
    
    NSDictionary * root =@{
                           @"type":@"addition",
                           @"values":@[
                                      @{
                                          @"type":@"addition",
                                          @"values":@[
                                                  @{
                                                      @"type":@"number",
                                                      @"value":@"1",
                                                  },
                                                  @{
                                                      @"type":@"number",
                                                      @"value":@"2"
                                                      }
                                                  ]
                                      },
                                      @{
                                          @"type":@"subtraction",
                                          @"values":@[
                                                  @{
                                                      @"type":@"multiplication",
                                                      @"values":@[
                                                              @{
                                                                  @"type":@"number",
                                                                  @"value":@"5"
                                                              },
                                                              @{
                                                                  @"type":@"number",
                                                                  @"value":@"6"
                                                                  }
                                                              ]
                                                  },
                                                  @{
                                                      @"type":@"number",
                                                      @"value":@"7"
                                                      }
                                                  ]
                                       },
                                      @{
                                          @"type":@"division",
                                          @"values":@[
                                                  @{
                                                      @"type":@"number",
                                                      @"value":@"3"
                                                      },
                                                  @{
                                                      @"type":@"number",
                                                      @"value":@"4"
                                                      }
                                                  ]
                                       }
                               ],
                           };
    self.treeModel = [TreeModel mj_objectWithKeyValues:root];
    NSLog(@"-------------- %f", [self calc:self.treeModel]);
   
}
@interface TreeModel : NSObject

@property (nonatomic,copy) NSString * type;
@property (nonatomic,strong) NSArray * values;
@property (nonatomic,strong) NSString * value;
@end
  • 运算执行如下:
- (float)calc:(TreeModel *)tree{
    if ([tree.type isEqualToString:@"number"]) {
        return [tree.value floatValue];
        
    }else if ([tree.type isEqualToString:@"addition"]){
        float v = 0;
        for (TreeModel * model in tree.values) {
            v +=  [self calc:model];
        }
        return v;
        
    }else if ([tree.type isEqualToString:@"subtraction"]){
        float v =0;
        for (int i = 0; i<tree.values.count; i++) {
            TreeModel * model =[tree.values objectAtIndex:i];
            if (i == 0) {
                v = [self calc:model];
            }else{
                v -= [self calc:model];
            }
        }
        return v;
        
    }else if ([tree.type isEqualToString:@"multiplication"]){
        float v = 1;
        for (TreeModel * model in tree.values) {
            v *= [self calc:model];
        }
        return v;
        
    }else if ([tree.type isEqualToString:@"division"]){
        float v = 0;
        for (int i = 0; i<tree.values.count; i++) {
            TreeModel * model = [tree.values objectAtIndex:i];
            if (i == 0) {
                v = [self calc:model];
            }else
            {
                v /= [self calc:model];
            }
        }
        return v;
    }
    return 0;
}