技术博客阅读笔记iOS篇(一、MRPEAK)
每天阅读那么多的博客,为什么自己对其中优秀的博客做一个笔记方便自己翻阅呢。而且牛逼的博主,其他文章含量金量也很高。虽然看博客不如看技术书籍来的全面、系统,读完之后也有不少的收获。关于泛读和精读,下面有篇文章有相应的介绍……
博主MRPEAK
一下内容大部分是摘自博客原文,如需细致阅读,建议看原文。
历史文章合集
一、 如何设计一个通讯协议
幸好作者提供了项目源码,可以直接看源码TKeyboard
google一搜如何设计一个通讯协议,出现了一大片文章。这里摘取比较有代表性的。
找到一个牛逼哄哄的网站即时通信网,做了IM这么久现在才知道有这样好资源。
理论联系实际:一套典型的IM通信协议设计详解
如何设计一个RPC系统
应用层常见的有三种:文本协议(Http)、二进制协议(ip)、流式XML协议(xmpp)。他们的各自优缺点可以大致总结出来。后文建议强列建议将Protobuf作为你的即时通讯应用数据传输格式
工作内容主要有:一是数据的序列化,即将我们平时所用的 model 转化为二进制流,其二是定义好包的格式,在通讯框架里做好包的切割,解析,和传递。最后简化调用流程,提供一套简单的类似 http 的双向数据调用 API 即可。
- 基本思路
- 第一个序列化的问题好解决,已有 google 的成熟方案 protobuf 可以使用(感觉业界都推荐用这个,但是之前在iOS端用过,发现了很多坑爹的地方),而且还有基于 Objective C 的版本,model 的序列化和反序列化,一个 API 调用即可完成。
- 第二个问题是包的格式定义。学习 TCP/IP 的意义在这里就能体现了,无论是 TCP 包还是 IP 包,都有自己的包格式定义,而且往往是一个 header 配合一个 payload(类似于 http 的 body)。之所以要有包,是因为二进制流只完成 stream 的传输,并不知道一次数据请求与相应的起始和结束,我们要预先定义好包结构才能做解析。
- 要能实现包的准确切割,我们需要明确包的长度。所以必须在 header 中留一个字段,表达整个包(header + payload)由多少 bytes 构成,两个字节的长度就可以描述 0~65535 个字节数,具体使用多少个字节就看协议的使用场景了。
- 因为是 RPC 调用协议,所以包体里必须有调用的名称,即 API name 字段,这个 name 是可变长度,所以也需要将其长度信息加入包体中,原则上,所有可变长度的内容都需记录其精确的长度信息,否则无法做信息的切割。另外,调用方还需要知道包是请求的回应(response)还是另一端的通知(notify),所以我们还需要定义 call type 信息,这种信息一个 8 比特位的枚举量就绰绰有余了,这种固定长度的信息就不需要记录其长度信息了。
- 一般固定长度的信息我们放在 header 中,可变长度的信息我们则放入 payload 中,当然,我们 RPC 调用的具体参数(经由 protobuf 序列化之后的 stream)也是放入 payload 中,接收方接收以后,只需读取固定长度的字节,即可通过反序列化,再在接收方还原成具体的应用层数据。
特别注意:因为我们是在设计应用层协议,所以还需要考虑传输层是可靠还是不可靠,CoreBlueTooth 实际上既提供了类似于 TCP 的可靠传输(CBCharacteristicPropertyIndicate),也有类似于 UDP 的不可靠传输(CBCharacteristicPropertyNotify),不明白这一点,必然会踩坑
二、TCP/IP 系列之重新认识 IP 地址
大学里面网络基础学过,但是基本上都忘记了。看到这篇突然想起了一些远古
的记忆。
internet 其实是由无数个子网所构成,是一个二级的结构,第一级是子网,第二级才是子网中的设备。所以 internet 中设备 A 的信息要抵达设备 B,必须先要找到 B 所在的子网,进而再在子网中找到 B。
IP 地址的结构:IP 地址 = 网络地址 + 主机地址。子网掩码(subnet mask) 就是为了分割 Network ID 和 Host ID 的
第一种切割方式
- 第一个字节为 Network ID,剩下三个字节为 Host ID
- 第二个字节为 Network ID,剩下两个字节为 Host ID
第三个字节为 Network ID,剩下一个字节为 Host ID
有问题我们如何确定一个 IP 地址是属于 A B C 的哪一类呢?我们以第一个字节来做一些约定:
- 如果第一个字节的起始比特位为 0,则是 A 类地址。
- 如果第一个字节的起始比特位为 10,则是 B 类地址。
- 如果第一个字节的起始比特位为 110,则是 C 类地址。
第二种切割方式CIDR
全称为 Classless Inter-Domain Routin。CIDR 是新的子网掩码的表达方式和路由方式。这里注意 CIDR 和 CIDR notation 的区别,CIDR notation 是描述 IP 地址如何切割的方式,而 CIDR 描述的是基于 CIDR notation 的路由方式。
CIDR notation 其实概念也很直白,它不再粗暴的以字节为粒度来切分 IP 地址,而是精确到 bit 位,我们看一个典型的 CIDR notation:
|
|
注意 IP 地址后面的 /23,这就是 CIDR notation,它表示 IP 地址的前 23 bits 为 Network ID,剩余的 9 bits 为 Host ID。23 并不是 8 的倍数,我们将切分的精读提高到了 bit。我们可以通过简单的位运算,得到具体的 Network ID 和 Host ID,我们将 IP 地址和 /23 先转为二进制:
|
|
得到 Network ID 和 Host ID:
|
|
再将二进制转换为十进制,我们就得到了便于理解的 Network ID:123.121.114.0。由于 Host ID 占用 9 个 bits,这个子网里一共可以有 2 的 9 次方个主机数,也就是 512 个主机,这个子网网段的起始地址为 123.121.114.0,结束地址为 123.121.115.255。我们对于某一个网段内的 IP 地址,有个约定,第一个地址为 Network ID,最后一个地址是该子网内的 Broadcast ID,那么剩下的可用于子网内设备的 IP 地址数量就是 510 个了。
IP 地址虽然只是一个二级的结构,但 IP 地址的分配却是一层一层,经历多层往下分发的,由一个国际机构 IANA 统一分配。具体规则可以参考 IANA 官网:https://www.iana.org/numbers
用户拿到 IP 地址后,所发送包要经过一个个的路由器才能抵达正确的地址。
三、TCP/IP 系列之 Header 篇
如果以一个 HTTP 请求为例,右图中 Application 部分就代表我们用 Charles 抓包时所感知的部分,这一部分要最后转化为光信号,在光纤中传输,还需要经过一层层的转化,这个转化过程说白了,就是在每一层加上一个 header。
- Application 层(HTTP)的数据在经过传输层(TCP Layer)的时候,会加上 TCP 的 header,成为一个 TCP Segment。
- 传输层(TCP)的 Segment 在经过网络层(IP Layer)的时候,会加上 IP 的 header,成为一个 IP Packet。
- 网络层的 IP Packet 在经过链路层(Link Layer)的时候,会加上Link Layer 的 header,成为一个 Frame。
- 最后 Frame 会在物理层,将数字信号转化为物理信号传输。
看张图片一切就明了了
深入研究可以看TCPdump抓包命令详解
Linux tcpdump命令详解
四、TCP/IP 系列之初印象
0 和 1 是计算机世界的基础粒子,大量的 0 和 1 组合在一起就形成了一个流(Stream),客户端向服务器发送数据的时候,说白了就是一堆 0 和 1 的组合。一次完整的 http 会话是建立在一个 TCP 连接之上,这个 TCP 连接的生命周期内所有发送的数据最后可以看做是一个流。而在这个流里,我们可以按照某种规则把它切割成一个个的包(packet)。比如 TCP 三次握手里就包含了 SYN,SYN+ACK,ACK 三个包,而这三个包,不过是整个 TCP Stream 最开始的部分数据而已。
所以简单来说,一个 TCP 连接里,是既有流的概念,又有包的存在,有些问题场景下会谈论流,另一些则会说起包,端看具体的场景如何。
客户端和服务器之间有两根管道,一根上行(从客户端到服务器),一根下行(从服务器到客户端),管道里流动着无数的 0 和 1,有时候管道里是满的,有时候管道里则空空如也,每次发送数据,都会有大量的 0 和 1 从一端涌向另一端。
延迟
由两部分组成的,其一是 Transmission Delay,另一个是 Propagation Delay。
Transmission Delay。计算机世界里的 0 和 1, 最后要能在光纤中传输,需要在 Physical Layer 将数字信号转化为物理信号,这个转化也是存在速度瓶颈的,我们用 Rate (bits/seconds) 来描述这个转化的速度,Rate 表示每一秒钟里,硬件设备能将多少 bits 转化为光信号放入光纤中。
Propagation Delay。这才是大部分人所理解的传播延迟,和距离直接相关。
五、技术文章的阅读姿势🍎
作者和自己的感想比较相似,关于泛读和精读给出了自己的解释。对于自己而言,确实是这样,总结出来的经验也非常值得自己借鉴。
由于技术的知识体系往往是个树形的结构,单个术语下都有其相关的知识域,可以一层又一层牵扯出更多的子术语。在阅读文章遭遇这种树形结构的时候,要能抑制住自己不停探索的欲望,对于技术术语的学习只做适度延伸,最终的目的还是在于完成根部文章的阅读。尽量只做一到两层的延伸。
尽量选择没人打扰的时间段来做阅读,可以是早上刚到公司,或者别人午睡时,总之越安静,越没人找越好。
对于基础知识的阅读,要重官方文档,切莫心急动手,看完文档形成知识体系后再写代码不迟。减少泛读行为,避免漫无目的的随意浏览技术文章。注重精读,一天一篇不算少,一周一篇也正常。重阅读质量而非数量,挑选每天安静且不易被打断的时间点来阅读,尽量多啃原版书。
今天的阅读就到这里了 2017-4-5 pm5:22 下次继续更新
时隔一个多月
六、闲聊 Hash 算法
数据结构和算法是相辅相成的,基础的其实就那么些:时间复杂度的概念,List,Array,Stack,Queue,Tree 等。Graph 实际应用中较少遇到,可以不做深入了解,但 BFS,DFS,Dijkstra 还是应该知道。基础的算法需要能达到手写的程度,比如排序至少能写出两种时间复杂度为 N*logN 的算法。理解这些比去 leetcode 刷题重要,学习难度也并不高。学习这些的意义在于掌握解决问题的基础思路,形成计算机思维,比如 divide and conque,recursive 等常规思想。
七、Improving Immutable Object Initialization in Objective-C
- Initializer mapping arguments to properties
- Initializer taking dictionary
- Mutable subclass
- Improving builder pattern
八、危险的UITableView
总体来说就是调用了tableView的reloadData方法之后,代理方法有些不是同步执行的。具体来讲。
|
|
优化方案
throttle机制,控制刷新事件的产生频率,建立一个Queue以一定的时间间隔来调用reloadData。事实上这是一种很常见的界面优化机制,对于一些刷新频率可能很高的列表界面,比如微信的会话列表界面,如果很长时间没有登录了,打开App时,堆积了很久的离线消息会在短时间内,导致大量的界面刷新请求,频繁的调用reloadData还会造成界面的卡顿,所以此时建立一个FIFO的Queue,以一定的间隔来刷新界面就很有必要了。
九、iOS当中的Cache设计
需要看看 一个简单的 Cache 淘汰策略
副作用的理解:
所有对我们整个App有副作用的代码都需要被集中管理,要能从架构的层面去理解和定位。怎么去定义副作用呢?可以抽象成一种「写操作」,往Cache中添加新的记录就是写操作,这种写操作的副作用是额外的内存开销,Cache的本质是以空间换时间,这空间损耗就是我们的副作用,一个副作用会引发其他更多的副作用,理清这些副作用往往需要反复查阅大量的代码。更好的办法是,一开始就把有副作用的代码集中管理。
- cache的另一个重要知识点是cache的淘汰策略,不同的策略表现也不一样,FIFO,LRU,2Queues等等,现在有不少成熟的第三方cache框架可以使用,系统也提供了淘汰策略不明确的NSCache。
cache的使用要有收有放,不能只创建不释放,事实上,所有涉及到data的操作都要考虑data的生命周期。我们做业务的时候,多是以Controller为基础单位,有些场景下,一个Controller在退出之后被再次进入的可能性就非常之低了,适时的清理cache会让我们App的整体表现更好。
只要保证业务模块从Cache中获取的数据都是独立的copy,就能避免数据共享带来的各种隐患。
最后看一下YYCache加深学习
十、如何用Xcode8解决多线程问题
data race:当至少有两个线程同时访问同一个变量,而且至少其中有一个是写操作时,就发生了data race。
常见场景
场景一:计算出错
123456789__block int count = 0;dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{for (int i = 0; i < 10000; i ++) {count ++;}});for (int i = 0; i < 10000; i ++) {count ++;}场景二:Crash!
123456789NSMutableString* str = [@"" mutableCopy];dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{for (int i = 0; i < 10000; i ++) {[str setString:@"1234567890"];}});for (int i = 0; i < 10000; i ++) {[str setString:@"abcdefghigk"];}
一般会出现在对于复杂对象(class或者struct)的多线程写操作中,原因是因为写操作本身不是原子的,而且写操作背后会调用更多的内存操作,多线程同时写时,会导致这块内存区间处于中间的不稳定状态,进而crash,这是真正的恶性的data race。
场景三:乱序
12345678//thread 1count = 10;countFinished = true;//thread 2while (countFinished == false) {usleep(1000);}NSLog(@"count: %d", count);
公共变量线程同步。error
编译器并不知道thread 2对count和countFinished这两个变量的赋值顺序有依赖,所以基于优化的目的,有可能会调整thread 1中count = 10;和countFinished = true;生成的最后指令的执行顺序,最后也就导致count值输出的时机不对,虽然最后count的值还是10。这也可以看做是一种benign race,因为也不会crash,而是程序的流程出错。
遇到这种多线程读写状态,而且存在顺序依赖的场景,不能简单依赖代码逻辑。解决这种data race场景有一个简单办法:加锁,比如使用NSLock,将对顺序有依赖的代码块整个原子化,加锁之所以有用是因为会生成memory barrier,从而避免了编译器优化。
场景四:内存泄漏(存在静态变量的时候)
1234567Singleton *getSingleton() {static Singleton *sharedInstance = nil;if (sharedInstance == nil) {sharedInstance = [[Singleton alloc] init];}return sharedInstance;}
多线程环境下,thread A和thread B会同时进入sharedInstance = [[Singleton alloc] init];,Singleton被多创建了一次,MRC环境就产生了内存泄漏。
顶层的还是不够牢固。哎!恶补
十一、iOS多线程到底不安全在哪里?
self.userName = @"123";
是在对指针本身进行赋值[self.userName rangeOfString:@"123"];
是在访问指针指向的字符串所在的内存区域,这二者并不一样
属性类型。基本类型、指针类型
至始至终只有三种:1.值类型Property、2.指针Property、3.指针Property指向的内存区域(这一类多线程的访问场景是我们很容易出错的地方)
atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。另外,atomic由于加锁也会带来一些性能损耗,所以我们在编写iOS代码的时候,一般声明property为nonatomic,在需要做多线程安全的场景,自己去额外加锁做同步。
多线程安全的时候,其实是在讨论多个线程同时访问一个内存区域的安全问题。针对同一块区域,我们有两种操作,读(load)和写(store),读和写同时发生在同一块区域的时候,就有可能出现多线程不安全。
多线程是如何同时访问内存的。不考虑CPU cache对变量的缓存,内存访问可以用下图表示:
只有一个地址总线,一个内存。即使是在多线程的环境下,也不可能存在两个线程同时访问同一块内存区域的场景,内存的访问一定是通过一个地址总线串行排队访问的
结论一:内存的访问时串行的,并不会导致内存数据的错乱或者应用的crash。
结论二:如果读写(load or store)的内存长度小于等于地址总线的长度,那么读写的操作是原子的,一次完成。比如bool,int,long在64位系统下的单次读写都是原子操作。
atomic作用:
- 用处一: 生成原子操作的getter和setter。设置atomic之后,默认生成的getter和setter方法执行是原子的。也就是说,当我们在线程1执行getter方法的时候(创建调用栈,返回地址,出栈),线程B如果想执行setter方法,必须先等getter方法完成才能执行。举个例子,在32位系统里,如果通过getter返回64位的double,地址总线宽度为32位,从内存当中读取double的时候无法通过原子操作完成,如果不通过atomic加锁,有可能会在读取的中途在其他线程发生setter操作,从而出现异常值。如果出现这种异常值,就发生了多线程不安全。
- 用处二:设置Memory Barrier。对于Objective C的实现来说,几乎所有的加锁操作最后都会设置memory barrier,atomic本质上是对getter,setter加了锁,所以也会设置memory barrier。memory barrier能够保证内存操作的顺序,按照我们代码的书写顺序来。听起来有点不可思议,事实是编译器会对我们的代码做优化,在它认为合理的场景改变我们代码最终翻译成的机器指令顺序。
问题代码:值类型Property
|
|
即使我将intA声明为atomic,最后的结果也不一定会是20000。原因就是因为self.intA = self.intA + 1;不是原子操作,虽然intA的getter和setter是原子操作,但当我们使用intA的时候,整个语句并不是原子的,这行赋值的代码至少包含读取(load),+1(add),赋值(store)三步操作,当前线程store的时候可能其他线程已经执行了若干次store了,导致最后的值小于预期值。
问题代码:指针Property
|
|
如果property为nonatomic,上述的setter方法就不是原子操作,我们可以假设一种场景,线程1先通过getter获取当前_userName,之后线程2通过setter调用[_userName release];,线程1所持有的_userName就变成无效的地址空间了,如果再给这个地址空间发消息就会导致crash,出现多线程不安全的场景。
问题代码:指针Property指向的内存区域
|
|
虽然stringA是atomic的property,而且在取substring的时候做了length判断,线程B还是很容易crash,因为在前一刻读length的时候self.stringA = @”a very long string”;,下一刻取substring的时候线程A已经将self.stringA = @”string”;,立即出现out of bounds的Exception,crash,多线程不安全。