100 Days, 100 Blogs

一直以来都活跃在微博、简书等各大网站搜罗技术博客,每当看到大牛发布或者转发技术博客都会点进去搂一眼,内容比较好或者比较感兴趣的话就会转发收藏。
但这些都是零零碎碎的,大多时候都是路上、睡觉前或者什么间隙看到一篇好文章,简单看一下就收藏了,当时来不及 想着以后细读,但往往是在这之后基本上不会再次翻阅了。所以计划从今天起每天仔细阅读一篇技术博客,在这里做一做笔记。

微博每日打卡:#100days,100blogs#

# 100天,这么快,结束了……

# Day 100 : AFNetworkReachabilityManager 监控网络状态(四)

Draveness 发布

– AFNetworkReachabilityManager 实际上只是一个对底层 SystemConfiguration 库中的 C 函数封装的类,它为我们隐藏了 C 语言的实现,提供了统一的 Objective-C 语言接口
– 它是 AFNetworking 中一个即插即用的模块

# Day 99 : 处理请求和响应 AFURLSerialization(三)

Draveness 发布

– AFURLResponseSerialization 负责对返回的数据进行序列化
– AFURLRequestSerialization 负责生成 NSMutableURLRequest,为请求设置 HTTP 头部,管理发出的请求

# Day 98 : AFNetworking 的核心 AFURLSessionManager(二)

Draveness 发布

– AFURLSessionManager 是对 NSURLSession 的封装
– 它通过 - [AFURLSessionManager dataTaskWithRequest:completionHandler:] 等接口创建 NSURLSessionDataTask 的实例
– 持有一个字典 mutableTaskDelegatesKeyedByTaskIdentifier 管理这些 data task 实例
– 引入 AFURLSessionManagerTaskDelegate 来对传入的 uploadProgressBlock downloadProgressBlockcompletionHandler 在合适的时间进行调用
– 实现了全部的代理方法来提供 block 接口
– 通过方法调剂在 data task 状态改变时,发出通知

# Day 97 : AFNetworking 概述(一)

Draveness 发布

– 如何使用 NSURLSession 发出 HTTP 请求
– 如何使用 AFNetworking 发出 HTTP 请求

# Day 96 : iOS 源代码分析 — SDWebImage

Draveness 发布

总结,SDWebImage 的图片加载过程其实很符合我们的直觉:

  1. 查看缓存
  2. 缓存命中
    2.1. 返回图片
    2.2. 更新 UIImageView
  3. 缓存未命中
    3.1. 异步下载图片
    3.2. 加入缓存
    3.3. 更新 UIImageView

# Day 95 : 深入理解哈希表

bestswifter 发布

总结:
有两个字典,分别存有 100 条数据和 10000 条数据,如果用一个不存在的 key 去查找数据,在哪个字典中速度更快?
完整的答案是:
在 Redis 中,得益于自动扩容和默认哈希函数,两者查找速度一样快。在 Java 和 Objective-C 中,如果哈希函数不合理,返回值过于集中,会导致大字典更慢。Java 由于存在链表和红黑树互换机制,搜索时间呈对数级增长,而非线性增长。在理想的哈希函数下,无论字典多大,搜索速度都是一样快。

# Day 94 : 史上最详细的iOS之事件的传递和响应机制-原理篇

VV木公子 发布

# Day 93 : iOS-Summary-Part1

liberalisman 发布

– 01-iOS程序启动过程
– 02-浅拷贝-深拷贝
– 03-View的生命周期
– 04-@property
– 05-事件传递和事件响应
– 06-KVC
– 07-KVO
– 08-iOS数据持久化方案

# Day 92 : iOS基础问答面试题连载(一)-附答案

IOS开发 微信公众号 发布

# Day 91 : 《招聘一个靠谱的 iOS》—参考答案(三)

iOS程序犭袁 发布

# Day 90 : 《招聘一个靠谱的 iOS》—参考答案(二)

iOS程序犭袁 发布

# Day 89 : 《招聘一个靠谱的 iOS》—参考答案(一)

iOS程序犭袁 发布

# Day 88 : iOS 面试大全从简单到复杂(简单篇)

kissGod 发布

接下来几篇面试题打打基础,没什么要总结的。

# Day 87 : WWDC2016 Session笔记 - iOS 10 UICollectionView新特性

halfrost 发布

– UICollectionView cell pre-fetching 预加载机制
– UICollectionView and UITableView prefetchDataSource 新增的API
– 针对self-sizing cells 的改进
– Interactive reordering

# Day 86 : 深入剖析 iOS 性能优化

戴铭(ming1016) 发布

  1. 时间复杂度
    – NSArray / NSMutableArray
    – NSSet / NSMutableSet / NSCountedSet
    – NSDictionary / NSMutableDictionary
    – containsObject 方法在数组和 Set 里不同的实现
  2. 用 GCD 来做优化
    – 异步处理事件
    – 需要耗时长的任务
    – 避免线程爆炸
    – GCD 相关 Crash 日志
  3. I/O 性能优化
    – NSCache
  4. 控制 App 的 Wake 次数

通过自动化代码检查的方式来避免问题
一系列牛逼操作……

# Day 85 : 检测iOS的APP性能的一些方法

戴铭(ming1016) 发布

– Time Profiler
– Allocations
– Leak
– 开发时需要注意如何避免一些性能问题:
– NSDateFormatter
– UIImage
– 页面加载
– 优化首次加载时间
– 监控卡顿的方法
– 堆栈dump的方法

# Day 84 : 深入理解Objective-C:方法缓存

美团点评技术团队 发布

objc_msgSend(就arm平台而言)的消息分发分为以下几个步骤:

  1. 判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象
  2. 从缓存里寻找,找到了则分发,否则
  3. 利用objc-class.mm中_class_lookupMethodAndLoadCache3(为什么有个这么奇怪的方法。本文末尾会解释)方法去寻找selector
    3.1. 如果支持GC,忽略掉非GC环境的方法(retain等)
    3.2. 从本class的method list寻找selector,如果找到,填充到缓存中,并返回selector,否则
    3.3. 寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector,否则
    3.4. 调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则
    3.5. 转发这个selector,否则
  4. 报错,抛出异常

# Day 83 : ReactiveCocoa核心元素与信号流

美团点评技术团队 发布

  1. RAC核心元素与管线
    1.1. 管线的设计 - createSignal:
    1.2. 管线工人 - Subscriber:
    1.3. 启动管线 - subscribe:
  2. RAC信号流
    2.1. bind函数会返回一个新的信号N。
    2.2. flattenMap:在RAC的使用中,flattenMap这个操作较为常见。事实上flattenMap是对bind的包装,为bind提供bindBlock。
    2.3. map :map操作可将原信号输出的数据通过自定义的方法转换成所需的数据, 同时将变化后的数据作为新信号的输出。它实际调用了flattenMap, 只不过中间信号是直接将mapBlock处理的值返回。
    2.4. flatten: 该操作主要作用于信号的信号。flatten内部调用了flattenMap ,flattenMap里对应的中间信号就是原信号O输出signalValue。
    2.5. switchToLatest:与flatten相同,其主要目的也是用于”压平”信号的信号。
    2.6. scanWithStart : 该操作可将上次reduceBlock处理后输出的结果作为参数,传入当次reduceBlock操作,往往用于信号输出值的聚合处理。
    2.7. throttle:这个操作接收一个时间间隔interval作为参数,如果Signal发出的next事件之后interval时间内不再发出next事件,那么它返回的Signal会将这个next事件发出。
  3. RACCommand
  4. RACChannel

# Day 82 : ReactiveCocoa中潜在的内存泄漏及解决方案

美团点评技术团队 发布

– 对 RACObserve 使用 @weakify 和 @strongify 避免循环引用
– 使用ReactiveCocoa必须要保证信号发送完成或者发送错误,避免内存泄漏。

# Day 81 : 细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号

美团点评技术团队 发布

给出了冷信号转换成热信号的方法。

# Day 80 : 细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号

美团点评技术团队 发布

举例说明冷信号在发送网络请求时可能引发的问题。

# Day 79 : 细说ReactiveCocoa的冷信号与热信号(一)

美团点评技术团队 发布

- [RACSignal publish]- [RACMulticastConnection connect]- [RACMulticastConnection signal]这几个操作生成了一个热信号。

冷热信号的如下特点:
– 热信号是主动的,即使你没有订阅事件,它仍然会时刻推送。如第二个例子,信号在50秒被创建,51秒的时候1这个值就推送出来了,但是当时还没有订阅者。
– 而冷信号是被动的,只有当你订阅的时候,它才会发送消息。如第一个例子。

– 热信号可以有多个订阅者,是一对多,信号可以与订阅者共享信息。如第二个例子,订阅者1和订阅者2是共享的,他们都能在同一时间接收到3这个值。
– 而冷信号只能一对一,当有不同的订阅者,消息会从新完整发送。如第一个例子,我们可以观察到两个订阅者没有联系,都是基于各自的订阅时间开始接收消息的。

# Day 78 : RACSignal的Subscription深入分析

美团点评技术团队 发布

主要分析RACSignal的subscription过程。

# Day 77 : [ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2

](https://www.raywenderlich.com/62796/reactivecocoa-tutorial-pt2)

raywenderlich.com 发布

In this, the second part of the series, you’re going to learn about the more advanced features of ReactiveCocoa. Including:
– Validating the Search Text
– Formatting of Pipelines
– Memory Management
– Avoiding Retain Cycles
– Requesting Access to Twitter
– Chaining Signals
– Searching Twitter
– Threading
– Updating the UI
– Asynchronous Loading of Images
– Throttling
– Wrap Up

# Day 76 : ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2

raywenderlich.com 发布

With the first installment of this two-part ReactiveCocoa tutorial series you learned how to replace standard actions and event handling logic with signals that emit streams of events. You also learned how to transform, split and combine these signals.
– Time To Play
– A Little Cast
– What’s An Event?
– Creating Valid State Signals
– Combining signals
– Reactive Sign-in
– Creating Signals
– Signal of Signals
– Adding side-effects
– Conclusions

# Day 75 : MVVM With ReactiveCocoa

雷纯锋的技术博客 发布

  1. 介绍了 MVC 及 MVVM 设计模式
  2. 介绍了 MVVMReactiveCocoa 开源项目
    2.1. viewModel 通过调用 MRCViewModelServicesImpl 中的空操作来表明需要执行相应的导航操作,而 MRCNavigationControllerStack 则通过 Hook 来捕获这些空操作,然后使用栈顶的 NavigationController 来执行真正的导航操作。
    2.2. 配置了一个从 viewModel 到 view 的映射,并且约定了一个统一的初始化 view 的方法 initWithViewModel:

# Day 74 : ReactiveCocoa v2.5 源码解析之架构总览

雷纯锋的技术博客 发布

介绍了 ReactiveCocoa 的四大核心组件,对它的架构有了宏观上的认识。

  1. 信号源
    1.1. RACStream
    1.2. RACSignal
    1.3. RACSubject
    1.4. RACSequence
  2. 订阅者
    2.1. RACSubscriber
    2.2. RACMulticastConnection
  3. 调度器
    3.1. RACScheduler
  4. 清洁工
    4.1. RACDisposable

# Day 73 : 美团App iOS开发与FRP

臧成威的专栏公众平台 发布

介绍了 MVVM 设计模式及函数响应式编程(Functional Reactive Programming,简称FRP)的使用。

# Day 72 : iOS 开发中,怎样用好 Notifications?

故胤道长 发布

目录:
– UserNotifications 介绍
– 本地通知(Local Notifications)
– 远程通知(Remote Notifications)
– 观察者模式(Observer Pattern)

# Day 71 : NSNotificationCenter实现原理?

Vein_ 发布

– NSNotificatinonCenter是使用观察者模式来实现的用于跨层传递消息,用来降低耦合度。
– NSNotificatinonCenter用来管理通知,将观察者注册到NSNotificatinonCenter的通知调度表中,然后发送通知时利用标识符name和object识别出调度表中的观察者,然后调用相应的观察者的方法,即传递消息(在Objective-C中对象调用方法,就是传递消息,消息有name或者selector,可以接受参数,而且可能有返回值),如果是基于block创建的通知就调用NSNotification的block。

# Day 70 : iOS 性能优化:Instruments 工具的救命三招

LeanCloud Blog 发布

  1. Time Profiler。Call Tree 的选项设置:
    1.1. Separate by Thread:按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。
    1.2. Invert Call Tree:反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
    1.3. Hide Missing Symbols:隐藏缺失符号。如果 dSYM 文件或其他系统架构缺失,列表中会出现很多奇怪的十六进制的数值,用此选项把这些干扰元素屏蔽掉,让列表回归清爽。
    1.4. Hide System Libraries:隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。
    1.5. Flattern Recursion:拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。
    1.6. Top Functions:找到最耗时的函数或方法。
  2. Allocations:内存分配
  3. Leaks:内存泄漏

# Day 69 : 放肆的使用UIBezierPath和CAShapeLayer画各种图形

J0hnnny 发布

评论区发现很有趣有用的Demo:BezierPath

# Day 68 : APP 缓存数据线程安全问题探讨

bang’s blog 发布

解决方案:
– 1.加锁
– 2.分线程cache
– 3.数据不可变

对于 APP 缓存数据线程安全问题,分线程 cache 和数据不可变是比较常见的解决方案,都有着不同的实现代价,分线程 cache 接口不友好,数据不可变需要配合单向数据流之类的规则或框架才会变得好用,可以按需选择合适的方案。

# Day 67(yesterday) : 优化 App 的启动时间

玉令天下的博客 发布

这是一篇 WWDC 2016 Session 406 的学习笔记,从原理到实践讲述了如何优化 App 的启动时间。

测量启动时间:
在 main() 方法执行前测量是很难的,好在 dyld 提供了内建的测量方法:在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 1。

# Day 66 : 今日头条iOS客户端启动速度优化

今日头条技术博客 发布

  1. main()调用之前的加载过程
    1.1. 什么是image
    1.2. 系统使用动态链接有几点好处
    1.3. 什么是ImageLoader
    1.4. 动态链接库加载的具体流程
    1.4.1. load dylibs image
    1.4.2. rebase/bind
    1.4.3. Objc setup
    1.4.4. initializers
  2. main()之前的加载时间如何衡量
    总结一下:对于main()调用之前的耗时我们可以优化的点有:
    2.1. 减少不必要的framework,因为动态链接比较耗时
    2.2. check framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查
    2.3. 合并或者删减一些OC类
    2.3.1. 删减一些无用的静态变量
    2.3.2. 删减没有被调用到或者已经废弃的方法
    2.3.3. 将不必须在+load方法中做的事情延迟到+initialize中
    2.3.4. 尽量不要用C++虚函数(创建虚函数表有开销)
  3. main()调用之后的加载时间

# Day 65 : IOS App 启动优化

我爱水果 发布

  1. App总启动时间 = t1(main()之前的加载时间) + t2(main()之后的加载时间)。
  2. t1 = 系统dylib(动态链接库)和自身App可执行文件的加载;
  3. t2 = main方法执行之后到AppDelegate类中的- (BOOL)Application:(UIApplication )Application didFinishLaunchingWithOptions:(NSDictionary )launchOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示。
  4. main()调用之前加载过程,优化内容
    4.1. 减少framework引用
    4.2. 删除无用类,无用函数
    4.3. 减少+load 函数使用
  5. main()调用之后, 优化内容
    5.1. launcherImage图片尽量小,实测这个大小会影响启动速度
    5.2. Splash 不要Xib,直接用代码尽量简单
    5.3. 将需要执行的处理,放入不同的block内,并发到不同的queue中进行。
    5.4. 提供串行队列,执行有依赖的逻辑
    5.5. 提供group,对彼此依赖不明确,但需要整体执行完成后,进行处理的业务,提供dispatch_group功能满足需求。
    5.6. 对于MainThread有需要的业务,提供mainThread 支持。

# Day 64 : 深入理解 weak-strong dance

Sheepy 发布

– 问:self指向的对象已经被废弃的情况下,_handler成员变量也不存在了,在 ARC 下会自动释放它指向的 Block 对象,这个时候 Block 对象应该已经没有被变量所持有了,它的引用计数应该已经为0了,它应该被废弃了啊,为什么它还能继续存在并执行?
– 答:把 Block 赋值给self.handler的时候,在栈上生成的 Block 被复制了一份,放到堆上,并被_handler持有。而之后如果你把这个 Block 当作 GCD 参数使用(比较常见的需要使用 weak-strong dance 的情况),GCD 函数内部会把该 Block 再 copy 一遍,而此时 Block 已经在堆上,则该 Block 的引用计数加1。所以此时 Block 的引用计数是大于1的,即使self对象被废弃(譬如执行了退出当前页面之类的操作),Block 会被 release 一次,但它的引用计数仍然大于0,故而不会被废弃。

– 问:本来在 Block 内部使用weakSelf就是为了让 Block 对象不持有self指向的对象,那在 Block 内部又把weakSelf赋给strongSelf不就又持有self对象了么?又循环引用了?
– 答:weak是个神奇的东西,每次使用weak变量的时候,都会取出该变量指向的对象并 retain,然后将该对象注册到 autoreleasepool 中。通过上述代码我们可以发现,在__xx_block_func_y中,局部变量occlass会持有捕获的对象,然后对象会被注册到 autoreleasepool。这是延长对象生命周期的关键(保证在执行 Block 期间对象不会被废弃),但这不会造成循环引用,当函数执行结束,变量occlass超出作用域,过一会儿(一般一次 RunLoop 之后),对象就被释放了。所以 weak-strong dance 的行为非常符合预期:延长捕获对象的生命周期,一旦 Block 执行完,对象被释放,而 Block 也会被释放(如果被 GCD 之类的 API copy 过一次增加了引用计数,那最终也会被 GCD 释放)

关于weak:
weak修饰的变量不会持有对象,它用一张 weak 表(类似于引用计数表的散列表)来管理对象和变量。赋值的时候它会以赋值对象的地址作为 key,变量的地址为 value,注册到 weak 表中。一旦该对象被废弃,就通过对象地址在 weak 表中找到变量的地址,赋值为 nil,然后将该条记录从 weak 表中删除。
– 每使用一次_weak变量就会把对象注册到 autoreleasepool 中,所以如果短时间内大量使用_weak变量的话,会导致注册到 autoreleasepool 中的对象大量增加,占用一定内存。而 weak-strong dance 恰好无意中解决了这个隐患,在执行 Block 时,把_weak变量(weakSelf)赋值给一个临时变量(strongSelf),之后一直都使用这个临时变量,所以_weak变量只使用了一次,也就只有一个对象注册到 autoreleasepool 中。

# Day 63 : 小笨狼漫谈多线程:GCD(一)

小笨狼 发布

– dispatch_queue_t
– async:
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
– sync:
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
– 获取主线程队列:
dispatch_queue_t dispatch_get_main_queue(void)
– 获取全局队列:
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);
– 创建队列:
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
– 设置目标队列:
void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);
注意:当目标队列串行时,任何在目标队列中执行的block都会串行执行,无论原队列是否串行
– 延时:
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *context, dispatch_function_t work);
– dispatch_barrier:
使用dispatch_barrier将任务加入到并行队列之后,任务会在前面任务全部执行完成之后执行,任务执行过程中,其他任务无法执行,直到barrier任务执行完成
– set_specific & get_specific:
void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor);

void *dispatch_queue_get_specific(dispatch_queue_t queue, const void *key);
void *dispatch_get_specific(const void *key);

# Day 62 : iOS/OS X内存管理(二):借助工具解决内存问题

Sam_Lau 发布

  1. 悬挂指针问题:悬挂指针(Dangling Pointer)就是当指针指向的对象已经释放或回收后,但没有对指针做任何修改(一般来说,将它指向空指针),而是仍然指向原来已经回收的地址。如果指针指向的对象已经释放,但仍然使用,那么就会导致程序crash。
  2. 启用NSZombieEnabled
  3. 内存泄露问题
    3.1. 手动静态分析(菜单栏的Product -> Analyze或快捷键shift + command + b)
    3.2. 自动静态分析(在Build Settings启用Analyze During ‘Build’,每次编译时都会自动静态分析)
    3.3. 启动Instruments(Xcode的菜单栏的 Product -> Profile)

# Day 61 : Xcode中断点的威力

破船译自:albertopasca

  1. 添加一个特殊的断点
    1.1. 异常断点(Exception breakpoint)
    1.2. 符号断点(Symbolic breakpoint)
  2. 打印到控制台
    2.1.使用NSLog打印字符串
    2.2. 使用NSLog打印对象(po)
    2.3. 带条件的打印
    2.4. 在循环里面打印一些东西
  3. 运行时设置断点
  4. 调试中播放声音
  5. LLDB中有用的一些命令
    5.1. 打印帮助(help)
    5.2. 打印调用栈(bt)
    5.3. 打印最基本的内容 (p)
    5.4. 打印对象(po)
    5.5. 打印表达式(expr)
    5.6. 打印线程中的一些东西(help frame)

# Day 60 : 小笨狼与LLDB的故事

小笨狼 发布

文章介绍了很多LLDB命令。

# Day 59 : 线程安全类的设计

ObjC 中国 发布

# Day 58 : CFRunLoop

戴铭(ming1016) 发布

  1. Thread包含一个CFRunLoop,一个CFRunLoop包含一种CFRunLoopMode,mode包含CFRunLoopSource,CFRunLoopTimer和CFRunLoopObserver。
  2. CFRunLoopMode:
    2.1. NSDefaultRunLoopMode:默认,空闲状态
    2.2. UITrackingRunLoopMode:ScrollView滑动时
    2.3. UIInitializationRunLoopMode:启动时
    2.4. NSRunLoopCommonModes:Mode集合 Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes来解决
  3. CFRunLoopTimer
  4. CFRunLoopSource
    4.1. source0:处理如UIEvent,CFSocket这样的事件
    4.2. source1:Mach port驱动,CFMachport,CFMessagePort
  5. CFRunLoopObserver
  6. 使用RunLoop的案例
    6.1. AFNetworking
    6.2. TableView中实现平滑滚动延迟加载图片
    6.3. 接到程序崩溃时的信号进行自主处理例如弹出提示等
    6.4. 异步测试

# Day 57 : 离屏渲染优化

iOS开发by唐巧 公众号发布

总结:
– RoundedCorner 在仅指定cornerRadius时不会触发离屏渲染,仅适用于特殊情况:contents为 nil 或者contents不会遮挡背景色圆角
– Shawdow 可以通过指定路径来取消离屏渲染
– Mask 无法取消离屏渲染

以上效果在同等数量的规模下,对性能的影响等级:Shadow > RoundedCorner > Mask > GroupOpacity(迷之效果)。

任何时候优先考虑避免触发离屏渲染,无法避免时优化方案有两种:
– Rasterization:适用于静态内容的视图,也就是内部结构和内容不发生变化的视图,对上面的所有效果而言,在实现成本以及性能上最均衡的。即使是动态变化的视图,开启 Rasterization 后能够有效降低 GPU 的负荷,不过在动态视图里是否启用还是看 Instruments 的数据
– 规避离屏渲染,用其他手法来模拟效果,混合图层是个性能最好、耗能最少的通用优化方案,尤其对于 rounded corer 和 mask

# Day 56 : iOS 事件处理机制与图像渲染过程

WeMobileDev 公众号发布

– iOS RunLoop都干了什么
– iOS 为什么必须在主线程中操作UI
– 事件响应
– CALayer
– CADisplayLink 和 NSTimer
– iOS 渲染过程
– 渲染时机
– CPU 和 GPU渲染
– Core Animation
– Facebook Pop介绍
– AsyncDisplay介绍
– 参考文章

# Day 55 : iOS 组件化方案探索

bang’s blog 发布

分别介绍了Limboy的蘑菇街组件化方案和Casa的组件化方案,通过简练的总结和简单的代码示例令这两种方案的原理更加易懂。

# Day 54 : iOS应用架构谈 组件化方案

Casa Taloyum 发布

iOS应用架构谈 系列文章。对比了蘑菇街的组件化方案和CTMediator组件化方案。

# Day 53 : iOS应用架构谈 本地持久化方案及动态部署

Casa Taloyum 发布

iOS应用架构谈 系列文章。

# Day 52 : iOS应用架构谈 网络层设计方案

Casa Taloyum 发布

iOS应用架构谈 系列文章。

# Day 51(yesterday) : iOS应用架构谈 view层的组织和调用方案

Casa Taloyum 发布

iOS应用架构谈 系列文章。

# Day 50 : iOS应用架构谈 开篇

Casa Taloyum 发布

iOS应用架构谈 系列文章。
评论区也是值得看的地方。

# Day 49 : 组件化架构漫谈

刘小壮 发布

主要以蘑菇街组件化架构和casatwy组件化方案讲解组件化实现思路,并介绍了滴滴淘宝的组件化过程。

# Day 48 : 细说GCD(Grand Central Dispatch)如何用

戴铭(ming1016) 发布

文中较详细介绍GCD队列,各种GCD使用方法,实例如何使用Dispatch Source监听系统底层对象,分析不同锁的性能对比,实例GCD死锁情况。

# Day 47 : 不再安全的 OSSpinLock

Garan no Dou (ibireme) 发布

– 最终的结论就是,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。
– 可以看到除了 OSSpinLock 外,dispatch_semaphore 和 pthread_mutex 性能是最高的。有消息称,苹果在新系统中已经优化了 pthread_mutex 的性能,所以它看上去和 OSSpinLock 差距并没有那么大了。

# Day 46(yesterday) : iOS 保持界面流畅的技巧

Garan no Dou (ibireme) 发布

  1. 演示项目
  2. 屏幕显示图像的原理
  3. 卡顿产生的原因和解决方案
    3.1. CPU 资源消耗原因和解决方案
    3.2. GPU 资源消耗原因和解决方案
  4. AsyncDisplayKit
    4.1. ASDK 的由来
    4.2. ASDK 的资料
    4.3. ASDK 的基本原理
    4.4. ASDK 的图层预合成
    4.5. ASDK 异步并发操作
    4.6. Runloop 任务分发
  5. 微博 Demo 性能优化技巧
    5.1. 预排版
    5.2. 预渲染
    5.3. 异步绘制
    5.4. 全局并发控制
    5.5. 更高效的异步图片加载
    5.6. 其他可以改进的地方
  6. 如何评测界面的流畅度

YY大神的深度好文,之前已经读过,温故而知新!

# Day 45 : iOS 处理图片的一些小 Tip

Garan no Dou (ibireme) 发布

– 如何把 GIF 动图保存到相册
– 将 UIImage 保存到磁盘,用什么方式最好
– UIImage 缓存是怎么回事
– 用 imageWithData 能不能避免缓存
– 怎么能避免缓存
– 能直接取到图片解码后的数据,而不是通过画布取到吗
– 如何判断一个文件的图片类型
– 怎样像浏览器那样边下载边显示图片

# Day 44 : 移动端图片格式调研

Garan no Dou (ibireme) 发布

  1. 几种图片格式简介
  2. 移动端图片类型的支持情况
  3. 静态图片的编码与解码
    3.1. JPEG
    3.2. PNG
    3.3. WebP
    3.4. BPG
  4. 动态图片的编码与解码
    4.1. GIF
    4.2. APNG
    4.3. WebP
    4.4. BPG
    4.5. 动图性能对比

分析了目前主流和新兴的几种图片格式的特点、性能分析、参数调优,以及相关开源库的选择。

# Day 43 : 深入理解RunLoop

Garan no Dou (ibireme) 发布

  1. RunLoop 的概念
  2. RunLoop 与线程的关系
  3. RunLoop 对外的接口
  4. RunLoop 的 Mode
  5. RunLoop 的内部逻辑
  6. RunLoop 的底层实现
  7. 苹果用 RunLoop 实现的功能
    7.1. AutoreleasePool
    7.2. 事件响应
    7.3. 手势识别
    7.4. 界面更新
    7.5. 定时器
    7.6. PerformSelecter
    7.7 关于GCD
    7.8 关于网络请求
  8. RunLoop 的实际应用举例
    8.1. AFNetworking
    8.2. AsyncDisplayKit

YY大神的经典文章,文章太长就不做笔记了,目录贴出来,适合再次阅读。
还有阳神关于 RunLoop 的线下分享视频:http://v.youku.com/v_show/id_XODgxODkzODI0.html

# Day 42 : Clang Attributes 黑魔法小记

sunnyxx的技术博客 发布

Clang Attributes 是 Clang 提供的一种源码注解,方便开发者向编译器表达某种要求,参与控制如 Static Analyzer、Name Mangling、Code Generation 等过程,一般以 __attribute__(xxx) 的形式出现在代码中;为方便使用,一些常用属性也被 Cocoa 定义成宏,比如在系统头文件中经常出现的 NS_CLASS_AVAILABLE_IOS(9_0) 就是 __attribute__(availability(...)) 这个属性的简单写法。

objc_subclassing_restricted:定义一个不可被继承的类。
objc_requires_super:标志子类继承这个方法时需要调用 super,否则给出编译警告。
objc_boxable:box 一个 struct 类型或是 union 类型成 NSValue 对象。
constructor / destructor:构造器和析构器,加上这两个属性的函数会在分别在可执行文件(或 shared library)load和 unload 时被调用,可以理解为在 main() 函数调用前和 return 后执行(若有多个 constructor 且想控制优先级的话,可以写成 attribute((constructor(101))),里面的数字越小优先级越高,1 ~ 100 为系统保留。)
enable_if:只能用在 C 函数上,可以用来实现参数的静态检查。
cleanup:声明到一个变量上,当这个变量作用域结束时,调用指定的一个函数,Reactive Cocoa 用这个特性实现了神奇的 @onExit。
overloadable:用于 C 函数,可以定义若干个函数名相同,但参数不同的方法,调用时编译器会自动根据参数选择函数原型。
objc_runtime_name:用于 @interface 或 @protocol,将类或协议的名字在编译时指定成另一个。

# Day 41 : 巧用 Class Extension 分离接口依赖

sunnyxx的技术博客 发布

– Category 的实现可以依赖主类,但主类一定不依赖 Category,也就是说移除任何一个 Category 的代码不会对主类产生任何影响。
– Category 可以直接使用主类已有的私有成员变量,但不应该为实现 Category 而往主类中添加成员变量,考虑在 Category 的实现中使用 objc association 来达到相同效果。
– Class Extension 和 Category 在语言机制上有着很大差别:Class Extension 在编译期就会将定义的 Ivar、属性、方法等直接合入主类,而 Category 在程序启动 Runtime Loading 时才会将属性(没 Ivar)和方法合入主类。
– 将对公业务和对私业务用 Class Extension 的形式拆到两个 Header 中,这样私有类对私有属性的依赖就被成功隔离开

# Day 40 : 2015 Objective-C 新特性

sunnyxx的技术博客 发布

– Nullability:Nullability 在编译器层面提供了空值的类型检查,在类型不符时给出 warning,方便开发者第一时间发现潜在问题。

1
2
3
4
@property (nonatomic, copy, readonly, nullable) NSArray *friends;
+ (nullable NSString *)friendWithName:(nonnull NSString *)name;
// 假如用来修饰一个变量,前面还要加双下划线:
- (void)startWithCompletionBlock:(nullable void (^)(NSError * __nullable error))block;

– Lightweight Generics *

1
2
3
4
NSArray<NSString *> *strings = @[@"sun", @"yuan"];
NSDictionary<NSString *, NSNumber *> *mapping = @{@"a": @1, @"b": @2};
@property (readonly) NSArray<NSURL *> *imageURLs;
@interface Stack<ObjectType> : NSObject

– __covariant - 协变性,子类型可以强转到父类型(里氏替换原则):

1
@interface Stack<__covariant ObjectType> : NSObject

– __contravariant - 逆变性,父类型可以强转到子类型:

1
@interface Stack<__contravariant ObjectType> : NSObject

– __kindof

1
2
3
@property (nonatomic, readonly, copy) NSArray<__kindof UIView *> *subviews;
这样,写下面的代码时就没有任何警告了:
UIButton *button = view.subviews.lastObject;

# Day 39(yesterday) : 黑魔法attribute((cleanup))

sunnyxx的技术博客 发布

attribute((cleanup(…))),用于修饰一个变量,在它的作用域结束时可以自动执行一个指定的方法。
– 所谓作用域结束,包括大括号结束、return、goto、break、exception等各种情况。
– 假如一个作用域内有若干个cleanup的变量,他们的调用顺序是先入后出的栈式顺序;而且,cleanup是先于这个对象的dealloc调用的。
– 可以修饰的变量有:NSString、自定义Class、基本类型、block:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 1.NSString ---------------------------------------------
// 指定一个cleanup方法,注意入参是所修饰变量的地址,类型要一样
// 对于指向objc对象的指针(id *),如果不强制声明__strong默认是__autoreleasing,造成类型不匹配
static void stringCleanUp(__strong NSString **string) {
NSLog(@"%@", *string);
}
// 在某个方法中:
{
__strong NSString *string __attribute__((cleanup(stringCleanUp))) = @"sunnyxx";
} // 当运行到这个作用域结束时,自动调用stringCleanUp

// 2.自定义的Class ---------------------------------------------
static void sarkCleanUp(__strong Sark **sark) {
NSLog(@"%@", *sark);
}
__strong Sark *sark __attribute__((cleanup(sarkCleanUp))) = [Sark new];

// 3.基本类型 ---------------------------------------------
static void intCleanUp(NSInteger *integer) {
NSLog(@"%d", *integer);
}
NSInteger integer __attribute__((cleanup(intCleanUp))) = 1;

// 4.block ---------------------------------------------
// void(^block)(void)的指针是void(^*block)(void)
static void blockCleanUp(__strong void(^*block)(void)) {
(*block)();
}
{
// 加了个`unused`的attribute用来消除`unused variable`的warning
__strong void(^block)(void) __attribute__((cleanup(blockCleanUp), unused)) = ^{
NSLog(@"I'm dying...");
};
}

Reactive Cocoa中神奇的@onExit方法,其实正是上面的写法,简单定义个宏:

1
2
#define onExit\
__strong void(^block)(void) __attribute__((cleanup(blockCleanUp), unused)) = ^

用这个宏就能将一段写在前面的代码最后执行:

1
2
3
4
5
{
onExit {
NSLog(@"yo");
};
} // Log "yo"

# Day 38 : iOS 程序员 6 级考试(答案和解释)

sunnyxx的技术博客 发布

五道iOS题目,查看原文。

# Day 37 : iOS 程序 main 函数之前发生了什么

sunnyxx的技术博客 发布

  1. 从dyld开始
    1.1. 动态链接库
    1.2. dyld:(the dynamic link editor),Apple 的动态链接器,系统 kernel 做好启动程序的初始准备后,交给 dyld 负责。
    1.3. ImageLoader:当然这个 image 不是图片的意思,它大概表示一个二进制文件(可执行文件或 so 文件),里面是被编译过的符号、代码等,所以 ImageLoader 作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。
  2. runtime 与 +load:dyld 担当了 runtime 和 ImageLoader 中间的协调者,当新 image 加载进来后交由 runtime 大厨去解析这个二进制文件的符号表和代码。
    2.1. 关于 +load 方法的几个 QA
  3. 简单总结:整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存,动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。

# Day 36 : 黑幕背后的Autorelease

sunnyxx的技术博客 发布

  1. Autorelease对象什么时候释放?
    1.1. 在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
  2. Autorelease原理
    2.1. AutoreleasePoolPage
    2.1.1. AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
    2.1.2. AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
    2.1.3. AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
    2.1.4. 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
    2.1.5. 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入
    2.2. 释放时刻
    2.2.1. 每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil)
    2.2.2. 根据传入的哨兵对象地址找到哨兵对象所处的page
    2.2.3. 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
    2.2.4. 从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
    2.3. 嵌套的AutoreleasePool
    2.3.1. 知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。
  3. Autorelease返回值的快速释放机制
    3.1. 黑魔法之Thread Local Storage
    3.2. 黑魔法之__builtin_return_address
    3.3. 黑魔法之反查汇编指令
  4. 其他Autorelease相关知识点
    4.1. 使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool。当然,在普通for循环和for in循环中没有,所以,还是新版的block版本枚举器更加方便。for循环中遍历产生大量autorelease变量时,就需要手加局部AutoreleasePool咯。

# Day 35 : Objective-C Method Swizzling

玉令天下的博客 发布

Method Swizzling 常用实现方案
方案A:
如果类中没有实现 Original selector 对应的方法,那就先添加 Method,并将其 IMP 映射为 Swizzle 的实现。然后替换 Swizzle selector 的 IMP 为 Original 的实现;否则交换二者 IMP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];

SEL originalSelector = @selector(method_original:);
SEL swizzledSelector = @selector(method_swizzle:);

Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

有时为了避免方法命名冲突和参数 _cmd 被篡改,也会使用下面这种『静态方法版本』的 Method Swizzle。CaptainHook 中的宏定义也是采用这种方式,比较推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
typedef IMP *IMPPointer;

static void MethodSwizzle(id self, SEL _cmd, id arg1);
static void (*MethodOriginal)(id self, SEL _cmd, id arg1);

static void MethodSwizzle(id self, SEL _cmd, id arg1) {
// do custom work
MethodOriginal(self, _cmd, arg1);
}

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}

+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}

+ (void)load {
[self swizzle:@selector(originalMethod:) with:(IMP)MethodSwizzle store:(IMP *)&MethodOriginal];
}

方案 B(方案 A 的阉割版):

1
2
3
4
5
6
7
8
9
10
11
12
13
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];

SEL originalSelector = @selector(method_original:);
SEL swizzledSelector = @selector(method_swizzle:);

Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

直接交换 IMP 是很危险的。因为如果这个类中没有实现这个方法,class_getInstanceMethod() 返回的是某个父类的 Method 对象,这样 method_exchangeImplementations() 就把父类的原始实现(IMP)跟这个类的 Swizzle 实现交换了。这样其他父类及其其他子类的方法调用就会出问题,最严重的就是 Crash。
但如果这个类中实现了此方法,那么方案 A 等价于方案 B。

结论:

  • Hook 顺序:先 Child 类后 Super 类
    为了保证 Hook 后方法调用顺序是对的,需要同时满足以下两个条件:
    – 1.Child 类实现被 Hook 的方法
    – 2.Super 类实现被 Hook 的方法或使用 A 方案 Hook
  • Hook 顺序:先 Super 类后 Child 类
    因为 Hook 的顺序是正确的,所以只需满足以下任意一个条件即可:
    – 1.实现被 Hook 的方法
    – 2.使用方案 A 进行 Hook

# Day 34 : Objective-C 引用计数原理

玉令天下的博客 发布

有些对象如果支持使用 TaggedPointer,苹果会直接将其指针值作为引用计数返回;如果当前设备是 64 位环境并且使用 Objective-C 2.0,那么“一些”对象会使用其 isa 指针的一部分空间来存储它的引用计数;否则 Runtime 会使用一张散列表来管理引用计数。

  1. 引用计数如何存储
    1.1. TaggedPointer
    1.2. isa 指针(NONPOINTER_ISA)
    1.3. 散列表
  2. 获取引用计数
  3. 修改引用计数
    3.1. retain 和 release
    3.2. alloc, new, copy, mutableCopy
    3.3. autorelease
  4. Reference

# Day 33(yesterday) : Objective-C 消息发送与转发机制原理

玉令天下的博客 发布

消息发送和转发流程可以概括为:
消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;
消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。

  1. 八面玲珑的 objc_msgSend
    1.1. 源码解析
    1.2. 为什么使用汇编语言
  2. 使用 lookUpImpOrForward 快速查找 IMP
    2.1. 优化缓存查找&类的初始化
    2.2. 继续在类的继承体系中查找
    2.3. 回顾 objc_msgSend 伪代码
  3. forwarding 中路漫漫的消息转发
    3.1. objc_msgForward_impcache 的转换
    3.2. objc_msgForward 也只是个入口
    3.3. objc_setForwardHandler 设置了消息转发的回调
    3.4. 逆向工程助力刨根问底
  4. 总结
  5. 参考文献

# Day 32 : HTTPS 是如何保证安全的?

程序员Delton 发布

  1. HTTP 面临的一个问题叫做 “窃听” 或者 “嗅探” ,指的是和你在同一个网络下或者是途径的路由上的攻击者可以偷窥到你传输的内容。
  2. HTTPS 通常是通过“加密”来解决这个问题。
    2.1. 对称加密:需要一个密钥 key 来加密整个信息,加密和解密所需要使用的 key 是一样的。比如 AES 算法,在数学上保证了,只要你使用的 key 足够足够足够足够的长,破解是几乎不可能的。
    2.2. 非对称加密:可以生成一对密钥 (k1, k2),凡是 k1 加密的数据,k1 自身不能解密,而需要 k2 才能解密;凡是 k2 加密的数据,k2 不能解密,需要 k1 才能解密。这种算法事实上有很多,常用的是 RSA,其基于的数学原理是两个大素数的乘积很容易算,而拿到这个乘积去算出是哪两个素数相乘就很复杂了。
    2.2.1. 用 RSA 技术生成了一对 k1、k2,你把 k1 用明文发送了出去,路经有人或许会截取,但是没有用,k1 加密的数据需要用 k2 才能解密。而此时,k2 在你自己的手里。k1 送达目的地后,目的地的人会去准备一个接下来用于对称加密传输的密钥 key,然后用收到的 k1 把 key 加密了,把加密好的数据传回来。路上的人就算截取到了,也解密不出 key。等到了你自己手上,你用手上的 k2 把用 k1 加密的 key 解出来,现在就只有你和你的目的地拥有 key,你们就可以用 AES 算法进行对称加密的传输啦。
    2.2.2. 第一个问题:因为 非对称加密 的密码对生成和加密的消耗时间比较长,为了节省双方的计算时间,通常只用它来交换密钥,而非直接用来传输数据。
    2.2.3. 第二个问题:中间人攻击。
    2.2.4. 为解决第二个问题引入一个第三方叫做 CA。CA 是一些非常权威的专门用于认证一个网站合法性的组织。服务商可以向他们申请一个证书,使得他们建立安全连接时可以带上 CA 的签名。如果和你建立安全连接的人带着这些人的签名,那么认为这个安全连接是安全的,没有遭到中间人攻击。

# Day 31 : Objective-C Runtime

玉令天下的博客 发布

  1. 引言
  2. 简介
  3. 与 Runtime 交互
    3.1. Objective-C 源代码
    3.2. NSObject 的方法
    3.3. Runtime 的函数
  4. Runtime 基础数据结构
    4.1. SEL
    4.2. id
    4.3. Class
    4.3.1. cache_t
    4.3.2. class_data_bits_t
    4.3.3. class_ro_t
    4.3.4. class_rw_t
    4.3.5. realizeClass
    4.4. Category
    4.5. Method
    4.6. Ivar
    4.7. objc_property_t
    4.8. protocol_t
    4.9. IMP
  5. 消息
    5.1. objc_msgSend 函数
    5.2. 方法中的隐藏参数
    5.3. 获取方法地址
  6. 动态方法解析
  7. 消息转发
    7.1. 重定向
    7.2. 转发
    7.3. 转发和多继承
    7.4. 替代者对象(Surrogate Objects)
    7.5. 转发与继承
  8. 健壮的实例变量 (Non Fragile ivars)
  9. Objective-C Associated Objects
  10. Method Swizzling
  11. 总结

文章相当长且有质量,把目录贴出来,有不清楚或需要学习的再去原文查阅吧!

# Day 30 : 神经病院Objective-C Runtime出院第三天——如何正确使用Runtime

一缕殇流化隐半边冰霜(halfrost) 发布

  • Runtime的优点
    • 实现多继承Multiple Inheritance:
      forwardingTargetForSelector:这一步将消息转发给正确的类对象就可以模拟多继承的效果
      – 即使我们利用转发消息来实现了“假”继承,但是NSObject类还是会将两者区分开,像respondsToSelector:和 isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链,可以重写 respondsToSelector:和 isKindOfClass:等方法来加入自己的转发算法。
    • Method Swizzling:
      • Method Swizzling原理:本质上就是对IMP和SEL进行交换。
      • Method Swizzling使用:通常应用于在category中添加一个方法。
      • Method Swizzling注意点
        – Swizzling应该总在+load中执行
        – Swizzling应该总是在dispatch_once中执行
        – Swizzling在+load中执行时,不要调用[super load]
      • Method Swizzling使用场景
        – 实现AOP
        – 实现埋点统计
        – 实现异常保护
    • Aspect Oriented Programming
    • Isa Swizzling
      – KVO是为了监听一个对象的某个属性值是否发生变化。在属性值发生变化的时候,肯定会调用其setter方法。所以KVO的本质就是监听对象有没有调用被监听属性对应的setter方法。
    • Associated Object关联对象
    • 动态的增加方法
    • NSCoding的自动归档和自动解档
      – 用runtime实现的思路就比较简单,我们循环依次找到每个成员变量的名称,然后利用KVC读取和赋值就可以完成encodeWithCoder和initWithCoder了。
    • 字典和模型互相转换
      • 字典转模型
        – 调用 class_getProperty 方法获取当前 Model 的所有属性。
        – 调用 property_copyAttributeList 获取属性列表。
        – 根据属性名称生成 setter 方法。
        – 使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC)
      • 模型转字典
        – 调用 class_copyPropertyList 方法获取当前 Model 的所有属性。
        – 调用 property_getName 获取属性名称。
        – 根据属性名称生成 getter 方法。
        – 使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC)
  • Runtime的缺点
    – Method swizzling is not atomic
    – Changes behavior of un-owned code
    – Possible naming conflicts
    – Swizzling changes the method’s arguments
    – The order of swizzles matters
    – Difficult to understand (looks recursive)
    – Difficult to debug

日常可能用的比较多的Runtime函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 调用指定方法的实现 
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );

//获取cls类对象所有成员ivar结构体
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
//获取cls类对象name对应的实例方法结构体
Method class_getInstanceMethod(Class cls, SEL name)
//获取cls类对象name对应类方法结构体
Method class_getClassMethod(Class cls, SEL name)
//获取cls类对象name对应方法imp实现
IMP class_getMethodImplementation(Class cls, SEL name)
//测试cls对应的实例是否响应sel对应的方法
BOOL class_respondsToSelector(Class cls, SEL sel)
//获取cls对应方法列表
Method *class_copyMethodList(Class cls, unsigned int *outCount)
//测试cls是否遵守protocol协议
BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
//为cls类对象添加新方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//替换cls类对象中name对应方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
//为cls添加新成员
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)
//为cls添加新属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
//获取m对应的选择器
SEL method_getName(Method m)
//获取m对应的方法实现的imp指针
IMP method_getImplementation(Method m)
//获取m方法的对应编码
const char *method_getTypeEncoding(Method m)
//获取m方法参数的个数
unsigned int method_getNumberOfArguments(Method m)
//copy方法返回值类型
char *method_copyReturnType(Method m)
//获取m方法index索引参数的类型
char *method_copyArgumentType(Method m, unsigned int index)
//获取m方法返回值类型
void method_getReturnType(Method m, char *dst, size_t dst_len)
//获取方法的参数类型
void method_getArgumentType(Method m, unsigned int index, char *dst, size_t dst_len)
//设置m方法的具体实现指针
IMP method_setImplementation(Method m, IMP imp)
//交换m1m2方法对应具体实现的函数指针
void method_exchangeImplementations(Method m1, Method m2)
//获取v的名称
const char *ivar_getName(Ivar v)
//获取v的类型编码
const char *ivar_getTypeEncoding(Ivar v)
//设置object对象关联的对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取object关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除object关联的对象
void objc_removeAssociatedObjects(id object)

⚠️注:runtime 系列文章,有原理讲解有用法示例,深度好文!

# Day 29 : 神经病院Objective-C Runtime住院第二天——消息发送与转发

一缕殇流化隐半边冰霜(halfrost) 发布

  • objc_msgSend函数简介。objc_msgSend会做一下几件事情:
    – 检测这个 selector 是不是要忽略的。
    – 检查target是不是为nil。如果这里有相应的nil的处理函数,就跳转到相应的函数中。如果没有处理nil的函数,就自动清理现场并返回。这一点就是为何在OC中给nil发送消息不会崩溃的原因。
    – 确定不是给nil发消息之后,在该class的缓存中查找方法对应的IMP实现。如果找到,就跳转进去执行。如果没有找到,就在方法分发表里面继续查找,一直找到NSObject为止
    – 如果还没有找到,那就需要开始消息转发阶段了。至此,发送消息Messaging阶段完成。这一阶段主要完成的是通过select()快速查找IMP的过程
  • 消息发送Messaging阶段—objc_msgSend源码解析
  • 消息转发Message Forwarding阶段
  • forwardInvocation的例子
  • 入院考试
  • Runtime中的优化
    – 方法列表的缓存
    – 虚函数表vTable
    – dyld共享缓存

runtime 系列文章,同样很有深度。因为有很多源码分析和原理讲解且内容较多所以不好简洁总结,之后再温故知新吧!

# Day 28 : 神经病院Objective-C Runtime入院第一天——isa和Class

一缕殇流化隐半边冰霜(halfrost) 发布

  • NSObject起源
    – 在NSObject协议中,有以下5个方法:

    1
    2
    3
    4
    5
    - (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead"); // 返回对象的类
    - (BOOL)isKindOfClass:(Class)aClass; // 检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量)
    - (BOOL)isMemberOfClass:(Class)aClass; // 检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量)
    - (BOOL)conformsToProtocol:(Protocol *)aProtocol; // 检查对象能否响应指定的消息
    - (BOOL)respondsToSelector:(SEL)aSelector; // 检查对象是否实现了指定协议类的方法

    在NSObject的类中还定义了一个方法:- (IMP)methodForSelector:(SEL)aSelector;这个方法会返回指定方法实现的地址IMP
    – 当我们导入了objc/Runtime.h和objc/message.h两个头文件之后,我们查找到了Runtime的函数之后,代码打完,发现没有代码提示了,可以在 Build settings 中搜索 objc_msgEnable Strict Checking objc_msgSend Calls 设为 NO
    – 在引入元类之后,类对象和对象查找方法的机制就完全统一了:对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。

    – isa_t结构体的具体实现
    – cache_t的具体实现:Cache的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找
    – class_data_bits_t的具体实现

  • 入院考试
    – [self class] 与 [super class]
    – isKindOfClass 与 isMemberOfClass
    – Class与内存地址

⚠️注:很有深度,应该是那种温故而知新的文章,后续还会再读!

# Day 27 : 处理手势冲突和错乱的一点经验

玉令天下的博客 发布

对于复杂页面掺合多种手势的场景,采用“状态机”的方案来区分手势进行页面操作

– 对手势统一处理和分发:把各种手势全都添加到底层的全屏视图上,然后统一处理和分发结果。因为每种手势只有一个且都加在了底层视图,所以不会发生不同视图间的手势错乱。而不同种手势之间的冲突就需要在 UIGestureRecognizerDelegate 中根据业务逻辑来解决了。
– 计算响应手势的视图:可以通过 locationInView: 获取手势的坐标,但这里决不能简单地计算手势坐标到视图 center 的距离并选取最近的视图,这里需要检测手势坐标处于哪个视图的范围内。
– 处理 Pinch 手势:1、分辨率:当对含有矢量内容的视图进行缩放时会有模糊和锯齿出现,这时递归需要改变 UIView 的 contentScaleFactor 和 CALayer 的 contentsScale 属性。2、坐标:视图的 transform 属性是不会修改视图的 bounds 的,但 frame 作为计算属性还是会变化的。也就是说无论视图放大了多少倍,视图内部的子视图的 frame 不会变。
– 处理 Rotation 手势:一直用『视图区域』而不直接用 frame 来描述手势判断依据,是因为当视图旋转(90°倍数除外)之后 frame 并不等于『视图区域』(文中有提供方法用于判断某个点是否在『视图区域』内)
– 如果所有手势都交给一个底层视图统一处理的话,上层那一坨视图是不需要响应触摸事件的,有些甚至可以用 Layer 来做。

其中两条参考链接值得参考:
Taking Charge of UIView Transforms in iOS Programming
Scaling UITextView using contentScaleFactor property

# Day 26 : 详解CALayer 和 UIView的区别和联系

kissGod 发布

– 每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。两者都有树状层级结构,layer 内部有 SubLayers,View 内部有 SubViews.但是 Layer 比 View 多了个AnchorPoint
– 在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display
– CALayer 是默认修改属性支持隐式动画的,在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过 actionForLayer:forKey:向 View请求相应的 action(动画行为)
– layer 内部维护着三分 layer tree,分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree (渲染树),在做 iOS动画的时候,我们修改动画的属性,在动画的其实是 Layer 的 presentLayer的属性值,而最终展示在界面上的其实是提供 View的modelLayer
– 两者最明显的区别是 View可以接受并处理事件,而 Layer 不可以

# Day 25 : iOS 常见知识点(三):Lock

AidenRao 发布

iOS 中的八大锁
– NSLock:遵循 NSLocking 协议lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO
互斥锁会使得线程阻塞,阻塞的过程又分两个阶段,第一阶段是会先空转,可以理解成跑一个 while 循环,不断地去申请加锁,在空转一定时间之后,线程会进入 waiting 状态,此时线程就不占用CPU资源了,等锁可用的时候,这个线程会立即被唤醒。
– NSConditionLock:遵循 NSLocking 协议,方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法。NSConditionLock 还可以实现任务之间的依赖
– NSRecursiveLock:递归锁,他和 NSLock 的区别在于,NSRecursiveLock 可以在一个线程中重复加锁(反正单线程内任务是按顺序执行的,不会出现资源竞争问题),NSRecursiveLock 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功
– NSCondition:NSCondition 并不会像上文的那些锁一样,先轮询,而是直接进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法
– @synchronized:
– dispatch_semaphore:GCD 用来同步的一种方式
– OSSpinLock:OSSpinLock 会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务
– pthread_mutex:C 语言下多线程加互斥锁的方式

这里介绍了iOS中的几种锁,简单介绍了其中的用法,但仅凭阅读一篇博客并不能熟知iOS中锁的应用,具体的原理及使用还是要代码中实践才是。

# Day 24 : iOS书写高质量代码之耦合的处理

MrPeak杂货铺 发布

写高质量代码之耦合的处理的几种方式:
– .m引用:直接在 .m 文件创建对象调用方法
– .h Property:.h 文件中声明引用,依赖关系清晰
– .h ReadOnly Property:重写 setter 方法,对代码有更大的掌控能力,且避免轻易修改引用属性
– init 注入:对外就只提供一次机会(初始化init)来设置
– parameter 注入:将依赖类作为参数传入函数,耦合只发生在函数内部,一旦函数调用结束,就结束了依赖关系
– 单例引用:函数调用中使用单例,缺点也十分明显,应当慎用单例
– 继承:通过继承调用父类的方法,但确认父子关系也是麻烦的存在
– runtime依赖:耦合度非常之低,甚至可以说感觉不到,但是当依赖类的方法名发生改变时编译并不会提醒
– protocol依赖:好处在于他只规定了方法的声明,并不限定具体是那个类来实现它,给后期的维护留下更大的空间和可能性

以上方法的优点和缺点都很明显,怎么使用还要靠业务和coder自己决定。

# Day 23 : iOS关于时间的处理

MrPeak杂货铺 发布

– GMT:以格林尼治的时间作为公共时间,也就是我们所说的GMT时间(Greenwich Mean Time)。
– UTC:原子钟所反映的时间,也就是我们现在所使用的UTC(Coordinated Universal Time )标准时间。
– NSDate:描述的是时间线上的一个绝对的值,和时区和文化无关,它参考的值是:以UTC为标准的,2001年一月一日00:00:00这一刻的时间绝对值。受手机系统时间控制的。也就是说,当你修改了手机上的时间显示,NSDate获取当前时间的输出也会随之改变。

1
2
3
4
NSDate* date = [NSDate date];
NSLog(@"current date: %@", date); // 绝对的UTC时间
NSLog(@"current date interval: %f", [date timeIntervalSinceReferenceDate]); // 返回的是距离参考时间的偏移量
NSLog(@"timeIntervalSince1970: %f", [date timeIntervalSince1970]); // 返回Unix time

– CFAbsoluteTimeGetCurrent():参考点是:以GMT为标准的,2001年一月一日00:00:00这一刻的时间绝对值。会跟着当前设备的系统时间一起变化,也可能会被用户修改。
– gettimeofday:获得的值是Unix time。Unix time是以UTC 1970年1月1号 00:00:00为基准时间,当前时间距离基准点偏移的秒数。受当前设备的系统时间影响。只不过是参考的时间基准点不一样而已。我们和服务器通讯的时候一般使用Unix time。

1
2
3
4
struct timeval now;
struct timezone tz;
gettimeofday(&now, &tz);
NSLog(@"gettimeofday: %ld", now.tv_sec); // Unix time

在Mac的终端可以通过 date -r 1481266031 命令转换成可阅读的时间。
– mach_absolute_time():返回的是CPU已经运行的tick(CPU的时钟周期数)的数量。将这个tick数经过一定的转换就可以变成秒数,或者纳秒数,这样就和时间直接关联了。不会受系统时间影响,只受设备重启和休眠行为影响。
– CACurrentMediaTime():就是将上面mach_absolute_time()的CPU tick数转化成秒数的结果。不会受系统时间影响,只受设备重启和休眠行为影响。

1
2
3
4
5
double mediaTime = CACurrentMediaTime();
NSLog(@"CACurrentMediaTime: %f", mediaTime); // 开机后设备一共运行了(设备休眠不统计在内)多少秒

NSTimeInterval systemUptime = [[NSProcessInfo processInfo] systemUptime];
NSLog(@"systemUptime: %f", systemUptime); // 相同效果:开机后设备一共运行了(设备休眠不统计在内)多少秒

– sysctl:返回的值是上次设备重启的Unix time。会受系统时间影响,用户如果修改时间,值也会随着变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/sysctl.h>
- (long)bootTime
{
#define MIB_SIZE 2
   int mib[MIB_SIZE];
   size_t size;    
   struct timeval  boottime;

   mib[0] = CTL_KERN;
   mib[1] = KERN_BOOTTIME;
   size = sizeof(boottime);    
   if (sysctl(mib, MIB_SIZE, &boottime, &size, NULL, 0) != -1)
   {        
       return boottime.tv_sec;
   }    
   return 0;
}

– 场景一,时间测量:CACurrentMediaTime与NSDate代码本身的损耗差异在几微秒,而我们做UI性能优化的维度在毫秒级别,几个微秒的差异完全不会影响我们最后的判断结果。所以使用NSDate做benchmark完全是可行的,以下是我常用的两个宏:

1
2
#define TICK   NSDate *startTime = [NSDate date]
#define TOCK   NSLog(@"Time Cost: %f", -[startTime timeIntervalSinceNow])

– 场景二:客户端和服务器之间的时间同步:(给出了一点技巧,参考原文)

# Day 22 : 正确使用@synchronized()

MrPeak杂货铺 发布

1
2
3
@synchronized(obj) {
 //code
}
  • @synchronized原理
    – synchronized是使用的递归mutex来做同步。
    – @synchronized(nil)不起任何作用
    – synchronized中传入的object的内存地址,被用作key,通过hash map对应的一个系统维护的递归锁。
  • @synchronized使用
    – 慎用@synchronized(self),正确的做法是传入一个类内部维护的NSObject对象,而且这个对象是对外不可见的。
    – 精准的粒度控制,不同的 @synchronized (token) 使用不同的 token
    – 注意内部的函数调用,避免 {} 内部有其他隐蔽的函数调用拖慢锁的性能

# Day 21 : iOS多线程到底不安全在哪里?

MrPeak杂货铺 发布

self.userName = @"peak"; 是在对指针本身进行赋值,而 [self.userName rangeOfString:@"peak"]; 是在访问指针指向的字符串所在的内存区域,这二者并不一样。
– 由于BOOL大小只有1个字节,64位系统的地址总线对于读写指令可以支持8个字节的长度,所以对于BOOL的读和写操作我们可以认为是原子的,所以当我们声明BOOL类型的property的时候,从原子性的角度看,使用atomic和nonatomic并没有实际上的区别(当然如果重载了getter方法就另当别论了)。
– atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。
– iOS给代码加锁的方式有很多种,常用的有:@synchronized(token)NSLockdispatch_semaphore_tOSSpinLock,这几种锁都可以带来原子性,性能的损耗依次更小。
_intA ++;(非原子性),OSAtomicIncrement32(&(_intA));(原子性),Atomic Operation只能应用于32位或者64位的数据类型,在多线程使用NSString或者NSArray这类对象的场景,还是得使用锁。

# Day 20 : 与调试器共舞 - LLDB 的华尔兹

ObjC 中国 发布

这是一篇有料有趣的文章,介绍了很多 LLDB 的命令和用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
help	// 列举出所有的命令
print // prin,pri,p,但不能使用 pr,因为 LLDB 不能消除和 process 的歧义
expression // 改变一个值
po // e -0 --,打印对象
p/x // 打印十六进制变量
p/t // 打印二进制变量(two)
p/c  // 打印字符
p/s  // 打印以空终止的字符串 (译者注:以 '\0' 结尾的字符串)
c // continue
n // next,thread step-over 如果这行代码是一个函数调用,那么就不会跳进这个函数,而是会执行这个函数,然后继续。
s // step,thread step in跳进一个函数调用来调试或者检查程序的执行情况。注意,当前行不是函数调用时,next 和 step 效果是一样的。
breakpoint list // br li,看到所有的断点
breakpoint set -f main.m -l 16 // b main.m:16,创建断点
b isEven // br s -F isEven,在一个符号 (C 语言函数) 上创建断点,而完全不用指定哪一行
// 创建断点
breakpoint set -F "-[NSArray objectAtIndex:]"
b -[NSArray objectAtIndex:]
breakpoint set -F "+[NSSet setWithObject:]"
b +[NSSet setWithObject:]
po [[[UIApplication sharedApplication] keyWindow] recursiveDescription] // 可以看到整个层次

在 Xcode 的UI上创建符号断点:
你可以点击断点栏左侧的 + 按钮,选择 “Add Symbolic Breakpoint”,这时会出现一个弹出框,你可以在里面添加例如 -[NSArray objectAtIndex:] 这样的符号断点。这样每次调用这个函数的时候,程序都会停止,不管是你调用还是苹果调用。

断点行为 (Action):
右击任意断点选择“Edit Breakpoint”
– 添加 action,可以添加 Debugger Command,Shell Command,Log Message,Sound 等行为
– 选项: “Automatically continue after evaluation actions.” 选中它,调试器会运行你所有的命令,然后继续运行

调试器可用命令总览参考:GDB to LLDB

# Day 19 : IP,TCP 和 HTTP

ObjC 中国 发布

又温习一遍网络连接的知识。。。

# Day 18 : 基础集合类

ObjC 中国 发布

这篇文章可真的是基础了,可能是今天时间太紧张没找到一篇比较有深度的文章,不过这篇文章从基础集合类(NSArray, NSSet, NSOrderedSet 和 NSDictionary)讲起,分别介绍了它们的方法,分析了性能,当是基础知识的一次复习吧。
文中总结列出了枚举 NSArray 和 NSDictionary 的几种方法,并分析了各自的性能,值得一看。因为都是一些基础集合类,就不做什么笔记了,各个类的方法到 Xcode 里都能轻易找着。

# Day 17 : Build 过程

ObjC 中国 发布

  • 解密 Build 日志
  • Build过程的控制
    – Build Phases:代表将代码转变为可执行文件的最高级别规则。
    – 定制Build Phases:可以在 build phases 中添加运行自定义脚本,就像CocoaPods使用的一样,来做额外的工作。
    – Build Rules:指定了不同的文件类型该如何编译。
    – Build Settings:可以配置每个任务(之前在 build log 输出中看到的任务)的详细内容。例如可以添加一个 “Run Script”:如果一个源文件超过指定行数,就发出警告。如下代码所示,设置的行数为 200:

    1
    find "${SRCROOT}" \( -name "*.h" -or -name "*.m" \) -print0 | xargs -0 wc -l | awk '$1 > 200 && $2 != "total" { print $2 ":1: warning: file more than 200 lines" }'
  • 工程文件(.pbxproj)

# Day 16 : 自定义控件

ObjC 中国 发布

该篇文章比较基础,介绍了自定义控件用到的 UIView 基类、渲染、交互、本地化及测试:

  • 视图层次概览
    – reponders (响应者):UIView 的父类,能够处理触摸、手势、远程控制等事件。
    – views (视图):视图的区域是由它的 frame 定义的。实际上 frame 是一个派生属性,是由 center 和 bounds 合成而来。
    – controls (控件):建立在视图上,增加了更多的交互支持。最重要的是,它增加了 target / action 模式。
  • 渲染
    – 尽量避免 drawRect:,使用现有的视图构建自定义视图。如果重写 drawRect:,确保检查内容模式。默认的模式是将内容缩放以填充视图的范围,这在当视图的 frame 改变时并不会重新绘制。
    – 处理图片时,你也可以让 GPU 为你工作来代替使用 Core Graphics。使用 Core Image,你不必用 CPU 做任何的工作就可以在图片上建立复杂的效果。
  • 自定义交互
    – 使用 Target-Action
    – 使用代理
    – 使用 Block
    – 使用 KVO
    – 使用通知
  • 辅助功能 (Accessibility)
    – 本地化:使用 NSLocalizedString 本地化字符串。
    – 测试:可以使用 UIAutomation 或者其它基于它的工具。

# Day 15 : 底层并发 API

ObjC 中国 发布

该篇文章并非是专门介绍 GCD(Grand Central Dispatch) 使用的,但从分析及解决并发问题的角度出发介绍了很多 GCD 的用法

  • 从前…:dispatch_once
  • 延后执行:dispatch_after
    – 队列
    – 目标队列:为一个类创建它自己的队列而不是使用全局的队列,这种方式可以设置队列的名字,在 Xcode 的 Debug Navigator 中可以看到所有的队列名字,(lldb) thread list 命令会在控制台打印出所有队列的名字。
    – 优先级
  • 隔离
    – 资源保护
    – 单一资源的多读单写:dispatch_barrier_async
    – 锁竞争
    – 全都使用异步分发:dispatch_async()
    – 如何写出好的异步 API
  • 迭代执行:dispatch_apply
    – 组:dispatch_group_t
    – 对现有API使用 dispatch_group_t
  • 事件源:dispatch_source_t
    – 监视进程
    – 监视文件
    – 定时器
    – 取消
  • 输入输出
    – GCD 和缓冲区:dispatch_data_t
    – 读和写:dispatch_io_create_with_pathdispatch_io_readdispatch_io_writedispatch_io_close
  • 基准测试:uint64_t dispatch_benchmark(size_t count, void (^block)(void));能够测量给定的代码执行的平均的纳秒数
  • 原子操作
    – 计数器
    – 比较和交换
    – 原子队列
    – 自旋锁

# Day 14 : 常见的后台实践

ObjC 中国 发布

作者从以下方面介绍了将耗时操作放进后台执行的策略:
– 后台的 Core Data
– 更新 Main Context
– 后台 UI 代码
– 后台绘制:如果确定 drawRect: 是应用的性能瓶颈,把 drawRect:中的代码放到后台操作中去。然后将原本打算绘制的视图用一个 image view 来替换,等到操作执行完后再去更新。在绘制的方法中,使用 UIGraphicsBeginImageContextWithOptions 来取代 UIGraphicsGetCurrentContext :

1
2
3
4
5
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
// drawing code here
UIImage *i = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return i;

通过在第三个参数中传入 0 ,设备的主屏幕的 scale 将被自动传入,这将使图片在普通设备和 retina 屏幕上都有良好的表现。

– 异步网络请求处理:像 AFNetworking 这样的框架:建立一个独立的线程,为建立的线程设置自己的 run loop,然后在其中调度 URL 连接。
– 进阶:后台文件 I/O:对于不方便一次性读入内存的大文件可以使用异步处理文件的 NSInputStream
。。。。。。

# Day 13 : 并发编程:API 及挑战

ObjC 中国 发布

##1、OS X 和 iOS 中的并发编程
– 线程
可以使用 Instruments 中的 CPU strategy view 来得知代码是如何在多核 CPU 中调度执行的。
– Grand Central Dispatch
GCD 公开有 5 个不同的队列:运行在主线程中的 main queue,3 个不同优先级的后台队列,以及一个优先级更低的后台队列(用于 I/O)。 另外,开发者可以创建自定义队列:串行或者并行队列。自定义队列非常强大,在自定义队列中被调度的所有 block 最终都将被放入到系统的全局队列中和线程池中。
强烈建议在绝大多数情况下使用默认的优先级队列,避免在不同优先级的队列中调度访问共享资源的任务而产生不可预期的行为,比如低优先级的任务阻塞了高优先级任务而造成程序的完全挂起。
– Operation Queues
NSOperationQueue 有两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。你可以通过重写 main 或者 start 方法来定义自己的 operations。
你可以通过 maxConcurrentOperationCount 属性来控制一个特定队列中可以有多少个操作参与并发执行,还可以在 operation 之间指定依赖关系。
– Run Loops
主线程一般来说都已经配置好了 main run loop。然而其他线程默认情况下都没有设置 run loop。你也可以自行为其他线程设置 run loop ,但是一般来说我们很少需要这么做。大多数时间使用 main run loop 会容易得多。

##2、并发编程中面临的挑战
– 资源共享
– 互斥锁
– 死锁
– 资源饥饿(Starvation)
– 优先级反转

##3、总结
我们建议采纳的安全模式是这样的:从主线程中提取出要使用到的数据,并利用一个操作队列在后台处理相关的数据,最后回到主队列中来发送你在后台队列中得到的结果。使用这种方式,你不需要自己做任何锁操作,这也就大大减少了犯错误的几率。

# Day 12 : 理解 Scroll Views

ObjC 中国 发布

该篇文章比较简洁,从 UIView 的光栅化和组合说起,简单明了的介绍了 Scroll View 的实现思路。
并介绍了 Scroll View 的:
– Content Offset:相当于更改 Superview.bounds.origin
– Content Size:可滚动区域
– Content Insets:可以改变 content offset 的最大和最小值,这样便可以滚动出可滚动区域

或许可以应用在有键盘弹出的页面,比如登录页面,弹出键盘输入用户名密码时会遮挡屏幕下半部的视图,可以采用 Scroll View 的滚动特性来解决这一尴尬,恰巧今天碰见 Medium 的登录页面貌似就是这么设计的。

# Day 11 : 绘制像素到屏幕上

ObjC 中国 发布

文章知识比较碎却又是全集中在屏幕绘制这一块,许多知识点都基于原理特别受用。以下是文章中提及的知识点(目录):
– 图形堆栈
– 软件组成:Display <-> GPU <-> GPU Driver <-> OpenGL(Open Graphics Library) <-> Core Animation/Core Graphics/Core Image <-> app
– 硬件参与者
– 合成:对于屏幕上的每一个像素,GPU 需要算出怎么混合这些纹理来得到像素 RGB 的值,并最终显示在屏幕上。所以减少视图层级可以有效提高性能,因为 GPU 需要将重叠的视图计算合成在一起(将纹理中的一个像素合成到另一个纹理的像素上)。
– 不透明 VS 透明:当源纹理是完全不透明的时候,目标像素就等于源纹理。这可以省下 GPU 很大的工作量,这样只需简单的拷贝源纹理而不需要合成所有的像素值。CALayer 有一个叫做 opaque 的属性,如果这个属性为 YES,GPU 将不会做任何合成,而是简单从这个层拷贝,不需要考虑它下方的任何东西(因为都被它遮挡住了),这节省了 GPU 相当大的工作量。Instruments 中 color blended layers 选项中所涉及的,允许你看到哪一个 layers(纹理) 被标注为透明的。
– 像素对齐 VS 不重合在一起:「缩放」和「纹理的起点不在一个像素的边界上」会导致一个 layer 上所有的像素和屏幕上的像素不对齐,GPU 需要再做额外的计算。它需要将源纹理上多个像素混合起来,生成一个用来合成的值。
– Masks(蒙板):mask 是一个拥有 alpha 值的位图,当像素要和它下面包含的像素合并之前都会把 mask 应用到图层的像素上去。当你要设置一个图层的圆角半径时,你可以有效的在图层上面设置一个 mask。
– 离屏渲染(Offscreen Rendering):为 layer 使用蒙板、设置圆角半径、产生阴影会造成屏幕外渲染。Instrument 的 Core Animation 工具有一个叫做 Color Offscreen-Rendered Yellow 的选项,它会将已经被渲染到屏幕外缓冲区的区域标注为黄色。同时记得检查 Color Hits Green and Misses Red 选项。绿色代表无论何时一个屏幕外缓冲区被复用,而红色代表当缓冲区被重新创建。
– 更多的关于合成
– OS X
– Core Animation OpenGL ES
– CPU 限制 VS GPU 限制:你可以使用 OpenGL ES Driver instrument,点击上面那个小的 i 按钮,配置一下,同时注意勾选 Device Utilization %。现在,当你运行你的 app 时,你可以看到你 GPU 的负荷。
– Core Graphics / Quartz 2D
– CGLayer
– 像素
– 默认的像素布局
– 深奥的布局
– 二维数据
– YCbCr
– 图片格式
– JPEG
– PNG
– 挑选一个格式
– UIKit 和 Pixels
– With –drawRect:
– 不使用 -drawRect:
– 实现-drawRect: 还是不实现 -drawRect:
– 单一颜色
– 可变尺寸的图像
– 并发绘图
– CALayer
– 自定义绘制的图层
– 形状和文本图层
– 异步绘图

# Day 10 : 谈谈 iOS 中图片的解压缩

雷纯锋的技术博客 发布

##1、图片加载
图片加载的工作流参考 FastImageCache 在 GitHub 上的 README.md。
图片在渲染到 UIImageView 的图层之前需要将其解压缩,而对图片解压缩默认是在主线程完成的,并且是一个非常耗时的操作,在对性能要求比较高时可以在图片渲染到屏幕之前在子线程提前对图片进行强制解压缩,强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate :

1
2
3
4
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

其中:
– data:如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;
– width 和 height:位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
– bitsPerComponent:像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
– bytesPerRow:位图的每一行使用的字节数,大小至少为 width
bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化;
– space:就是我们前面提到的颜色空间,一般使用 RGB 即可;
– bitmapInfo:就是我们前面提到的位图的布局信息。
涉及到的概念:
Pixel Format(像素格式)、Color and Color Spaces(颜色空间)、Color Spaces and Bitmap Layout(位图布局)

##2、开源库
贴出了 YYKit 中存在于 YYImageCoder 类中用于解压缩图片的核心代码的函数 YYCGImageCreateDecodedCopy。讲解代码步骤并对 YYKit,SDWebImage,FLAnimatedImage 做了性能比对。

# Day 9 : Objective-C Fast Enumeration 的实现原理

雷纯锋的技术博客 发布

##1、解析 NSFastEnumeration 协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
Returns by reference a C array of objects over which the sender should iterate, and as the return value the number of objects in the array.

@param state Context information that is used in the enumeration to, in addition to other possibilities, ensure that the collection has not been mutated.
@param buffer A C array of objects over which the sender is to iterate.
@param len The maximum number of objects to return in stackbuf.

@discussion The state structure is assumed to be of stack local memory, so you can recast the passed in state structure to one more suitable for your iteration.

@return The number of objects returned in stackbuf. Returns 0 when the iteration is finished.
*/
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id __unsafe_unretained [])stackbuf
count:(NSUInteger)len

结构体 NSFastEnumerationState 的定义:

1
2
3
4
5
6
typedef struct {
unsigned long state;
id __unsafe_unretained _Nullable * _Nullable itemsPtr;
unsigned long * _Nullable mutationsPtr;
unsigned long extra[5];
} NSFastEnumerationState;

##2、快速枚举的内部实现
通过 clang -rewrite-objc main.m 命令重写了一段 OC 的快速枚举代码,详尽的注释介绍了快速枚举的内部实现,及在 for/in 语句中利用 goto 实现 continuebreak 命令的 c++ 代码。

##3、实现 NSFastEnumeration 协议
作者给出了一个实现 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])stackbuf count:(NSUInteger)len; 的例子

##3、参考链接
在文末的参考链接中有文 Friday Q&A 2010-04-16: Implementing Fast Enumeration 同样介绍了快速枚举的实现。

# Day 8 : iOS 并发编程之 Operation Queues

雷纯锋的技术博客 发布

这篇文章比较长,且相当详细,很多也是 Operation Queues 的基础用法,所以这里就只将文章目录记下来好了,下次复习(的话)先来过一遍目录,记不起的再去查阅文章吧。
该篇文章详细介绍了 Operation Queues 的一系列使用,并且 SDWebImage 也用到了 Operation Queues,对于不久后去阅读 SDWebImage 源码想必会很有帮助。

##1、基本概念
– 进程(process)、线程(thread)、任务(task)
– 串行 vs. 并发
– 同步 vs. 异步
– 队列 vs. 线程的概念。

##2、iOS 的并发编程模型

##3、Operation Queues vs. Grand Central Dispatch (GCD)

##4、关于 Operation 对象
– 并发 vs. 非并发 Operation
– 创建 NSInvocationOperation 对象
– 创建 NSBlockOperation 对象

##5、自定义 Operation 对象
– 执行主任务
– 响应取消事件
– 配置并发执行的 Operation
– 维护 KVO 通知

##6、定制 Operation 对象的执行行为
– 配置依赖关系
– 修改 Operation 在队列中的优先级
– 修改 Operation 执行任务线程的优先级
– 设置 Completion Block

##7、执行 Operation 对象
– 添加 Operation 到 Operation Queue 中
– 手动执行 Operation
– 取消 Operation
– 等待 Operation 执行完成
– 暂停和恢复 Operation Queue

##8、总结

# Day 7 : Objective-C Method Swizzling 的最佳实践

雷纯锋的技术博客 发布

##1、Method 的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct method_t {
SEL name; // 方法的名称,用于唯一标识某个方法,比如 @selector(viewWillAppear:)
const char *types; // 方法的返回值和参数类型
IMP imp; // 函数指针,指向方法的实现

struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
}; // 一个根据 name 的地址对方法进行排序的函数
};

– 实例方法保存在类对象中,类方法保存在元类对象中。(参见Objective-C 对象模型
– 原则上,方法的名称 name 和方法的实现 imp 是一一对应的,而 Method Swizzling 的原理就是动态地改变它们的对应关系,以达到替换方法实现的目的。

##2、Method Swizzling 的最佳实践
+load 和 +initialize 是 Objective-C runtime 会自动调用的两个类方法。
– +initialize 方法以懒加载的方式被调用的,有可能不会被调用,而 +load 方法是在类被加载的时候调用。
– Objective-C runtime 自动调用 +load 方法时,分类中的 +load 方法并不会对主类中的 +load 方法造成覆盖
综上,分类中的 +load 方法是实现 Method Swizzling 逻辑的最佳“场所”
– 应当使用 dispatch_once 保证 Method Swizzling 只被调用一次

文章示例代码:
– 主类本身有实现需要替换的方法,即返回 NO 时,直接交换两个方法的实现;
– 主类本身没有实现需要替换的方法,而是继承了父类的实现,即返回 YES 时,将父类的实现替换到我们自定义的 mrc_viewWillAppear 方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@implementation UIViewController (MRCUMAnalytics)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];

SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(mrc_viewWillAppear:);

Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

#pragma mark - Method Swizzling
- (void)mrc_viewWillAppear:(BOOL)animated {
[self mrc_viewWillAppear:animated];
[MobClick beginLogPageView:NSStringFromClass([self class])];
}
@end

# Day 6 : Objective-C Autorelease Pool 的实现原理

雷纯锋的技术博客 发布

##1、autoreleased 对象的释放时机
对象 A 被创建时引用计数为 1,当有变量 B 指向这个对象时,引用计数 +1,变量 B 离开作用域后为 nil,对象 A 的引用计数 -1,当对象 A 所在的 autoreleasepool 被 drain ,其中的 autoreleased 对象被 release ,对象 A 的引用计数 -1。当对象 A 的引用计数为 0 时随即被释放。

其中提到了 __weak 的两个特性:
__weak 变量不会影响所指向对象的生命周期
__weak 变量所指向的对象被释放时,__weak 变量的值会被置为 nil

##2、AutoreleasePoolPage
– 介绍了 AutoreleasePoolPage 的内存结构
– Autorelease Pool Blocks 通过 clang -rewrite-objc 改写成 c++ 代码的实现,和其中的 push 操作、autorelease 操作、pop 操作

##3、NSThread、NSRunLoop 和 NSAutoreleasePool

Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed.

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.

# Day 5 : Objective-C Associated Objects 的实现原理

雷纯锋的技术博客 发布

##1、相关函数
在 Objective-C 中可以通过 Category 给一个现有的类添加属性,但是却不能添加实例变量,我们可以通过 Associated Objects 来弥补这一不足

1
2
3
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); // 用于给对象添加关联对象,传入 nil 则可以移除已有的关联对象
id objc_getAssociatedObject(id object, const void *key); // 用于获取关联对象
void objc_removeAssociatedObjects(id object); // 用于移除一个对象的所有关联对象

##2、key 值
上边前两个函数需要传入一个 key 作为唯一变量,作者推荐 selector ,使用 getter 方法的名称作为 key 值。这样就省掉了一个变量名,解决了给这个 key 命名的烦恼。

##3、关联策略

关联策略 等价属性 说明
OBJC_ASSOCIATION_ASSIGN @property (assign) or @property (unsafe_unretained) 弱引用关联对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (strong, nonatomic) 强引用关联对象,且为非原子操作
OBJC_ASSOCIATION_COPY_NONATOMIC @property (copy, nonatomic) 复制关联对象,且为非原子操作
OBJC_ASSOCIATION_RETAIN @property (strong, atomic) 强引用关联对象,且为原子操作
OBJC_ASSOCIATION_COPY @property (copy, atomic) 复制关联对象,且为原子操作

##4、实现 getter setter 方法
ViewController+AssociatedObjects.h:

1
2
3
@interface ViewController (AssociatedObjects)
@property (nonatomic, copy) NSString *associatedObject_copy;
@end

ViewController+AssociatedObjects.m:

1
2
3
4
5
6
7
8
@implementation ViewController (AssociatedObjects)
- (NSString *)associatedObject_copy {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setAssociatedObject_copy:(NSString *)associatedObject_copy {
objc_setAssociatedObject(self, @selector(associatedObject_copy), associatedObject_copy, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

##5、总结
– 关联对象与被关联对象本身的存储并没有直接的关系,它是存储在单独的哈希表中的;
– 关联对象的五种关联策略与属性的限定符非常类似,在绝大多数情况下,我们都会使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的关联策略,这可以保证我们持有关联对象;
– 关联对象的释放时机与移除时机并不总是一致,比如实验中用关联策略 OBJC_ASSOCIATION_ASSIGN 进行关联的对象,很早就已经被释放了,但是并没有被移除,而再使用这个关联对象时就会造成 Crash 。

# Day 4 : Objective-C +load vs +initialize

雷纯锋的技术博客 发布

##1、+load
– 调用所有类的 +load 方法(包括分类的 +load 方法)是直接使用函数内存地址的方式 (*load_method)(cls, SEL_load); 进行调用的,而不是使用发送消息 objc_msgSend 的方式。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。
– 子类的 +load 方法会在它的所有父类的 +load 方法之后执行,而分类的 +load 方法会在它的主类的 +load 方法之后执行。但是不同的类之间的 +load 方法的调用顺序是不确定的。

##2、+initialize
– runtime 使用了发送消息 objc_msgSend 的方式对 +initialize 方法进行调用。如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。
– 如果一个子类没有实现 +initialize 方法,那么父类的实现是会被执行多次的。可以使用下面的代码确保自己的 +initialize 方法只执行一次:

1
2
3
4
5
+ (void)initialize {
if (self == [ClassName self]) {
// ... do the initialization ...
}
}

##3、总结

+load +initialize
调用时机 被添加到 runtime 时 收到第一条消息前,可能永远不调用
调用顺序 父类->子类->分类 父类->子类
调用次数 1次 多次
是否需要显式调用父类实现
是否沿用父类的实现
分类中的实现 类和分类都执行 覆盖类中的方法,只执行分类的实现

# Day 3 : Objective-C对象模型及应用

唐巧的博客 发布

##1、ISA 指针
– 每一个对象都有一个名为 isa 的指针,指向该对象的类。
– 在 Objective-C 语言中,每一个类实际上也是一个对象,每一个类也有一个名为 isa 的指针,指向该类的元类。
– 元类的 isa 指针指向根元类。
– 根元类的 isa 指针指向自己。

(文章中有一张图直白的说明了 isa 指针的指向)

##2、Method Swizzling API 说明
class_replaceMethod, 当需要替换的方法可能有不存在的情况时,可以考虑使用该方法。
method_exchangeImplementations,当需要交换 2 个方法的实现时使用。
method_setImplementation 最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。

# Day 2 : 谈Objective-C block的实现

唐巧的博客 发布

##1、block 的 struct 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};

struct Block_layout {
void *isa; // 所有对象都有该指针,用于实现对象相关的功能。
int flags; // 用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。
int reserved; // 保留变量。
void (*invoke)(void *, ...); // 函数指针,指向具体的 block 实现的函数调用地址。
struct Block_descriptor *descriptor; // 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
/* Imported variables. */
};

##2、clang 命令改写 OC 语言下的 block
使用 clang 的 clang -rewrite-objc block.c 命令,可以将 Objetive-C 的源码改写成 c 语言的,生成的 .cpp 文件中有许多关键代码展示了 block 的实现原理。

其中,block 中操作未经 __block 修饰的外部变量时,.cpp 文件中相关代码为:

1
2
3
4
5
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy

printf("%d\n", a);
}

block 中操作经 __block 修饰的外部变量时,.cpp 文件中相关代码为:

1
2
3
4
5
6
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref

printf("%d\n", (a->__forwarding->a));
(a->__forwarding->a) = 1023;
}

结论:
1、在 block 内部修改变量 a 的内容,不会影响外部的实际变量 a,是因为在 block 内部 copy 了一份变量 a。
2、当变量使用 __block 修饰时,block 内部是引用的外部变量的指针,所以可以修改外部的实际变量 a。

# Day 1 : 深入理解Objective-C:Category

美团点评技术团队 发布

##1、category 在 runtime 层用结构体 category_t 定义:

1
2
3
4
5
6
7
8
typedef struct category_t {
const char *name; // 类的名字
classref_t cls; // 类
struct method_list_t *instanceMethods; // category中所有给类添加的实例方法的列表
struct method_list_t *classMethods; // category中所有添加的类方法的列表
struct protocol_list_t *protocols; // category实现的所有协议的列表
struct property_list_t *instanceProperties; // category中添加的所有属性
} category_t;

由该结构体可以看出 category 可以动态的添加实例方法,类方法,甚至可以实现协议,添加属性,但无法添加实例变量
因为无法添加实例变量,所以当采用 category 为类添加属性时,并不会自动生成带有 _ 的实例变量,所以 settergetter 方法也需要自己去实现,在文章末尾有介绍采用关联对象来实现,使用 objc_setAssociatedObjectobjc_getAssociatedObject 方法。

##2、category 替换原来类已有的方法
category 的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休。
所以,category 的方法没有“完全替换掉”原来类已经有的方法。在文章第六节也有介绍可以使用 class_copyMethodList 方法获取方法列表,顺着方法列表找到最后一个对应名字的方法,就可以调用原来类的方法

##3、-category 和 +load 方法
在类的 +load 方法调用的时候,我们可以调用 category 中声明的方法,因为附加 category 到类的工作会先于 +load 方法的执行
+load 的执行顺序是先类,后 category,而 category 的 +load 执行顺序是根据编译顺序决定的(Compile Sources(4 items)里的顺序)

后记