├── Categroy └── iOS │ ├── Aspects │ ├── aspects.md │ └── aspects │ │ ├── aop.jpg │ │ ├── aspects.jpg │ │ ├── aspects_core.png │ │ ├── aspects_logo.jpg │ │ ├── aspects_rank.jpg │ │ └── aspects_struct.png │ ├── BlocksKit │ └── blocks-kit.md │ ├── Tips │ ├── oc-class-properties.md │ └── oc-class-properties │ │ ├── after-decoupling.png │ │ ├── before-decoupling.png │ │ ├── llvm.jpg │ │ ├── oc-class-properties.jpg │ │ └── oc-feature.jpg │ ├── WWDC │ ├── ios-rendering-process.md │ └── ios-rendering-process │ │ ├── animation.png │ │ ├── core_animation_pipeline.png │ │ ├── ios_rendering_framework.png │ │ ├── opengl_rendering_pipeline.png │ │ ├── rasterization.jpg │ │ ├── rendering.jpg │ │ ├── rendering_pass.png │ │ └── tile_based_rendering.jpg │ ├── WebViewJavascriptBridge │ ├── webview-javascript-bridge.md │ └── webview-javascript-bridge │ │ ├── img_bridge.png │ │ ├── img_bridge_beautiful.jpg │ │ ├── img_golden_bridge.jpeg │ │ ├── img_javascript.jpg │ │ ├── img_merry_ch.jpg │ │ ├── img_pier.jpg │ │ └── img_tower_bridge.jpg │ └── YYKit │ ├── yycache.md │ ├── yycache │ ├── cache-hit-ratio.png │ ├── good-cache.jpg │ ├── how-to-design-a-good-cache.jpg │ ├── lock_benchmark.jpg │ ├── performance-yydiskcache.jpg │ ├── performance-yymemorycache.jpg │ ├── yycache.jpg │ ├── yydiskcache.jpg │ └── yymemorycache.jpg │ ├── yyimage.md │ ├── yyimage │ ├── blood_wheel_eye.jpeg │ ├── image_coder.jpg │ ├── ss_wukong.png │ ├── xiaomai.gif │ ├── yyimage.jpg │ ├── yyimage_h.jpg │ └── yyimage_struct.png │ ├── yymodel_x01.md │ ├── yymodel_x01 │ ├── class-diagram.jpg │ ├── design-model-x01.jpg │ ├── nsobject-yymodel.jpg │ ├── yyclassinfo.jpg │ ├── yymodel-performance.png │ └── yymodel.png │ ├── yymodel_x02.md │ └── yymodel_x02 │ ├── d2m.jpg │ ├── design-model-x02.jpg │ ├── j2d2m.jpg │ ├── m2j.jpg │ ├── switch.jpg │ └── wechat.jpg ├── README.md └── Resources ├── license.png └── pixiv.jpg /Categroy/iOS/Aspects/aspects.md: -------------------------------------------------------------------------------- 1 | # 从 Aspects 源码中我学到了什么? 2 | 3 | ![](aspects/aspects.jpg) 4 | 5 | ## 前言 6 | 7 | [AOP (Aspect-oriented programming)](https://en.wikipedia.org/wiki/Aspect-oriented_programming) 译为 “面向切面编程”,是通过预编译方式和运行期动态代理实现程序功能统一维护的一种技术。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 8 | 9 | Emmmmm...AOP 目前是较为热门的一个话题,尽管你也许没有听说过它,但是你的项目中可能已经渗入了它,例如:用户统计(不添加一行代码即实现对所有 ViewController 的跟踪日志)。 10 | 11 | 对于 iOS 开发者而言,无外乎 Swift 和 Objective-C 两种主流开发语言: 12 | 13 | - Swift 受限于 ABI 尚未稳定,动态性依赖 `dynamic` 修饰符,在 Runtime 没有留给我们太多的发挥空间(前几日新增了 `swift-5.0-branch` 分支,写这篇文章时看了一眼 `181 commits behind master` 😂)。 14 | - Objective-C 在动态性上相对 Swift 具有无限大的优势,这几年 Objective-C Runtime 相关文章多如牛毛,相信现在的 iOSer 都具备一定的 Runtime 相关知识。 15 | 16 | [Aspects](https://github.com/steipete/Aspects) 作为 Objective-C 语言编写的 AOP 库,适用于 iOS 和 Mac OS X,使用体验简单愉快,已经在 GitHub 摘得 5k+ Star。Aspects 内部实现比较健全,考虑到了 Hook 安全方面可能发生的种种问题,非常值得我们学习。 17 | 18 | > Note: 本文内引用 Aspects 源码版本为 v1.4.2,要求读者具备一定的 Runtime 知识。 19 | 20 | ## 索引 21 | 22 | - AOP 简介 23 | - Aspects 简介 24 | - Aspects 结构剖析 25 | - Aspects 核心代码剖析 26 | - 优秀 AOP 库应该具备的特质 27 | - 总结 28 | 29 | ## AOP 简介 30 | 31 | ![](aspects/aop.jpg) 32 | 33 | > 在**运行时,动态地**将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。 34 | 35 | [AOP (Aspect-oriented programming)](https://en.wikipedia.org/wiki/Aspect-oriented_programming),即 “面向切面编程” 是一种编程范式,或者说是一种编程思想,它解决了 [OOP (Object-oriented programming)](https://en.wikipedia.org/wiki/Object-oriented_programming) 的延伸问题。 36 | 37 | ### 什么时候需要使用 AOP 38 | 39 | 光是给个概念可能初次接触 AOP 的人还是无法 Get 到其中微秒,拿我们前言中举的例子🌰,假设随着我们所在的公司逐步发展,之前第三方的用户页面统计已经不能满足需求了,公司要求实现一个我们自己的用户页面统计。 40 | 41 | 嘛~ 我们来理一下 OOP 思想下该怎么办? 42 | 43 | - 一个熟悉 OOP 思想的程序猿会理所应当的想到要把用户页面统计这一任务放到 ViewController 中; 44 | - 考虑到一个个的手动添加统计代码要死人(而且还会漏,以后新增 ViewController 也要手动加),于是想到了 OOP 思想中的继承; 45 | - 不巧由于项目久远,所有的 ViewController 都是直接继承自系统类 UIViewController(笑),此时选择抽一个项目 RootViewController,替换所有 ViewController 继承 RootViewController; 46 | - 然后在 RootViewController 的 `viewWillAppear:` 和 `viewWillDisappear:` 方法加入时间统计代码,记录 ViewController 以及 Router 传参。 47 | 48 | 你会想,明明 OOP 也能解决问题是不是?不要急,再假设你们公司有多个 App,你被抽调至基础技术组专门给这些 App 写**通用**组件,要把之前实现过的用户页面统计重新以**通用**的形式实现,提供给你们公司所有的 App 使用。 49 | 50 | MMP,使用标准 OOP 思想貌似无解啊...这个时候就是 AOP 的用武之地了。 51 | 52 | 这里简单给个思路:Hook UIViewController 的 `viewWillAppear:` 和 `viewWillDisappear:` 方法,在原方法执行之后记录需要统计的信息上报即可。 53 | 54 | > Note: 简单通过 Method Swizzling 来 Hook 不是不可以,但是有很多安全隐患! 55 | 56 | ## Aspects 简介 57 | 58 | ![](aspects/aspects_logo.jpg) 59 | 60 | [Aspects](https://github.com/steipete/Aspects) 是一个使用起来简单愉快的 AOP 库,使用 Objective-C 编写,适用于 iOS 与 Mac OS X。 61 | 62 | > Aspects 内部实现考虑到了很多 Hook 可能引发的问题,笔者在看源码的过程中抠的比较细,真的是受益匪浅。 63 | 64 | Aspects 简单易用,作者通过在 `NSObject (Aspects)` 分类中暴露出的两个接口分别提供了对实例和 Class 的 Hook 实现: 65 | 66 | ``` obj-c 67 | @interface NSObject (Aspects) 68 | 69 | + (id)aspect_hookSelector:(SEL)selector 70 | withOptions:(AspectOptions)options 71 | usingBlock:(id)block 72 | error:(NSError **)error; 73 | 74 | - (id)aspect_hookSelector:(SEL)selector 75 | withOptions:(AspectOptions)options 76 | usingBlock:(id)block 77 | error:(NSError **)error; 78 | 79 | @end 80 | ``` 81 | 82 | Aspects 支持实例 Hook,相较其他 Objective-C AOP 库而言可操作粒度更小,适合的场景更加多样化。作为使用者无需进行更多的操作即可 Hook 指定实例或者 Class 的指定 SEL,AspectOptions 参数可以指定 Hook 的点,以及是否执行一次之后就撤销 Hook。 83 | 84 | ## Aspects 结构剖析 85 | 86 | ![](aspects/aspects_struct.png) 87 | 88 | Emmmmm...尽管 Aspects 只有不到千行的源码,但是其内部实现考虑到了很多 Hook 相关的安全问题和其他细节,对比其他 Objective-C AOP 开源项目来说 Aspects 更为健全,所以我自己在扒 Aspects 源码时也看的比较仔细。 89 | 90 | ### Aspects 内部结构 91 | 92 | Aspects 内部定义了两个协议: 93 | 94 | - AspectToken - 用于注销 Hook 95 | - AspectInfo - 嵌入 Hook 中的 Block 首位参数 96 | 97 | 此外 Aspects 内部还定义了 4 个类: 98 | 99 | - AspectInfo - 切面信息,遵循 AspectInfo 协议 100 | - AspectIdentifier - 切面 ID,**应该**遵循 AspectToken 协议(作者漏掉了,已提 PR) 101 | - AspectsContainer - 切面容器 102 | - AspectTracker - 切面跟踪器 103 | 104 | 以及一个结构体: 105 | 106 | - AspectBlockRef - 即 `_AspectBlock`,充当内部 Block 107 | 108 | 如果你扒一遍源码,还会发现两个内部静态全局变量: 109 | 110 | - `static NSMutableDictionary *swizzledClassesDict;` 111 | - `static NSMutableSet *swizzledClasses;` 112 | 113 | 现在你也许还不能理解为什么要定义这么多东西,别急~ 我们后面都会分析到。 114 | 115 | ### Aspects 协议 116 | 117 | 按照上面列出的顺序,先来介绍一些 Aspects 声明的协议。 118 | 119 | #### AspectToken 120 | 121 | AspectToken 协议旨在让使用者可以灵活的注销之前添加过的 Hook,内部规定遵守此协议的对象须实现 `remove` 方法。 122 | 123 | ``` obj-c 124 | /// 不透明的 Aspect Token,用于注销 Hook 125 | @protocol AspectToken 126 | 127 | /// 注销一个 aspect. 128 | /// 返回 YES 表示注销成功,否则返回 NO 129 | - (BOOL)remove; 130 | 131 | @end 132 | ``` 133 | 134 | #### AspectInfo 135 | 136 | AspectInfo 协议旨在规范对一个切面,即 aspect 的 Hook 内部信息的纰漏,我们在 Hook 时添加切面的 Block 第一个参数就遵守此协议。 137 | 138 | ``` obj-c 139 | /// AspectInfo 协议是我们块语法的第一个参数。 140 | @protocol AspectInfo 141 | 142 | /// 当前被 Hook 的实例 143 | - (id)instance; 144 | 145 | /// 被 Hook 方法的原始 invocation 146 | - (NSInvocation *)originalInvocation; 147 | 148 | /// 所有方法参数(装箱之后的)惰性执行 149 | - (NSArray *)arguments; 150 | 151 | @end 152 | ``` 153 | 154 | > Note: 装箱是一个开销昂贵操作,所以用到再去执行。 155 | 156 | ### Aspects 内部类 157 | 158 | 接着协议,我们下面详细介绍一下 Aspects 的内部类。 159 | 160 | #### AspectInfo 161 | 162 | > Note: AspectInfo 在这里是一个 Class,其遵守上文中讲到的 AspectInfo 协议,不要混淆。 163 | 164 | AspectInfo 类定义: 165 | 166 | ``` obj-c 167 | @interface AspectInfo : NSObject 168 | 169 | - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation; 170 | 171 | @property (nonatomic, unsafe_unretained, readonly) id instance; 172 | @property (nonatomic, strong, readonly) NSArray *arguments; 173 | @property (nonatomic, strong, readonly) NSInvocation *originalInvocation; 174 | 175 | @end 176 | ``` 177 | 178 | > Note: 关于装箱,对于提供一个 NSInvocation 就可以拿到其 `arguments` 这一点上,ReactiveCocoa 团队提供了很大贡献(细节见 Aspects 内部 NSInvocation 分类)。 179 | 180 | AspectInfo 比较简单,参考 ReactiveCocoa 团队提供的 NSInvocation 参数通用方法可将参数装箱为 NSValue,简单来说 AspectInfo 扮演了一个提供 Hook 信息的角色。 181 | 182 | #### AspectIdentifier 183 | 184 | AspectIdentifier 类定义: 185 | 186 | ``` obj-c 187 | @interface AspectIdentifier : NSObject 188 | 189 | + (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error; 190 | 191 | - (BOOL)invokeWithInfo:(id)info; 192 | 193 | @property (nonatomic, assign) SEL selector; 194 | @property (nonatomic, strong) id block; 195 | @property (nonatomic, strong) NSMethodSignature *blockSignature; 196 | @property (nonatomic, weak) id object; 197 | @property (nonatomic, assign) AspectOptions options; 198 | 199 | @end 200 | ``` 201 | 202 | > Note: AspectIdentifier 实际上是添加切面的 Block 的第一个参数,其应该遵循 AspectToken 协议,事实上也的确如此,其提供了 `remove` 方法的实现。 203 | 204 | AspectIdentifier 内部需要注意的是由于使用 Block 来写 Hook 中我们加的料,这里生成了 `blockSignature`,在 AspectIdentifier 初始化的过程中会去判断 `blockSignature` 与入参 `object` 的 `selector` 得到的 `methodSignature` 的兼容性,兼容性判断成功才会顺利初始化。 205 | 206 | #### AspectsContainer 207 | 208 | AspectsContainer 类定义: 209 | 210 | ``` obj-c 211 | @interface AspectsContainer : NSObject 212 | 213 | - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition; 214 | - (BOOL)removeAspect:(id)aspect; 215 | - (BOOL)hasAspects; 216 | 217 | @property (atomic, copy) NSArray *beforeAspects; 218 | @property (atomic, copy) NSArray *insteadAspects; 219 | @property (atomic, copy) NSArray *afterAspects; 220 | 221 | @end 222 | ``` 223 | 224 | AspectsContainer 作为切面的容器类,**关联**指定对象的指定方法,内部有三个切面队列,分别容纳关联指定对象的指定方法中相对应 AspectOption 的 Hook: 225 | 226 | - `NSArray *beforeAspects;` - AspectPositionBefore 227 | - `NSArray *insteadAspects;` - AspectPositionInstead 228 | - `NSArray *afterAspects;` - AspectPositionAfter 229 | 230 | 为什么要说关联呢?因为 AspectsContainer 是在 NSObject 分类中通过 AssociatedObject 方法与当前要 Hook 的目标关联在一起的。 231 | 232 | > Note: 关联目标是 Hook 之后的 Selector,即 `aliasSelector`(原始 SEL 名称加 `aspects_` 前缀对应的 SEL)。 233 | 234 | #### AspectTracker 235 | 236 | AspectTracker 类定义: 237 | 238 | ``` obj-c 239 | @interface AspectTracker : NSObject 240 | 241 | - (id)initWithTrackedClass:(Class)trackedClass parent:(AspectTracker *)parent; 242 | 243 | @property (nonatomic, strong) Class trackedClass; 244 | @property (nonatomic, strong) NSMutableSet *selectorNames; 245 | @property (nonatomic, weak) AspectTracker *parentEntry; 246 | 247 | @end 248 | ``` 249 | 250 | AspectTracker 作为切面追踪器,原理大致如下: 251 | 252 | ``` obj-c 253 | // Add the selector as being modified. 254 | currentClass = klass; 255 | AspectTracker *parentTracker = nil; 256 | do { 257 | AspectTracker *tracker = swizzledClassesDict[currentClass]; 258 | if (!tracker) { 259 | tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker]; 260 | swizzledClassesDict[(id)currentClass] = tracker; 261 | } 262 | [tracker.selectorNames addObject:selectorName]; 263 | // All superclasses get marked as having a subclass that is modified. 264 | parentTracker = tracker; 265 | }while ((currentClass = class_getSuperclass(currentClass))); 266 | ``` 267 | 268 | > Note: 聪明的你应该已经注意到了全局变量 `swizzledClassesDict` 中的 `value` 对应着 AspectTracker 指针。 269 | 270 | 嘛~ 就是说 AspectTracker 是从下而上追踪,最底层的 `parentEntry` 为 `nil`,父类的 `parentEntry` 为子类的 `tracker`。 271 | 272 | ### Aspects 内部结构体 273 | 274 | #### AspectBlockRef 275 | 276 | AspectBlockRef,即 `struct _AspectBlock`,其定义如下: 277 | 278 | ``` obj-c 279 | typedef struct _AspectBlock { 280 | __unused Class isa; 281 | AspectBlockFlags flags; 282 | __unused int reserved; 283 | void (__unused *invoke)(struct _AspectBlock *block, ...); 284 | struct { 285 | unsigned long int reserved; 286 | unsigned long int size; 287 | // requires AspectBlockFlagsHasCopyDisposeHelpers 288 | void (*copy)(void *dst, const void *src); 289 | void (*dispose)(const void *); 290 | // requires AspectBlockFlagsHasSignature 291 | const char *signature; 292 | const char *layout; 293 | } *descriptor; 294 | // imported variables 295 | } *AspectBlockRef; 296 | ``` 297 | 298 | Emmmmm...没什么特别的,大家应该比较眼熟吧。 299 | 300 | > Note: `__unused` 宏定义实际上是 `__attribute__((unused))` GCC 定语,旨在告诉编译器“如果我没有在后面使用到这个变量也别警告我”。 301 | 302 | 嘛~ 想起之前自己挖的坑还没有填,事实上自己也不知道什么时候填(笑): 303 | 304 | - 之前挖坑说要写一篇文章记录一些阅读源码时发现的代码书写技巧 305 | - 之前挖坑说要封装一个 WKWebView 给群里的兄弟参考 306 | 307 | 不要急~ 你瞧伦家不是都记得嘛(至于什么时候填坑嘛就...咳咳) 308 | 309 | ### Aspects 静态全局变量 310 | 311 | #### `static NSMutableDictionary *swizzledClassesDict;` 312 | 313 | `static NSMutableDictionary *swizzledClassesDict;` 在 Aspects 中扮演着已混写类字典的角色,其内部结构应该是这样的: 314 | 315 | ``` obj-c 316 | 317 | ``` 318 | 319 | Aspects 内部提供了专门访问这个全局字典的方法: 320 | 321 | ``` obj-c 322 | static NSMutableDictionary *aspect_getSwizzledClassesDict() { 323 | static NSMutableDictionary *swizzledClassesDict; 324 | static dispatch_once_t pred; 325 | dispatch_once(&pred, ^{ 326 | swizzledClassesDict = [NSMutableDictionary new]; 327 | }); 328 | return swizzledClassesDict; 329 | } 330 | ``` 331 | 332 | 这个全局变量可以简单理解为记录整个 Hook 影响的 Class 包含其 SuperClass 的追踪记录的全局字典。 333 | 334 | #### `static NSMutableSet *swizzledClasses;` 335 | 336 | `static NSMutableSet *swizzledClasses;` 在 Aspects 中担当记录已混写类的角色,其内部结构如下: 337 | 338 | ``` obj-c 339 | 340 | ``` 341 | 342 | Aspects 内部提供一个用于修改这个全局变量内容的方法: 343 | 344 | ``` obj-c 345 | static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) { 346 | static NSMutableSet *swizzledClasses; 347 | static dispatch_once_t pred; 348 | dispatch_once(&pred, ^{ 349 | swizzledClasses = [NSMutableSet new]; 350 | }); 351 | @synchronized(swizzledClasses) { 352 | block(swizzledClasses); 353 | } 354 | } 355 | ``` 356 | 357 | > Note: 注意 `@synchronized(swizzledClasses)`。 358 | 359 | 这个全局变量记录了 `forwardInvocation:` 被混写的的类名称。 360 | 361 | > Note: 注意在用途上与 `static NSMutableDictionary *swizzledClassesDict;` 区分理解。 362 | 363 | ## Aspects 核心代码剖析 364 | 365 | ![](aspects/aspects_core.png) 366 | 367 | 嘛~ Aspects 的整体实现代码不超过一千行,而且考虑的情况也比较全面,非常值得大家花时间去读一下,这里我只准备给出自己对其核心代码的理解。 368 | 369 | ### Hook Class && Hook Instance 370 | 371 | Aspects 不光支持 Hook Class 还支持 Hook Instance,这提供了更小粒度的控制,配合 Hook 的撤销功能可以更加灵活精准的做我们想做的事~ 372 | 373 | Aspects 为了能区别 Class 和 Instance 的逻辑,实现了名为 `aspect_hookClass` 的方法,我认为其中的实现值得我用一部分篇幅来单独讲解,也觉得读者们有必要花点时间理解这里的实现逻辑。 374 | 375 | ``` obj-c 376 | static Class aspect_hookClass(NSObject *self, NSError **error) { 377 | // 断言 self 378 | NSCParameterAssert(self); 379 | // class 380 | Class statedClass = self.class; 381 | // isa 382 | Class baseClass = object_getClass(self); 383 | NSString *className = NSStringFromClass(baseClass); 384 | 385 | // 已经子类化过了 386 | if ([className hasSuffix:AspectsSubclassSuffix]) { 387 | return baseClass; 388 | // 我们混写了一个 class 对象,而非一个单独的 object 389 | }else if (class_isMetaClass(baseClass)) { 390 | // baseClass 是元类,则 self 是 Class 或 MetaClass,混写 self 391 | return aspect_swizzleClassInPlace((Class)self); 392 | // 可能是一个 KVO'ed class。混写就位。也要混写 meta classes。 393 | }else if (statedClass != baseClass) { 394 | // 当 .class 和 isa 指向不同的情况,混写 baseClass 395 | return aspect_swizzleClassInPlace(baseClass); 396 | } 397 | 398 | // 默认情况下,动态创建子类 399 | // 拼接子类后缀 AspectsSubclassSuffix 400 | const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String; 401 | // 尝试用拼接后缀的名称获取 isa 402 | Class subclass = objc_getClass(subclassName); 403 | 404 | // 找不到 isa,代表还没有动态创建过这个子类 405 | if (subclass == nil) { 406 | // 创建一个 class pair,baseClass 作为新类的 superClass,类名为 subclassName 407 | subclass = objc_allocateClassPair(baseClass, subclassName, 0); 408 | if (subclass == nil) { // 返回 nil,即创建失败 409 | NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName]; 410 | AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc); 411 | return nil; 412 | } 413 | 414 | // 混写 forwardInvocation: 415 | aspect_swizzleForwardInvocation(subclass); 416 | // subClass.class = statedClass 417 | aspect_hookedGetClass(subclass, statedClass); 418 | // subClass.isa.class = statedClass 419 | aspect_hookedGetClass(object_getClass(subclass), statedClass); 420 | // 注册新类 421 | objc_registerClassPair(subclass); 422 | } 423 | 424 | // 覆盖 isa 425 | object_setClass(self, subclass); 426 | return subclass; 427 | } 428 | ``` 429 | 430 | > Note: 其实这里的难点就在于对 `.class` 和 `object_getClass` 的区分。 431 | 432 | - `.class` 当 target 是 Instance 则返回 Class,当 target 是 Class 则返回自身 433 | - `object_getClass` 返回 `isa` 指针的指向 434 | 435 | > Note: 动态创建一个 Class 的完整步骤也是我们应该注意的。 436 | 437 | - objc_allocateClassPair 438 | - class_addMethod 439 | - class_addIvar 440 | - objc_registerClassPair 441 | 442 | 嘛~ 难点和重点都讲完了,大家结合注释理解其中的逻辑应该没什么困难了,有什么问题可以找我一起交流~ 443 | 444 | ### Hook 的实现 445 | 446 | 在上面 `aspect_hookClass` 方法中,不仅仅是返回一个要 Hook 的 Class,期间还做了一些细节操作,不论是 Class 还是 Instance,都会调用 `aspect_swizzleForwardInvocation` 方法,这个方法没什么难点,简单贴一下代码让大家有个印象: 447 | 448 | ``` obj-c 449 | static void aspect_swizzleForwardInvocation(Class klass) { 450 | // 断言 klass 451 | NSCParameterAssert(klass); 452 | // 如果没有 method,replace 实际上会像是 class_addMethod 一样 453 | IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); 454 | // 拿到 originalImplementation 证明是 replace 而不是 add,情况少见 455 | if (originalImplementation) { 456 | // 添加 AspectsForwardInvocationSelectorName 的方法,IMP 为原生 forwardInvocation: 457 | class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@"); 458 | } 459 | AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass)); 460 | } 461 | ``` 462 | 463 | 上面的方法就是把要 Hook 的目标 Class 的 `forwardInvocation:` 混写了,混写之后 `forwardInvocation:` 的具体实现在 `__ASPECTS_ARE_BEING_CALLED__` 中,里面能看到 invoke 标识位的不同是如何实现的,还有一些其他的实现细节: 464 | 465 | ``` obj-c 466 | // 宏定义,以便于我们有一个更明晰的 stack trace 467 | #define aspect_invoke(aspects, info) \ 468 | for (AspectIdentifier *aspect in aspects) {\ 469 | [aspect invokeWithInfo:info];\ 470 | if (aspect.options & AspectOptionAutomaticRemoval) { \ 471 | aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \ 472 | } \ 473 | } 474 | 475 | static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { 476 | // __unsafe_unretained NSObject *self 不解释了 477 | // 断言 self, invocation 478 | NSCParameterAssert(self); 479 | NSCParameterAssert(invocation); 480 | // 从 invocation 可以拿到很多东西,比如 originalSelector 481 | SEL originalSelector = invocation.selector; 482 | // originalSelector 加前缀得到 aliasSelector 483 | SEL aliasSelector = aspect_aliasForSelector(invocation.selector); 484 | // 用 aliasSelector 替换 invocation.selector 485 | invocation.selector = aliasSelector; 486 | 487 | // Instance 的容器 488 | AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector); 489 | // Class 的容器 490 | AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector); 491 | AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation]; 492 | NSArray *aspectsToRemove = nil; 493 | 494 | // Before hooks. 495 | aspect_invoke(classContainer.beforeAspects, info); 496 | aspect_invoke(objectContainer.beforeAspects, info); 497 | 498 | // Instead hooks. 499 | BOOL respondsToAlias = YES; 500 | if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) { 501 | // 如果有任何 insteadAspects 就直接替换了 502 | aspect_invoke(classContainer.insteadAspects, info); 503 | aspect_invoke(objectContainer.insteadAspects, info); 504 | }else { // 否则正常执行 505 | // 遍历 invocation.target 及其 superClass 找到实例可以响应 aliasSelector 的点 invoke 506 | Class klass = object_getClass(invocation.target); 507 | do { 508 | if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) { 509 | [invocation invoke]; 510 | break; 511 | } 512 | }while (!respondsToAlias && (klass = class_getSuperclass(klass))); 513 | } 514 | 515 | // After hooks. 516 | aspect_invoke(classContainer.afterAspects, info); 517 | aspect_invoke(objectContainer.afterAspects, info); 518 | 519 | // 如果没有 hook,则执行原始实现(通常会抛出异常) 520 | if (!respondsToAlias) { 521 | invocation.selector = originalSelector; 522 | SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName); 523 | // 如果可以响应 originalForwardInvocationSEL,表示之前是 replace method 而非 add method 524 | if ([self respondsToSelector:originalForwardInvocationSEL]) { 525 | ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation); 526 | }else { 527 | [self doesNotRecognizeSelector:invocation.selector]; 528 | } 529 | } 530 | 531 | // 移除 aspectsToRemove 队列中的 AspectIdentifier,执行 remove 532 | [aspectsToRemove makeObjectsPerformSelector:@selector(remove)]; 533 | } 534 | #undef aspect_invoke 535 | ``` 536 | 537 | > Note: `aspect_invoke` 宏定义的作用域。 538 | 539 | - 代码实现对应了 Hook 的 AspectOptions 参数的 Before,Instead 和 After。 540 | - `aspect_invoke` 中 `aspectsToRemove` 是一个 NSArray,里面容纳着需要被销户的 Hook,即 AspectIdentifier(之后会调用 `remove` 移除)。 541 | - 遍历 invocation.target 及其 superClass 找到实例可以响应 aliasSelector 的点 invoke 实现代码。 542 | 543 | ### Block Hook 544 | 545 | Aspects 让我们在指定 Class 或 Instance 的特定 Selector 执行时,根据 AspectOptions 插入我们自己的 Block 做 Hook,而这个 Block 内部有我们想要的有关于当前 Target 和 Selector 的信息,我们来看一下 Aspects 是怎么办到的: 546 | 547 | ``` obj-c 548 | - (BOOL)invokeWithInfo:(id)info { 549 | NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature]; 550 | NSInvocation *originalInvocation = info.originalInvocation; 551 | NSUInteger numberOfArguments = self.blockSignature.numberOfArguments; 552 | 553 | // 偏执。我们已经在 hook 注册的时候检查过了,(不过这里我们还要检查)。 554 | if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) { 555 | AspectLogError(@"Block has too many arguments. Not calling %@", info); 556 | return NO; 557 | } 558 | 559 | // block 的 `self` 将会是 AspectInfo。可选的。 560 | if (numberOfArguments > 1) { 561 | [blockInvocation setArgument:&info atIndex:1]; 562 | } 563 | 564 | // 简历参数分配内存 argBuf 然后从 originalInvocation 取 argument 赋值给 blockInvocation 565 | void *argBuf = NULL; 566 | for (NSUInteger idx = 2; idx < numberOfArguments; idx++) { 567 | const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx]; 568 | NSUInteger argSize; 569 | NSGetSizeAndAlignment(type, &argSize, NULL); 570 | 571 | // reallocf 优点,如果创建内存失败会自动释放之前的内存,讲究 572 | if (!(argBuf = reallocf(argBuf, argSize))) { 573 | AspectLogError(@"Failed to allocate memory for block invocation."); 574 | return NO; 575 | } 576 | 577 | [originalInvocation getArgument:argBuf atIndex:idx]; 578 | [blockInvocation setArgument:argBuf atIndex:idx]; 579 | } 580 | 581 | // 执行 582 | [blockInvocation invokeWithTarget:self.block]; 583 | 584 | // 释放 argBuf 585 | if (argBuf != NULL) { 586 | free(argBuf); 587 | } 588 | return YES; 589 | } 590 | ``` 591 | 592 | 考虑两个问题: 593 | 594 | - `[blockInvocation setArgument:&info atIndex:1];` 为什么要在索引 1 处插入呢? 595 | - `for (NSUInteger idx = 2; idx < numberOfArguments; idx++)` 为什么要从索引 2 开始遍历参数呢? 596 | 597 | 嘛~ 如果你对 Block 的 Runtime 结构以及执行过程下断点研究一下就全都明白了,感兴趣的同学有疑问可以联系我(与真正勤奋好学的人交流又有谁会不乐意呢?笑~) 598 | 599 | ## 优秀 AOP 库应该具备的特质 600 | 601 | ![](aspects/aspects_rank.jpg) 602 | 603 | - 良好的使用体验 604 | - 可控粒度小 605 | - 使用 Block 做 Hook 606 | - 支持撤销 Hook 607 | - 安全性 608 | 609 | ### 良好的使用体验 610 | 611 | Aspects 使用 NSObject + Categroy 的方式提供接口,非常巧妙的涵盖了 Instance 和 Class。 612 | 613 | Aspects 提供的接口保持高度一致(本着**易用,简单,方便**的原则设计接口和整个框架的实现会让你的开源项目更容易被人们接纳和使用): 614 | 615 | ``` obj-c 616 | + (id)aspect_hookSelector:(SEL)selector 617 | withOptions:(AspectOptions)options 618 | usingBlock:(id)block 619 | error:(NSError **)error; 620 | 621 | - (id)aspect_hookSelector:(SEL)selector 622 | withOptions:(AspectOptions)options 623 | usingBlock:(id)block 624 | error:(NSError **)error; 625 | ``` 626 | 627 | > Note: 其实接口这里对于 `block` 的参数自动补全可以更进一步,不过 Aspects 当初是没有办法做到的,单从接口设计这块已经很优秀了。 628 | 629 | ### 可控粒度小 630 | 631 | Aspects 不仅支持大部分 AOP 框架应该做到的对于 Class 的 Hook,还支持粒度更小的 Instance Hook,而其在内部实现中为了支持 Instance Hook 所做的代码也非常值得我们参考和学习(已在上文 **Aspects 核心代码剖析** 处单独分析)。 632 | 633 | 为使用者提供更为自由的 Hook 方式以达到更加精准的控制是每个使用者乐于见到的事。 634 | 635 | ### 使用 Block 做 Hook 636 | 637 | Aspects 使用 Block 来做 Hook 应该考虑到了很多东西,支持使用者通过在 Block 中获取到相关的信息,书写自己额外的操作就可以实现 Hook 需求。 638 | 639 | ### 支持撤销 Hook 640 | 641 | Aspects 还支持撤销之前做的 Hook 以及已混写的 Method,为了实现这个功能 Aspects 设计了全局容器,把 Hook 和混写用全局容器做记录,让一切都可以复原,这不正是我们想要的吗? 642 | 643 | ### 安全性 644 | 645 | 嘛~ 我们在学习 Runtime 的时候,就应该看到过不少文章讲解 Method Swizzling 要注意的安全性问题,由于用到了大量 Runtime 方法,加上 AOP 是面向整个切面的,所以一单发现问题就会比较严重,涉及的面会比较广,而且难以调试。 646 | 647 | > Note: 我们不能因为容易造成问题就可以回避 Method Swizzling,就好比大学老师讲到递归时强调容易引起循环调用,很多人就在内心回避使用递归,甚至于非常适合使用递归来写的算法题(这里指递归来写会易读写、易维护)只会用复杂的方式来思考。 648 | 649 | ## 总结 650 | 651 | - 文章简单介绍了 AOP 的概念,希望能给各位读者对 AOP 思想的理解提供微薄的帮助。 652 | - 文章系统的剖析了 Aspects 开源库的内部结构,希望能让大家在浏览 Aspects 源码时快速定位代码位置,找到核心内容。 653 | - 文章重点分析了 Aspects 的核心代码,提炼了一些笔者认为值得注意的点,但愿可以在大家扒源码时提供一些指引。 654 | - 文章结尾总结了 Aspects 作为一个比较优秀 655 | 656 | 文章写得比较用心(是我个人的原创文章,转载请注明 [https://lision.me/](https://lision.me/)),如果发现错误会优先在我的 [个人博客](https://lision.me/) 中更新。如果有任何问题欢迎在我的微博 [@Lision](https://weibo.com/lisioncode) 联系我~ 657 | 658 | 希望我的文章可以为你带来价值~ -------------------------------------------------------------------------------- /Categroy/iOS/Aspects/aspects/aop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Aspects/aspects/aop.jpg -------------------------------------------------------------------------------- /Categroy/iOS/Aspects/aspects/aspects.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Aspects/aspects/aspects.jpg -------------------------------------------------------------------------------- /Categroy/iOS/Aspects/aspects/aspects_core.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Aspects/aspects/aspects_core.png -------------------------------------------------------------------------------- /Categroy/iOS/Aspects/aspects/aspects_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Aspects/aspects/aspects_logo.jpg -------------------------------------------------------------------------------- /Categroy/iOS/Aspects/aspects/aspects_rank.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Aspects/aspects/aspects_rank.jpg -------------------------------------------------------------------------------- /Categroy/iOS/Aspects/aspects/aspects_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Aspects/aspects/aspects_struct.png -------------------------------------------------------------------------------- /Categroy/iOS/BlocksKit/blocks-kit.md: -------------------------------------------------------------------------------- 1 | # BlocksKit 源码解析 2 | 3 | 头图 4 | 5 | ## 前言 6 | 7 | **Block** 是 Objective-C 可以进行 [Functional programming (FP)](https://en.wikipedia.org/wiki/Functional_programming) 编码的**核心前提**。 8 | 9 | 毫无疑问,**Block** 使得 Objective-C 的编码**更容易、更快捷、更高效**,尤其在多线程和 [Grand Central Dispatch (GCD)](https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html) 的书写时更为明显。 10 | 11 | [BlocksKit](https://github.com/BlocksKit/BlocksKit) 希望通过消除一些恼人的问题(并且移除 Block 在某些情况下编码的限制)来推进使用 Block 进行编程。 12 | 13 | BlocksKit 原本是由 [Zachary Waldowski](https://github.com/zwaldowski) 和 [Alexsander Akers](https://github.com/a2) 创建,并由前者维护,在 GitHub 早早拿下 6.5k Stars 的骄人成绩。 14 | 15 | 遗憾的是,在 Issues List 中可以找到 [issue 332](https://github.com/BlocksKit/BlocksKit/issues/332),其中 Zachary Waldowski 已经明确表示自己没有时间继续维护 BlocksKit 了,这也是 BlocksKit 代码版本止步于 v2.2.5 的原因... 16 | 17 | 不过瑕不掩瑜,用过 BlocksKit 的同学相信都有 Get 到使用 BlocksKit 的那种令人愉悦的编码体验~ 18 | 19 | BlocksKit 的内部实现有很多值得我们学习借鉴之处,本文将会针对其中笔者认为有价值的源码实现进行分析解读。 20 | 21 | ## 索引 22 | 23 | - BlocksKit 简介 24 | - BlocksKit - Core 25 | - BlocksKit - UIKit 26 | - BlocksKit - DynamicDelegate 27 | - 总结 28 | 29 | ## BlocksKit 简介 30 | 31 | 图片 BlocksKit 32 | 33 | BlocksKit 旨在帮助我们去掉 Block 在某些使用场景的局限性和体验不好的地方,提供令人愉悦的使用 Block 的编码体验。 34 | 35 | 从 BlocksKit 的名字上我们不难看出,其并不只是实现某一功能的**组件**,而是一组主题一致的**套件**。其内部源文件相对较多且杂,其中还包含很多 Categroy ,所以在对 BlocksKit 的划分上我大致遵从其内部的文件结构进行划分,弃掉一些不常用的部分大致划分为三大块: 36 | 37 | - Core 38 | - UIKit 39 | - DynamicDelegate 40 | 41 | 其中 Core 内部是我们在使用 BlocksKit 日常编码时**经常会用到的功能**,UIKit 是一些日常开发工作中高频使用的 **UI 组件的 Categroy 的实现**,而 DynamicDelegate 则负责以**优雅**的方式实现用 Block 替代 CocoaTouch 框架中的常用协议(DataSource,Delegate)代理的实现。 42 | 43 | 图片 BlocksKit 结构划分 44 | 45 | ## BlocksKit - Core 46 | 47 | BlocksKit - Core 中存放了我们在使用 BlocksKit 日常编码时经常用到的功能实现: 48 | 49 | - 对于**集合体的遍历、过滤、映射等高频便捷功能**的支持 50 | - **AssociatedObject** 便捷书写的支持 51 | - 任意 **NSObject** 执行 Block 52 | - **KVO** 便捷书写的支持 53 | 54 | ### 集合遍历相关便捷功能 55 | 56 | 跟很多支持 FP 编程的其他语言一样,提供了一些常用的遍历相关的 Block 快捷 API: 57 | 58 | - `bk_each:` 简单遍历 59 | - `bk_apply:` 并发遍历 60 | - `bk_match:` 找到第一个符合条件的对象并返回 61 | - `bk_select:` 找到所有符合条件的对象,以数组形式返回 62 | - `bk_reject:` 与 `bk_select:` 相对应,找到所有不符合条件的对象,以数组形式返回 63 | - `bk_map:` 对该数组中的每个对象执行操作后返回一个新的值,以数组形式返回 64 | - `bk_reduce:withBlock:` 初始化一个值,根据 Block 遍历原集合体得到最终值 65 | - `bk_reduceInteger:withBlock:` 同上,适用于 NSInteger 66 | - `bk_reduceFloat:withBlock:` 同上,适用于 CGFloat 67 | - `bk_any:` 是否有条件匹配的对象 68 | - `bk_none:` 是否没有条件匹配的对象 69 | - `bk_all:` 是否全部满足给定条件 70 | - `bk_corresponds:withBlock:` 判断入参数组 list 与该数组是否符合某种联系 block 71 | 72 | 嘛~ 有没有很眼熟?其实这些集合遍历的便捷方法在 Ruby、Haskell 等语言中也都有对应方法存在。集合遍历相关大致就提供上面列出的 API 了,其内部基本上都是通过 `enumerateObjectsUsingBlock:` 来实现的。 73 | 74 | > Note: 想要了解更多关于 `enumerateObjectsUsingBlock:` 与 `for` 以及 `for in` 的区别请[点击这里](https://stackoverflow.com/questions/4486622/when-to-use-enumerateobjectsusingblock-vs-for)。 75 | 76 | ### AssociatedObject 便捷化 77 | 78 | AssociatedObject 作为一项 Objective-C 2.0 Runtime 中加入的新特性,在 OS X Snow Leopard (iOS 4) 之后可用。它的出现允许我们在 Categroy 中加入自定义属性到现有 Class,主要提供了三个方法操作关联对象: 79 | 80 | - objc_setAssociatedObject 81 | - objc_getAssociatedObject 82 | - objc_removeAssociatedObjects 83 | 84 | BlocksKit 也是使用上述方法实现该功能的,值得一提的是 `objc_AssociationPolicy` 枚举中仅有以下枚举值: 85 | 86 | ``` objc 87 | typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { 88 | OBJC_ASSOCIATION_ASSIGN = 0, 89 | OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 90 | OBJC_ASSOCIATION_COPY_NONATOMIC = 3, 91 | OBJC_ASSOCIATION_RETAIN = 01401, 92 | OBJC_ASSOCIATION_COPY = 01403 93 | }; 94 | ``` 95 | 96 | **对于 `weak` 而言,并没有对应的枚举值,BlocksKit 中自己实现了 `weak` 策略的关联。**在 BlocksKit 内部定义了一个 Class: 97 | 98 | ``` objc 99 | @interface _BKWeakAssociatedObject : NSObject 100 | @property (nonatomic, weak) id value; 101 | @end 102 | 103 | @implementation _BKWeakAssociatedObject 104 | @end 105 | ``` 106 | 107 | 然后用 `OBJC_ASSOCIATION_RETAIN_NONATOMIC` 关联策略关联 `_BKWeakAssociatedObject` 对象到相应的 `key`,实际操作 `_BKWeakAssociatedObject` 内部 `weak` 引用的 `value` 来实现 `weak` AssociatedObject。 108 | 109 | ``` objc 110 | + (void)bk_weaklyAssociateValue:(__autoreleasing id)value withKey:(const void *)key { 111 | _BKWeakAssociatedObject *assoc = objc_getAssociatedObject(self, key); 112 | if (!assoc) { 113 | assoc = [_BKWeakAssociatedObject new]; 114 | [self bk_associateValue:assoc withKey:key]; 115 | } 116 | assoc.value = value; 117 | } 118 | 119 | + (id)bk_associatedValueForKey:(const void *)key { 120 | id value = objc_getAssociatedObject(self, key); 121 | if (value && [value isKindOfClass:[_BKWeakAssociatedObject class]]) { 122 | return [(_BKWeakAssociatedObject *)value value]; 123 | } 124 | return value; 125 | } 126 | ``` 127 | 128 | > Note: `__autoreleasing` 表示通过引用(id *)传递的参数,并且在返回时自动释放。 129 | 130 | ### 任意 NSObject 执行 Block 131 | 132 | BlocksKit 的 NSObject+BKBlockExecution 分类中使用 Block 的方式对 `performSelector:` 方法做了修改。它不仅仅在 `perform` 时执行十分迅速,线程安全,使用 GCD 实现异步,并且**每个便捷方法还会返回一个指针,用于在 Block 执行之前取消操作**。 133 | 134 | NSObject+BKBlockExecution 内部的便捷方法均依赖 `bk_performBlock:onQueue:afterDelay:` 实现,不论是实例方法还是类方法的实现代码都是一套逻辑: 135 | 136 | ``` objc 137 | NSParameterAssert(block != nil); 138 | 139 | __block BOOL cancelled = NO; 140 | 141 | void (^wrapper)(BOOL) = ^(BOOL cancel) { 142 | if (cancel) { 143 | cancelled = YES; 144 | return; 145 | } 146 | if (!cancelled) block(); 147 | }; 148 | 149 | dispatch_after(BKTimeDelay(delay), queue, ^{ wrapper(NO); }); 150 | 151 | return [wrapper copy]; 152 | ``` 153 | 154 | 唯一值得一提的就是 `cancelled` 变量。 155 | 156 | ``` objc 157 | + (void)bk_cancelBlock:(id)block { 158 | NSParameterAssert(block != nil); 159 | void (^wrapper)(BOOL) = block; 160 | wrapper(YES); 161 | } 162 | ``` 163 | 164 | 在 `bk_cancelBlock:` 函数中调用 `wrapper(YES);` 将 `cancelled` 标记为 `YES` 这样 `wrapper` 在执行时会直接 `return` 实现了对未执行 Block 的可取消。 165 | 166 | ### KVO 便捷化 167 | 168 | 如果你想要了解 KVO 的底层是如何实现的,那么恐怕要让你失望了,笔者这里只想简单的介绍一下 BlocksKit 是如何提高我们在开发时编写 KVO 编码效率的。 169 | 170 | #### `_BKObserver` 171 | 172 | 既然是 KVO 便捷化,那么肯定还是基于 KVO 实现的,传统 KVO 实现过程中要实现的那些令人讨厌的方法和固定写法肯定还是绕不开,那么怎么解决这个问题对外仅提供一个简单便捷的 API 呢? 173 | 174 | BlocksKit 定义了一个 `_BKObserver` 类作为对 KVO 观察者的具体实现。 175 | 176 | ``` objc 177 | @interface _BKObserver : NSObject { 178 | BOOL _isObserving; 179 | } 180 | 181 | @property (nonatomic, readonly, unsafe_unretained) id observee; 182 | @property (nonatomic, readonly) NSMutableArray *keyPaths; 183 | @property (nonatomic, readonly) id task; 184 | @property (nonatomic, readonly) BKObserverContext context; 185 | 186 | - (id)initWithObservee:(id)observee keyPaths:(NSArray *)keyPaths context:(BKObserverContext)context task:(id)task; 187 | 188 | @end 189 | ``` 190 | 191 | > Node: `_BKObserver` 内部 `unsafe_unretained` 引用实际的 `observee`。 192 | 193 | 这里面的 `BKObserverContext` 实际上是一个枚举: 194 | 195 | ``` objc 196 | typedef NS_ENUM(int, BKObserverContext) { 197 | BKObserverContextKey, 198 | BKObserverContextKeyWithChange, 199 | BKObserverContextManyKeys, 200 | BKObserverContextManyKeysWithChange 201 | }; 202 | ``` 203 | 204 | `BKObserverContext` 枚举主要起到区分对应 `task` 的 Block 类型的作用,其受 NSKeyValueObservingOptions 的影响。 205 | 206 | ``` objc 207 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 208 | if (context != BKBlockObservationContext) return; 209 | 210 | @synchronized(self) { 211 | switch (self.context) { 212 | case BKObserverContextKey: { 213 | void (^task)(id) = self.task; 214 | task(object); 215 | break; 216 | } 217 | case BKObserverContextKeyWithChange: { 218 | void (^task)(id, NSDictionary *) = self.task; 219 | task(object, change); 220 | break; 221 | } 222 | case BKObserverContextManyKeys: { 223 | void (^task)(id, NSString *) = self.task; 224 | task(object, keyPath); 225 | break; 226 | } 227 | case BKObserverContextManyKeysWithChange: { 228 | void (^task)(id, NSString *, NSDictionary *) = self.task; 229 | task(object, keyPath, change); 230 | break; 231 | } 232 | } 233 | } 234 | } 235 | ``` 236 | 237 | `_BKObserver` 实现了对于 `observee` 的观察同样绕不开 KVO 的系统方法: 238 | 239 | ``` objc 240 | - (void)startObservingWithOptions:(NSKeyValueObservingOptions)options { 241 | @synchronized(self) { 242 | if (_isObserving) return; 243 | 244 | [self.keyPaths bk_each:^(NSString *keyPath) { 245 | [self.observee addObserver:self forKeyPath:keyPath options:options context:BKBlockObservationContext]; 246 | }]; 247 | 248 | _isObserving = YES; 249 | } 250 | } 251 | ``` 252 | 253 | > Note: `@synchronized(self)` 确保 `_BKObserver` 对象的线程安全,避免了多次 `addObserver` 同一个 `keyPath` 的情况。 254 | 255 | 对应的还有 `removeObserver:forKeyPath:context:` 方法,这里就不赘述了,不过别忘了在 `_BKObserver` 的 `dealloc` 方法中要对所有的 `keyPath` 注销: 256 | 257 | ``` objc 258 | - (void)dealloc { 259 | if (self.keyPaths) { 260 | [self _stopObservingLocked]; 261 | } 262 | } 263 | ``` 264 | 265 | #### NSObject (BlockObservation) 266 | 267 | 既然是 KVO 的便捷化实现,那么对于 NSObject 做 Categroy 自然在情理之中,BlockObservation 分类中提供了 KVO 的便捷使用 API,其内部最终都会调用 `bk_addObserverForKeyPaths:identifier:options:context:task:` 方法,只不过是参数上有所差异罢了。 268 | 269 | ``` objc 270 | - (void)bk_addObserverForKeyPaths:(NSArray *)keyPaths identifier:(NSString *)identifier options:(NSKeyValueObservingOptions)options context:(BKObserverContext)context task:(id)task { 271 | // 断言参数 272 | NSParameterAssert(keyPaths.count); 273 | NSParameterAssert(identifier.length); 274 | NSParameterAssert(task); 275 | 276 | Class classToSwizzle = self.class; 277 | // 单例 NSMutableSet,做全局被替换过 dealloc 类的缓存 278 | NSMutableSet *classes = self.class.bk_observedClassesHash; 279 | @synchronized (classes) { 280 | NSString *className = NSStringFromClass(classToSwizzle); 281 | // 如果单例 NSMutableSet 中没有当前类 Class 282 | if (![classes containsObject:className]) { 283 | // 拿到 deallocSelector 284 | SEL deallocSelector = sel_registerName("dealloc"); 285 | 286 | __block void (*originalDealloc)(__unsafe_unretained id, SEL) = NULL; 287 | 288 | // 这里仅仅引用一下 objSelf,不需要考虑自动置为 nil 的情况 289 | // 使用 __unsafe_unretained 再合适不过了 290 | id newDealloc = ^(__unsafe_unretained id objSelf) { 291 | // 在 dealloc 中添加删除所有 Block 观察者操作 292 | [objSelf bk_removeAllBlockObservers]; 293 | 294 | // 判断此类是否注册过 dealloc 方法 295 | if (originalDealloc == NULL) { // 没有注册过 dealloc 方法 296 | // 用 Block 的入参 objSelf 初始化 objc_super 结构体 297 | struct objc_super superInfo = { 298 | .receiver = objSelf, 299 | .super_class = class_getSuperclass(classToSwizzle) 300 | }; 301 | 302 | // 将 objc_msgSendSuper 强转 msgSend 303 | void (*msgSend)(struct objc_super *, SEL) = (__typeof__(msgSend))objc_msgSendSuper; 304 | // 这样 msgSend 就可以接受 objc_super 作为第一个入参咯 305 | // 这样就达到了对当前类父类发送 dealloc 方法的效果 306 | msgSend(&superInfo, deallocSelector); 307 | } else { // 已经注册过 dealloc 方法,执行就是了 308 | originalDealloc(objSelf, deallocSelector); 309 | } 310 | }; 311 | 312 | // 创建 newDealloc Block 对应的函数指针 IMP 313 | IMP newDeallocIMP = imp_implementationWithBlock(newDealloc); 314 | 315 | // 尝试使用 class_addMethod 为当前类添加 dealloc 方法 316 | // 如果此类已经有 dealloc 方法的实现,class_addMethod 不会覆盖 317 | if (!class_addMethod(classToSwizzle, deallocSelector, newDeallocIMP, "v@:")) { 318 | // 添加失败,则获取当前类的 dealloc 方法 319 | Method deallocMethod = class_getInstanceMethod(classToSwizzle, deallocSelector); 320 | 321 | // 拿到当前方法 dealloc 的 IMP 322 | // 在设置新的实现之前,我们需要存储原始实现,以防在设置时调用方法 323 | originalDealloc = (void(*)(__unsafe_unretained id, SEL))method_getImplementation(deallocMethod); 324 | 325 | // 用新的 dealloc IMP 绑定 dealloc 方法 326 | // 我们需要再次存储原始实现,以防它刚刚更改 327 | originalDealloc = (void(*)(__unsafe_unretained id, SEL))method_setImplementation(deallocMethod, newDeallocIMP); 328 | } 329 | 330 | // 完成方法替换后,将当前类加入全局替换类缓存 331 | [classes addObject:className]; 332 | } 333 | } 334 | 335 | // 通过 self 和入参构建 _BKObserver 336 | // 通过 _BKObserver 实现具体的观察 337 | NSMutableDictionary *dict; 338 | _BKObserver *observer = [[_BKObserver alloc] initWithObservee:self keyPaths:keyPaths context:context task:task]; 339 | [observer startObservingWithOptions:options]; 340 | 341 | @synchronized (self) { 342 | // 这个 dict 也是通过 runtime 关联的 343 | dict = [self bk_observerBlocks]; 344 | 345 | // lazyload 346 | if (dict == nil) { 347 | dict = [NSMutableDictionary dictionary]; 348 | [self bk_setObserverBlocks:dict]; 349 | } 350 | } 351 | 352 | // 将 _BKObserver 对象实例 retain 到 dict 当中 353 | dict[identifier] = observer; 354 | } 355 | ``` 356 | 357 | Emmmmm...代码比较长,所以加了详细的注释,不过没有什么特别难懂的点,简单来说,就是 Hook 了 `dealloc` 方法,加入了注销 KVO 观察者的操作而已~ 358 | 359 | 至于存储 `_BKObserver` 的字典其实是通过 AssociatedObject 关联的: 360 | 361 | ``` objc 362 | - (void)bk_setObserverBlocks:(NSMutableDictionary *)dict { 363 | [self bk_associateValue:dict withKey:BKObserverBlocksKey]; 364 | } 365 | 366 | - (NSMutableDictionary *)bk_observerBlocks { 367 | return [self bk_associatedValueForKey:BKObserverBlocksKey]; 368 | } 369 | ``` 370 | 371 | -------------------------------------------------------------------------------- /Categroy/iOS/Tips/oc-class-properties.md: -------------------------------------------------------------------------------- 1 | # 巧用 LLVM 特性: Objective-C Class Properties 解耦 2 | 3 | ![](oc-class-properties/oc-class-properties.jpg) 4 | 5 | ## 前言 6 | 7 | Emmmmm... Objective-C Class Properties 早在 WWDC 2016 中就已经公示,给 Objective-C 加入这个特性主要是为了与 Swift 类型属性相互操作。 8 | 9 | 官方是这么说明的: 10 | 11 | > Interoperate with Swift type properties. 12 | 13 | 嘛~ 虽然是为了配合 Swift 加入的新特性,不过聊胜于无哈! 14 | 15 | > Note: 值得一提的是 Objective-C Class Properties 语法特性虽然是 WWDC 2016 加入的,不过由于是 Xcode 8 中 LLVM Compiler 的特性,因此也适用于 iOS 10 之前的部署版本哟~ 16 | 17 | ## 索引 18 | 19 | - LLVM 20 | - Objective-C Class Properties 21 | - 解耦 22 | - 总结 23 | 24 | ## LLVM 25 | 26 | ![](oc-class-properties/llvm.jpg) 27 | 28 | [LLVM 官网](https://llvm.org/) 对于 LLVM 的定义: 29 | 30 | > Note: The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. 31 | 32 | Emmmmm... 有趣的是,有的文章把 LLVM 强行展开为 "low level virtual machine" 译为 “低级别虚拟机”,不过在 [LLVM 官网](https://llvm.org/) 可以看到官方明示 LLVM 与传统的虚拟机**没有一毛钱关系**,名称 "LLVM" 本身**不是缩写**,它仅仅是项目的名称而已~ 33 | 34 | 嘛~ 可能有的同学不能理解为何 LLVM 是一个编译器工具链集合?这就要从 Apple 的编译器历史讲起咯~ 35 | 36 | 很久很久以前... 算了,我感觉要跑题了(囧),这里简单列一下 Apple 采用过的编译方案吧: 37 | 38 | - GCC 39 | - LLVM & GCC 40 | - LLVM Compiler 41 | 42 | ### GCC 43 | 44 | [GCC, the GNU Compiler Collection](https://gcc.gnu.org/) 是一套由 GNU 开发的编程语言编译器,最初作为 [GNU 操作系统](http://www.gnu.org/gnu/thegnuproject.html) 的编译器使用,后面发展成为类 Unix 操作系统以及 Apple Mac OS X 操作系统的标准编译器。 45 | 46 | 原本 GCC 仅能处理 C 语言的编译,不过 GCC 很快扩展以支持 C++,之后的 GCC 越发全面,支持 Objective-C,Fortran,Ada,以及 Go 语言。 47 | 48 | 值得一提的是 GCC 是一套以 GPL 以及 LGPL 许可证锁发行的 100% 自由软件,这意味着**用户可以自由地运行,拷贝,分发,学习,修改并改进该软件**。 49 | 50 | ### LLVM & GCC 51 | 52 | LLVM 我们前面介绍过了,是模块化 & 可重用性编译器以及工具链技术集合。 53 | 54 | LLVM 能够进行程序语言的 **编译期优化、链接优化、在线编译优化、代码生成**。 55 | 56 | ### LLVM Compiler 57 | 58 | 前面介绍过 GCC 支持很多语言,系统架构庞大而笨重,而 Apple 大量使用的 Objective-C 在 GCC 中顺位(优先级)较低。此外,GCC 作为一个纯粹的编译系统,在与 IDE 配合方面的表现也很差。 59 | 60 | So,Apple 决定从零开始写 C,C++,Objective-C 的编译器 Clang。 61 | 62 | 至此,Apple 彻底与 GCC 了断。 63 | 64 | ## Objective-C Class Properties 65 | 66 | ![](oc-class-properties/oc-feature.jpg) 67 | 68 | Objective-C Class Properties 作为 Objective-C 新语法特性在 [WWDC2016 What's New in LLVM](https://developer.apple.com/videos/play/wwdc2016/405/) 中公示,表示 Xcode 8 之后可以使用这一新语法特性。 69 | 70 | 使用方式很简单: 71 | 72 | - Declared with `class` flag 73 | - Accessed with dot syntax 74 | - Never synthesized 75 | - Use `@dynamic` to defer to runtime 76 | 77 | ### Declared with `class` flag 78 | 79 | ``` objc 80 | @interface MyType : NSObject 81 | @property (class) NSString *someString; 82 | @end 83 | ``` 84 | 85 | ### Accessed with dot syntax 86 | 87 | ``` objc 88 | NSLog(@"format string: %@", MyType.someString); 89 | ``` 90 | 91 | ### Never synthesized 92 | 93 | ``` objc 94 | @implementation MyType 95 | static NSString *_someString = nil; 96 | + (NSString *)someString { return _someString; } 97 | + (void)setSomeString:(NSString *)newString { _someString = newString; } 98 | @end 99 | ``` 100 | 101 | ### Use `@dynamic` to defer to runtime 102 | 103 | ``` objc 104 | @implementation MyType 105 | @dynamic (class) someString; 106 | + (BOOL)resolveClassMethod:(SEL) name { 107 | ... 108 | } 109 | @end 110 | ``` 111 | 112 | ## 解耦 113 | 114 | 笔者在做项目组件下沉时,遇到一个问题,正好适用于 Objective-C Class Properties 发挥:将要下沉的组件库中某系统类 Categroy 引用了业务层某方法。 115 | 116 | ![](oc-class-properties/before-decoupling.png) 117 | 118 | 业务层应该依赖于将要下沉的组件,而组件既然要下沉就不应该再反过来依赖上层业务实现! 119 | 120 | 按照常规思路,想要把上层业务中被依赖的部分一起随组件下沉,但是发现被依赖的部分虽然也属于一个较为基础的模块,不过此模块现阶段不做下沉... 121 | 122 | 后来经过组内大佬指点,使用 Objective-C Class Properties 解决了这个问题,即将上层业务被依赖的部分化作将要下沉组件依赖方系统类 Categroy 的 Class Properties。 123 | 124 | > Note: 在 Categroy 中写 Objective-C Class Properties 需要使用 Runtime 关联方法。 125 | 126 | ![](oc-class-properties/after-decoupling.png) 127 | 128 | ## 总结 129 | 130 | - 介绍了 LLVM 顺便提到了 Apple 的编译系统发展简史。 131 | - 使用官方 Demo 简单介绍了 Objective-C Class Properties 语法特性的书写方式。 132 | - 提供了一种巧妙使用 Objective-C Class Properties 解耦的思路。 133 | 134 | 文章写得比较用心(是我个人的原创文章,转载请注明 [https://lision.me/](https://lision.me/)),如果发现错误会优先在我的 [个人博客](https://lision.me/) 中更新。如果有任何问题欢迎在我的微博 [@Lision](https://weibo.com/lisioncode) 联系我~ 135 | 136 | 希望我的文章可以为你带来价值~ -------------------------------------------------------------------------------- /Categroy/iOS/Tips/oc-class-properties/after-decoupling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Tips/oc-class-properties/after-decoupling.png -------------------------------------------------------------------------------- /Categroy/iOS/Tips/oc-class-properties/before-decoupling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Tips/oc-class-properties/before-decoupling.png -------------------------------------------------------------------------------- /Categroy/iOS/Tips/oc-class-properties/llvm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Tips/oc-class-properties/llvm.jpg -------------------------------------------------------------------------------- /Categroy/iOS/Tips/oc-class-properties/oc-class-properties.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Tips/oc-class-properties/oc-class-properties.jpg -------------------------------------------------------------------------------- /Categroy/iOS/Tips/oc-class-properties/oc-feature.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/Tips/oc-class-properties/oc-feature.jpg -------------------------------------------------------------------------------- /Categroy/iOS/WWDC/ios-rendering-process.md: -------------------------------------------------------------------------------- 1 | # 深入理解 iOS Rendering Process 2 | 3 | ![](ios-rendering-process/rendering.jpg) 4 | 5 | ## 前言 6 | 7 | iOS 最早名为 iPhone OS,是 [Apple](https://www.apple.com/) 公司专门为其硬件设备开发的操作系统,最初于 2007 年随第一代 iPhone 推出,后扩展为支持 Apple 公司旗下的其他硬件设备,如 iPod、iPad 等。 8 | 9 | 作为一名 iOS Developer,相信大多数人都有写出过造成 iOS 设备卡顿的代码经历,相应的也有过想方设法优化卡顿代码的经验。 10 | 11 | 本文将从 OpenGL 的角度结合 Apple 官方给出的部分资料,介绍 iOS Rendering Process 的概念及其整个底层渲染管道的各个流程。 12 | 13 | 相信在理解了 iOS Rendering Process 的底层各个阶段之后,我们可以在平日的开发工作之中写出性能更高的代码,在解决帧率不足的显示卡顿问题时也可以多一些思路~ 14 | 15 | ## 索引 16 | 17 | - iOS Rendering Process 概念 18 | - iOS Rendering 技术框架 19 | - OpenGL 主要渲染步骤 20 | - OpenGL Render Pipeline 21 | - Core Animation Pipeline 22 | - Commit Transaction 23 | - Animation 24 | - 全文总结 25 | - 扩展阅读 26 | 27 | ## iOS Rendering Process 概念 28 | 29 | iOS Rendering Process 译为 iOS 渲染流程,本文特指 iOS 设备从设置将要显示的图元数据到最终在设备屏幕成像的整个过程。 30 | 31 | 在开始剖析 iOS Rendering Process 之前,我们需要对 iOS 的渲染概念有一个基本的认知: 32 | 33 | ### 基于平铺的渲染 34 | 35 | iOS 设备的屏幕分为 N * N 像素的图块,每个图块都适合于 [SoC](https://en.wikipedia.org/wiki/System_on_a_chip) 缓存,几何体在图块内被大量拆分,只有在所有几何体全部提交之后才可以进行光栅化(Rasterization)。 36 | 37 | ![](ios-rendering-process/tile_based_rendering.jpg) 38 | 39 | > Note: 这里的光栅化指将屏幕上面被大量拆分出来的几何体渲染为像素点的过程。 40 | 41 | ![](ios-rendering-process/rasterization.jpg) 42 | 43 | ## iOS Rendering 技术框架 44 | 45 | 事实上 iOS 渲染相关的层级划分大概如下: 46 | 47 | ![](ios-rendering-process/ios_rendering_framework.png) 48 | 49 | ### UIKit 50 | 51 | 嘛~ 作为一名 iOS Developer 来说,应该对 UIKit 都不陌生,我们日常开发中使用的用户交互组件都来自于 UIKit Framework,我们通过设置 UIKit 组件的 Layout 以及 BackgroundColor 等属性来完成日常的界面绘画工作。 52 | 53 | 其实 UIKit Framework 自身并不具备在屏幕成像的能力,它主要负责对用户操作事件的响应,事件响应的传递大体是经过逐层的**视图树**遍历实现的。 54 | 55 | > 那么我们日常写的 UIKit 组件为什么可以呈现在 iOS 设备的屏幕上呢? 56 | 57 | ### Core Animation 58 | 59 | Core Animation 其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做 **Layer Kit** 这么一个不怎么和动画有关的名字演变而来的,所以做动画仅仅是 Core Animation 特性的冰山一角。 60 | 61 | Core Animation 本质上可以理解为是一个复合引擎,旨在尽可能快的组合屏幕上不同的显示内容。这些显示内容被分解成独立的图层,即 CALayer,CALayer 才是你所能在屏幕上看见的一切的基础。 62 | 63 | 其实很多同学都应该知道 CALayer,UIKit 中需要在屏幕呈现的组件内部都有一个对应的 CALayer,也就是所谓的 Backing Layer。正是因为一一对应,所以 CALayer 也是树形结构的,我们称之为**图层树**。 64 | 65 | 视图的职责就是**创建并管理**这个图层,以确保当子视图在层级关系中**添加或者被移除**的时候,**他们关联的图层**也**同样对应在层级关系树当中有相同的操作**。 66 | 67 | > 但是为什么 iOS 要基于 UIView 和 CALayer 提供两个平行的层级关系呢?为什么不用一个简单的层级关系来处理所有事情呢? 68 | 69 | 原因在于要做**职责分离**,这样也能**避免很多重复代码**。在 iOS 和 Mac OS X 两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘的交互有着本质的区别,这就是为什么 iOS 有 UIKit 和 UIView,而 Mac OS X 有 AppKit 和 NSView 的原因。他们功能上很相似,但是在实现上有着显著的区别。 70 | 71 | > Note: 实际上,这里并不是两个层级关系,而是**四个**,每一个都扮演不同的角色,除了**视图树**和**图层树**之外,还存在**呈现树**和**渲染树**。 72 | 73 | ### OpenGL ES & Core Graphics 74 | 75 | #### OpenGL ES 76 | 77 | [OpenGL ES](https://en.wikipedia.org/wiki/OpenGL_ES) 简称 GLES,即 OpenGL for Embedded Systems,是 OpenGL 的子集,通常面向**图形硬件加速处理单元(GPU)**渲染 2D 和 3D 计算机图形,例如视频游戏使用的计算机图形。 78 | 79 | OpenGL ES 专为智能手机,平板电脑,视频游戏机和 PDA 等嵌入式系统而设计 。OpenGL ES 是“历史上应用最广泛的 3D 图形 API”。 80 | 81 | #### Core Graphics 82 | 83 | [Core Graphics](https://developer.apple.com/documentation/coregraphics?language=objc) Framework 基于 Quartz 高级绘图引擎。它提供了具有无与伦比的输出保真度的低级别轻量级 2D 渲染。您可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像创建和图像遮罩以及 PDF 文档创建,显示和分析。 84 | 85 | > Note: 在 Mac OS X 中,Core Graphics 还包括用于处理显示硬件,低级用户输入事件和窗口系统的服务。 86 | 87 | ### Graphics Hardware 88 | 89 | [Graphics Hardware](https://en.wikipedia.org/wiki/Graphics_hardware) 译为图形硬件,iOS 设备中也有自己的图形硬件设备,也就是我们经常提及的 GPU。 90 | 91 | [图形处理单元(GPU)]()是一种专用电子电路,旨在快速操作和改变存储器,以加速在用于输出到显示设备的帧缓冲器中创建图像。GPU 被用于嵌入式系统,手机,个人电脑,工作站和游戏控制台。现代 GPU 在处理计算机图形和图像方面非常高效,并且 GPU 的高度并行结构使其在**大块数据并行处理的算法**中比通用 CPU 更有效。 92 | 93 | ## OpenGL 主要渲染步骤 94 | 95 | [OpenGL](https://en.wikipedia.org/wiki/OpenGL) 全称 Open Graphics Library,译为开放图形库,是用于渲染 2D 和 3D 矢量图形的**跨语言,跨平台**的**应用程序编程接口(API)**。OpenGL 可以直接访问 GPU,以实现硬件加速渲染。 96 | 97 | 一个用来渲染图像的 OpenGL 程序主要可以大致分为以下几个步骤: 98 | 99 | - 设置图元数据 100 | - 着色器-shader 计算图元数据(位置·颜色·其他) 101 | - 光栅化-rasterization 渲染为像素 102 | - fragment shader,决定最终成像 103 | - 其他操作(显示·隐藏·融合) 104 | 105 | > Note: 其实还有一些非必要的步骤,与本文主题不相关,这里点到为止。 106 | 107 | 我们日常开发时使用 UIKit 布局视图控件,设置透明度等等都属于**设置图元数据**这步,这也是我们日常开发中可以影响 OpenGL 渲染的主要步骤。 108 | 109 | ## OpenGL Render Pipeline 110 | 111 | 如果有同学看过 WWDC 的一些演讲稿或者接触过一些 OpenGL 知识,应该对 Render Pipeline 这个专业术语并不陌生。 112 | 113 | 不过 Render Pipeline 实在是一个初次见面不太容易理解的词,它译为**渲染管道**,也有译为渲染管线的... 114 | 115 | 其实 Render Pipeline 指的是**从应用程序数据转换到最终渲染的图像之间的一系列数据处理过程**。 116 | 117 | 好比我们上文中提到的 OpenGL 主要渲染步骤一样,我们开发应用程序时在**设置图元数据**这步为视图控件的设定布局,背景颜色,透明度以及阴影等等数据。 118 | 119 | 下面以 OpenGL 4.5 的 Render Pipeline 为例介绍一下: 120 | 121 | ![](ios-rendering-process/opengl_rendering_pipeline.png) 122 | 123 | 这些图元数据流入 OpenGL 中,传入**顶点着色器(vetex shader)**,然后顶点着色器对其进行着色器内部的处理后流出。之后可能进入**细分着色阶段(tessellation shading stage)**,其中又有可能分为细分控制着色器和细分赋值着色器两部分处理,还可能会进入**几何着色阶段(geometry shading stage)**,数据从中传递。最后都会走**片元着色阶段(fragment shading stage)**。 124 | 125 | > Note: 图元数据是以 copy 的形式流入 shader 的,shader 一般会以特殊的**类似全局变量的形式**接收数据。 126 | 127 | OpenGL 在最终成像之前还会经历一个阶段名为**计算着色阶段(compute shaing stage)**,这个阶段 OpenGL 会计算最重要在屏幕中成像的像素位置以及颜色,如果在之前提交代码时用到了 CALayer 会引起 **blending** 的显示效果(例如 Shadow)或者视图颜色或内容图片的 alpha 通道开启,都将会加大这个阶段 OpenGL 的工作量。 128 | 129 | ## Core Animation Pipeline 130 | 131 | 上文说到了 iOS 设备之所以可以成像不是因为 UIKit 而是因为 LayerKit,即 Core Animation。 132 | 133 | Core Animation 图层,即 CALayer 中包含一个属性 contents,我们可以通过给这个属性赋值来**控制 CALayer 成像的内容**。这个属性的类型定义为 id,在程序编译时不论我们给 contents 赋予任何类型的值,都是可以编译通过的。但实践中,**如果 contents 赋值类型不是 CGImage,那么你将会得到一个空白图层**。 134 | 135 | > Note: 造成 contents 属性的奇怪表现的原因是 Mac OS X 的历史包袱,它之所以被定义为 id 类型是因为在 Mac OS X 中这个属性对 CGImage 和 NSImage 类型的值都起作用。但是在 iOS 中,如果你赋予一个 UIImage 属性的值,仅仅会得到一个空白图层。 136 | 137 | 说完 Core Animation 的 contents 属性,下面介绍一下 iOS 中 Core Animation Pipeline: 138 | 139 | - 在 Application 中布局 UIKit 视图控件间接的关联 Core Animation 图层 140 | - Core Animation 图层相关的数据提交到 iOS Render Server,即 OpenGL ES & Core Graphics 141 | - Render Server 将与 GPU 通信把数据经过处理之后传递给 GPU 142 | - GPU 调用 iOS 当前设备渲染相关的图形设备 Display 143 | 144 | ![](ios-rendering-process/core_animation_pipeline.png) 145 | 146 | > Note: 由于 iOS 设备目前的显示屏最大支持 **60 FPS** 的刷新率,所以每个处理间隔为 16.67 ms。 147 | 148 | 可以看到从 Commit Transaction 之后我们的图元数据就将会在下一次 RunLoop 时被 Application 发送给底层的 Render Server,底层 Render Server 直接面向 GPU 经过一些列的数据处理将处理完毕的数据传递给 GPU,然后 GPU 负责渲染工作,根据当前 iOS 设备的屏幕计算图像**像素位置以及像素 alpha 通道混色计算**等等最终在当前 iOS 设备的显示屏中呈现图像。 149 | 150 | > 嘛~ 由于 Core Animation Pipeline 中 Render Server 包含 OpenGL ES & Core Graphics,其中 OpenGL ES 的渲染可以参考上文 OpenGL Render Pipeline 理解。 151 | 152 | ## Commit Transaction 153 | 154 | Core Animation Pipeline 的整个管线中 iOS 常规开发一般可以影响到的范围也就仅仅是在 Application 中布局 UIKit 视图控件间接的关联 Core Animation 图层这一级,即 **Commit Transaction 之前的一些操作**。 155 | 156 | 那么在 Commit Transaction 之前我们一般要做的事情有哪些? 157 | 158 | - Layout,构建视图 159 | - Display,绘制视图 160 | - Prepare,额外的 Core Animation 工作 161 | - Commit,打包图层并将它们发送到 Render Server 162 | 163 | ### Layout 164 | 165 | 在 Layout 阶段我们能做的是把 constraint 写的尽量高效,iOS 的 Layout Constraint 类似于 Android 的 Relative Layout。 166 | 167 | > Note: Emmmmm... 据观察 iOS 的 Layout Constraint 在书写时应该尽量少的依赖于视图树中同层级的兄弟视图节点,它会拖慢整个视图树的 Layout 计算过程。 168 | 169 | **这个阶段的 Layout 计算工作是在 CPU 完成的**,包括 `layoutSubviews` 方法的重载,`addSubview:` 方法填充子视图等 170 | 171 | ### Display 172 | 173 | 其实这里的 Display 仅仅是我们设置 iOS 设备要最终成像的图元数据而已,重载视图 `drawRect:` 方法可以自定义 UIView 的显示,其原理是在 `drawRect:` 方法内部绘制 bitmap。 174 | 175 | > Note: 重载 `drawRect:` 方法绘制 bitmap 过程**使用 CPU 和 内存**。 176 | 177 | 所以重载 `drawRect:` 使用不当会造成 CPU 负载过重,App 内存飙升等问题。 178 | 179 | ### Prepare 180 | 181 | 这个步骤属于附加步骤,一般处理图像的解码 & 转换等操作。 182 | 183 | ### Commit 184 | 185 | Commit 步骤指打包图层并将它们发送到 Render Server。 186 | 187 | > Note: Commit 操作会**递归执行**,由于图层和视图一样是以树形结构存在的,当图层树过于复杂时 Commit 操作的开销也会非常大。 188 | 189 | #### CATransaction 190 | 191 | CATransaction 是 Core Animation 中用于将多个图层树操作分配到渲染树的**原子更新**中的机制,对图层树的每个修改都必须是事务的一部分。 192 | 193 | CATransaction 类没有属性或者实例方法,并且也不能用 `+alloc` 和 `-init` 方法创建它,我们只能用类方法 `+begin` 和 `+commit` 分别来入栈或者出栈。 194 | 195 | 事实上任何可动画化的图层属性都会被添加到栈顶的事务,你可以通过 `+setAnimationDuration:` 方法设置当前事务的动画时间,或者通过 `+animationDuration` 方法来获取时长值(默认 0.25 秒)。 196 | 197 | Core Animation 在每个 RunLoop 周期中自动开始一次新的事务,即使你不显式地使用 `[CATransaction begin]` 开始一次事务,在一个特定 RunLoop 循环中的任何属性的变化都会被收集起来,然后做一次 0.25 秒的动画(CALayer 隐式动画)。 198 | 199 | > Note: CATransaction 支持**嵌套**。 200 | 201 | ## Animation 202 | 203 | 对于 App 用户交互体验提升最明显的工作莫过于使用动画了,那么 iOS 是如何处理动画的渲染过程的呢? 204 | 205 | 日常开发中如果不是特别复杂的动画我们一般会使用 UIView Animation 实现,iOS 将 UIView Animation 的处理过程分为以下三个阶段: 206 | 207 | - 调用 `animateWithDuration:animations:` 方法 208 | - 在 Animation Block 中进行 Layout,Display,Prepare,Commit 209 | - Render Server 根据 Animation 逐帧渲染 210 | 211 | ![](ios-rendering-process/animation.png) 212 | 213 | > Note: 原理是 `animateWithDuration:animations:` 内部使用了 CATransaction 来将整个 Animation Block 中的代码作为原子操作 commit 给了 RunLoop。 214 | 215 | ### 基于 CATransaction 实现链式动画 216 | 217 | 事实上大多数的动画交互都是有动画执行顺序的,尽管 UIView Animation 很强大,但是在写一些顺序动画时使用 UIView Animation 只能在 `+ (void)animateWithDuration:delay:options:animations:completion:` 方法的 completion block 中层级嵌套,写成一坨一坨 block 堆砌而成的代码,实在是难以阅读更别提后期维护了。 218 | 219 | 在得知 UIView Animation 使用了 CATransaction 时,我们不禁会想到这个 completion block 是不是也是基于 CATransaction 实现的呢? 220 | 221 | Bingo!CATransaction 中有 `+completionBlock` 以及 `+setCompletionBlock:` 方法可以对应于 UIView Animation 的 completion block 的书写。 222 | 223 | > Note: 我的一个开源库 [**LSAnimator - 可多链式动画库**](https://github.com/Lision/LSAnimator) 在**动画顺序链接时也用到了 CATransaction**。 224 | 225 | ## 全文总结 226 | 227 | 结合上下文不难梳理出一个 iOS **最基本的完整渲染经过(Rendering pass)**。 228 | 229 | ![](ios-rendering-process/rendering_pass.png) 230 | 231 | ### 性能检测思路 232 | 233 | 基于整篇文章的内容归纳一下我们在日常的开发工作中遇到性能问题时检测问题代码的思路: 234 | 235 | | 问题 | 建议 | 检测工具 | 236 | | :---: | :---: | :---: | 237 | | 目标帧率 | 60 FPS | Core Animation instrument | 238 | | CPU or GPU | 降低使用率节约能耗 | Time Profiler instrument | 239 | | 不必要的 CPU 渲染 | GPU 渲染更理想,但要清楚 CPU 渲染在何时有意义 | Time Profiler instrument | 240 | | 过多的 offscreen passes | 越少越好 | Core Animation instrument | 241 | | 过多的 blending | 越少越好 | Core Animation instrument | 242 | | 奇怪的图片格式或大小 | 避免实时转换或调整大小 | Core Animation instrument | 243 | | 开销昂贵的视图或特效 | 理解当前方案的开销成本 | Xcode View Debugger | 244 | | 想象不到的层次结构 | 了解实际的视图层次结构 | Xcode View Debugger | 245 | 246 | 文章写得比较用心(是我个人的原创文章,转载请注明 [https://lision.me/](https://lision.me/)),如果发现错误会优先在我的个人博客中更新。如果有任何问题欢迎在我的微博 [@Lision](http://weibo.com/lisioncode) 联系我~ 247 | 248 | 希望我的文章可以为你带来价值~ 249 | 250 | ## 扩展阅读 251 | 252 | - [WWDC2014-Advanced Graphics and Animations for iOS Apps](https://developer.apple.com/videos/play/wwdc2014/419/) 253 | - [iOS 保持界面流畅的技巧](https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/) 254 | - [iOS-Core-Animation-Advanced-Techniques](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques) -------------------------------------------------------------------------------- /Categroy/iOS/WWDC/ios-rendering-process/animation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/WWDC/ios-rendering-process/animation.png -------------------------------------------------------------------------------- /Categroy/iOS/WWDC/ios-rendering-process/core_animation_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/WWDC/ios-rendering-process/core_animation_pipeline.png -------------------------------------------------------------------------------- /Categroy/iOS/WWDC/ios-rendering-process/ios_rendering_framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/WWDC/ios-rendering-process/ios_rendering_framework.png -------------------------------------------------------------------------------- /Categroy/iOS/WWDC/ios-rendering-process/opengl_rendering_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/WWDC/ios-rendering-process/opengl_rendering_pipeline.png -------------------------------------------------------------------------------- /Categroy/iOS/WWDC/ios-rendering-process/rasterization.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/WWDC/ios-rendering-process/rasterization.jpg -------------------------------------------------------------------------------- /Categroy/iOS/WWDC/ios-rendering-process/rendering.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/WWDC/ios-rendering-process/rendering.jpg -------------------------------------------------------------------------------- /Categroy/iOS/WWDC/ios-rendering-process/rendering_pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/WWDC/ios-rendering-process/rendering_pass.png -------------------------------------------------------------------------------- /Categroy/iOS/WWDC/ios-rendering-process/tile_based_rendering.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/WWDC/ios-rendering-process/tile_based_rendering.jpg -------------------------------------------------------------------------------- /Categroy/iOS/WebViewJavascriptBridge/webview-javascript-bridge.md: -------------------------------------------------------------------------------- 1 | # WebViewJavascriptBridge 源码中 Get 到的“桥梁美学” 2 | 3 | ![](webview-javascript-bridge/img_golden_bridge.jpeg) 4 | 5 | ## 前言 6 | 7 | Emmmmm...这篇文章发布出来可能正逢圣诞节🎄,Merry Christmas! 8 | 9 | ![](webview-javascript-bridge/img_merry_ch.jpg) 10 | 11 | Web 页面中的 JS 与 iOS Native 如何交互是每个 iOS 猿必须掌握的技能。而 JS 和 iOS Native 就好比两块没有交集的大陆,如果想要使它们相互通信就必须要建立一座“桥梁”。 12 | 13 | **思考一下,如果项目组让你去造这座“桥”,如何才能做到既优雅又实用?** 14 | 15 | 本文将结合 WebViewJavascriptBridge 源码逐步带大家找到答案。 16 | 17 | [WebViewJavascriptBridge](https://github.com/marcuswestin/WebViewJavascriptBridge) 是盛名已久的 JSBridge 库,早在 2011 年就被作者 [Marcus Westin](https://github.com/marcuswestin) 发布到 GitHub,直到现在作者还在积极维护中,目前该项目已收获近 1w star 咯,其源码非常值得我们学习。 18 | 19 | WebViewJavascriptBridge 的代码逻辑清晰,风格良好,加上自身代码量比较小使得其源码阅读非常轻松(可能需要一些 JS 基础)。更加难能可贵的是它仅使用了少量代码就实现了对于 Mac OS X 的 WebView 以及 iOS 平台的 UIWebView 和 WKWebView 三种组件的完美支持。 20 | 21 | 我对 WebViewJavascriptBridge 的评价是**小而美**,这类小而美的源码非常利于我们对其实现思想的学习(本文分析 WebViewJavascriptBridge 源码版本为 v6.0.3)。 22 | 23 | 关于 iOS 与 JS 的原生交互知识,之前我有写过一篇文章[《iOS 与 JS 交互开发知识总结》](https://lision.me/ios-native-js/),文章除了介绍 JavaScriptCore 库以及 UIWebView 和 WKWebView 与 JS 原生交互的方法之外还捎带提到了 [Hybrid](https://en.wikipedia.org/wiki/Hybrid) 的发展简史,文末还提供了一个 [JS 通过 Native 调用 iOS 设备摄像头的 Demo](https://github.com/Lision/HybridCameraDemo)。 24 | 25 | 所以这篇文章不会再把重点放在 iOS 与 JS 的原生交互了,本文旨在介绍 [WebViewJavascriptBridge](https://github.com/marcuswestin/WebViewJavascriptBridge) 的设计思路和实现原理,对 iOS 与 JS 原生交互知识感兴趣的同学推荐去阅读上面提到的文章,应该会有点儿帮助(笑)。 26 | 27 | ## 索引 28 | 29 | - WebViewJavascriptBridge 简介 30 | - WebViewJavascriptBridge && WKWebViewJavascriptBridge 探究 31 | - WebViewJavascriptBridgeBase - JS 调用 Native 实现原理剖析 32 | - WebViewJavascriptBridge_JS - Native 调用 JS 实现解读 33 | - WebViewJavascriptBridge 的“桥梁美学” 34 | - 文章总结 35 | 36 | ## WebViewJavascriptBridge 简介 37 | 38 | ![](webview-javascript-bridge/img_bridge.png) 39 | 40 | WebViewJavascriptBridge 是用于在 WKWebView,UIWebView 和 WebView 中的 Obj-C 和 JavaScript 之间发送消息的 iOS / OSX 桥接器。 41 | 42 | 有许多不错的项目都有使用 WebViewJavascriptBridge,这里简单列一部分(笑): 43 | 44 | - [Facebook Messenger](https://www.messenger.com/) 45 | - [Facebook Paper](https://www.facebook.com/paper) 46 | - [ELSEWHERE](http://www.stayelsewhere.com/) 47 | - ... & many more! 48 | 49 | 关于 WebViewJavascriptBridge 的具体使用方法详见其 [GitHub 页面](https://github.com/marcuswestin/WebViewJavascriptBridge)。 50 | 51 | 在读完 WebViewJavascriptBridge 的源码之后我将其划分为三个层级: 52 | 53 | | 层级 | 源文件 | 54 | | :---: | :---: | 55 | | 接口层 | WebViewJavascriptBridge && WKWebViewJavascriptBridge | 56 | | 实现层 | WebViewJavascriptBridgeBase | 57 | | JS 层 | WebViewJavascriptBridge_JS | 58 | 59 | 其中 WebViewJavascriptBridge && WKWebViewJavascriptBridge 作为接口层主要负责提供方便的接口,隐藏实现细节,其实现细节都是通过实现层 WebViewJavascriptBridgeBase 去做的,而 WebViewJavascriptBridge_JS 作为 JS 层其实存储了一段 JS 代码,在需要的时候注入到当前 WebView 组件中,最终实现 Native 与 JS 的交互。 60 | 61 | ## WebViewJavascriptBridge && WKWebViewJavascriptBridge 探究 62 | 63 | ![](webview-javascript-bridge/img_tower_bridge.jpg) 64 | 65 | WebViewJavascriptBridge 和 WKWebViewJavascriptBridge 作为接口层分别对应于 UIWebView 和 WKWebView 组件,我们来简单看一下这两个文件暴露出的信息: 66 | 67 | WebViewJavascriptBridge 暴露信息: 68 | 69 | ``` obj-c 70 | @interface WebViewJavascriptBridge : WVJB_WEBVIEW_DELEGATE_INTERFACE 71 | 72 | + (instancetype)bridgeForWebView:(id)webView; // 初始化 73 | + (instancetype)bridge:(id)webView; // 初始化 74 | 75 | + (void)enableLogging; // 开启日志 76 | + (void)setLogMaxLength:(int)length; // 设置日志最大长度 77 | 78 | - (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler; // 注册 handler (Native) 79 | - (void)removeHandler:(NSString*)handlerName; // 删除 handler (Native) 80 | - (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback; // 调用 handler (JS) 81 | - (void)setWebViewDelegate:(id)webViewDelegate; // 设置 webViewDelegate 82 | - (void)disableJavscriptAlertBoxSafetyTimeout; // 禁用 JS AlertBox 的安全时长来加速消息传递,不推荐使用 83 | 84 | @end 85 | ``` 86 | 87 | WKWebViewJavascriptBridge 暴露信息: 88 | 89 | ``` obj-c 90 | // Emmmmm...这里应该不需要我注释了吧 91 | @interface WKWebViewJavascriptBridge : NSObject 92 | 93 | + (instancetype)bridgeForWebView:(WKWebView*)webView; 94 | + (void)enableLogging; 95 | 96 | - (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler; 97 | - (void)removeHandler:(NSString*)handlerName; 98 | - (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback; 99 | - (void)reset; 100 | - (void)setWebViewDelegate:(id)webViewDelegate; 101 | - (void)disableJavscriptAlertBoxSafetyTimeout; 102 | 103 | @end 104 | ``` 105 | 106 | > Note: `disableJavscriptAlertBoxSafetyTimeout` 方法是通过禁用 JS 端 AlertBox 的安全时长来加速网桥消息传递的。如果想使用那么需要和前端约定好,如果禁用之后前端 JS 代码仍有调用 AlertBox 相关代码(alert, confirm, 或 prompt)则程序将被挂起,所以这个方法是不安全的,如无特殊需求笔者不推荐使用。 107 | 108 | 可以看得出来这两个文件暴露出的接口几乎一致,其中 WebViewJavascriptBridge 中使用了宏定义 `WVJB_WEBVIEW_DELEGATE_INTERFACE` 来分别适配 iOS 和 Mac OS X 平台的 UIWebView 和 WebView 组件需要实现的代理方法。 109 | 110 | ### WebViewJavascriptBridge 中的宏定义 111 | 112 | 其实 WebViewJavascriptBridge 中为了适配 iOS 和 Mac OS X 平台的 UIWebView 和 WebView 组件使用了一系列的宏定义,其源码比较简单: 113 | 114 | ``` obj-c 115 | #if defined __MAC_OS_X_VERSION_MAX_ALLOWED 116 | #define WVJB_PLATFORM_OSX 117 | #define WVJB_WEBVIEW_TYPE WebView 118 | #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject 119 | #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject 120 | #elif defined __IPHONE_OS_VERSION_MAX_ALLOWED 121 | #import 122 | #define WVJB_PLATFORM_IOS 123 | #define WVJB_WEBVIEW_TYPE UIWebView 124 | #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject 125 | #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject 126 | #endif 127 | ``` 128 | 129 | 分别根据所在平台不同定义了 `WVJB_WEBVIEW_TYPE`,`WVJB_WEBVIEW_DELEGATE_TYPE` 以及刚才提到的 `WVJB_WEBVIEW_DELEGATE_INTERFACE` 宏定义,并且分别定义了 `WVJB_PLATFORM_OSX` 和 `WVJB_PLATFORM_IOS` 便于之后的实现源码区分当前平台时使用,下面的 `supportsWKWebView` 宏定义也是同样的道理: 130 | 131 | ``` obj-c 132 | #if (__MAC_OS_X_VERSION_MAX_ALLOWED > __MAC_10_9 || __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_7_1) 133 | #define supportsWKWebView 134 | #endif 135 | ``` 136 | 137 | 在引入头文件的时候可以通过这个 `supportsWKWebView` 宏灵活引入所需的头文件: 138 | 139 | ``` obj-c 140 | // WebViewJavascriptBridge.h 141 | #if defined supportsWKWebView 142 | #import 143 | #endif 144 | 145 | // WebViewJavascriptBridge.m 146 | #if defined(supportsWKWebView) 147 | #import "WKWebViewJavascriptBridge.h" 148 | #endif 149 | ``` 150 | 151 | ### WebViewJavascriptBridge 的实现分析 152 | 153 | 我们接着看一下 WebViewJavascriptBridge 的实现部分,首先从内部变量信息看起: 154 | 155 | ``` obj-c 156 | #if __has_feature(objc_arc_weak) 157 | #define WVJB_WEAK __weak 158 | #else 159 | #define WVJB_WEAK __unsafe_unretained 160 | #endif 161 | 162 | @implementation WebViewJavascriptBridge { 163 | WVJB_WEAK WVJB_WEBVIEW_TYPE* _webView; // bridge 对应的 WebView 组件 164 | WVJB_WEAK id _webViewDelegate; // 给 WebView 组件设置的代理(需要的话) 165 | long _uniqueId; // 唯一标识,Emmmmm...但是我发现没卵用,只有 _base 中的 _uniqueId 才有用 166 | WebViewJavascriptBridgeBase *_base; // 上文说过,底层实现其实都是 WebViewJavascriptBridgeBase 在做 167 | } 168 | ``` 169 | 170 | 上文提到 WebViewJavascriptBridge 和 WKWebViewJavascriptBridge 的 .h 文件暴露接口信息非常相似,那么我们要不要看看 WKWebViewJavascriptBridge 的内部变量信息呢? 171 | 172 | ``` obj-c 173 | // 注释参见 WebViewJavascriptBridge 就好 174 | @implementation WKWebViewJavascriptBridge { 175 | __weak WKWebView* _webView; 176 | __weak id _webViewDelegate; 177 | long _uniqueId; 178 | WebViewJavascriptBridgeBase *_base; 179 | } 180 | ``` 181 | 182 | 嘛~ 这俩货简直是一个妈生的。其实这是作者故意为之,因为作者想对外提供一套接口,即 WebViewJavascriptBridge,我们只需要使用 WebViewJavascriptBridge 就可以自动根据绑定的 WebView 组件的不同生成与之对应的 JSBridge 实例。 183 | 184 | ``` obj-c 185 | + (instancetype)bridge:(id)webView { 186 | // 如果支持 WKWebView 187 | #if defined supportsWKWebView 188 | // 需要先判断当前入参 webView 是否从属于 WKWebView 189 | if ([webView isKindOfClass:[WKWebView class]]) { 190 | // 返回 WKWebViewJavascriptBridge 实例 191 | return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView]; 192 | } 193 | #endif 194 | // 判断当前入参 webView 是否从属于 WebView(Mac OS X)或者 UIWebView(iOS) 195 | if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) { 196 | // 返回 WebViewJavascriptBridge 实例 197 | WebViewJavascriptBridge* bridge = [[self alloc] init]; 198 | [bridge _platformSpecificSetup:webView]; 199 | return bridge; 200 | } 201 | 202 | // 抛出 BadWebViewType 异常并返回 nil 203 | [NSException raise:@"BadWebViewType" format:@"Unknown web view type."]; 204 | return nil; 205 | } 206 | ``` 207 | 208 | 我们可以看到上面的代码,实现并不复杂。如果支持 WKWebView 的话(`#if defined supportsWKWebView`)则去判断当前绑定的 WebView 组件是否从属于 WKWebView,这样可以返回 WKWebViewJavascriptBridge 实例,否则返回 WebViewJavascriptBridge 实例,最后如果入参 `webView` 的类型不满足判断条件则抛出 `BadWebViewType` 异常。 209 | 210 | 还有一个关于 `_webViewDelegate` 的小细节,本来不打算讲的,但是还是提一下吧(囧)。其实在 WebViewJavascriptBridge 以及 WKWebViewJavascriptBridge 的初始化实现过程中,会把当前 WebView 组件的代理绑定为自己: 211 | 212 | ``` obj-c 213 | // WebViewJavascriptBridge 214 | - (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView { 215 | _webView = webView; 216 | _webView.delegate = self; 217 | _base = [[WebViewJavascriptBridgeBase alloc] init]; 218 | _base.delegate = self; 219 | } 220 | 221 | // WKWebViewJavascriptBridge 222 | - (void) _setupInstance:(WKWebView*)webView { 223 | _webView = webView; 224 | _webView.navigationDelegate = self; 225 | _base = [[WebViewJavascriptBridgeBase alloc] init]; 226 | _base.delegate = self; 227 | } 228 | ``` 229 | 230 | > Note: 替换组件的代理将其代理绑定为 bridge 自己是因为 WebViewJavascriptBridge 的实现原理上是利用我之前的文章[《iOS 与 JS 交互开发知识总结》](https://lision.me/ios-native-js/)中讲过的假 Request 方法实现的,所以需要监听 WebView 组件的代理方法获取加载之前的 Request.URL 并做处理。这也是为什么 WebViewJavascriptBridge 提供了一个接口 `setWebViewDelegate:` 存储了一个逻辑上的 `_webViewDelegate`,这个 `_webViewDelegate` 也需要遵循 WebView 组件的代理协议,这样在 WebViewJavascriptBridge 内部不同的代理方法中做完 bridge 要做的事情只有就会再去调用 `_webViewDelegate` 对应的代理方法,其实可以理解为 WebViewJavascriptBridge 对当前 WebView 组件的代理做了 hook。 231 | 232 | 对于 WebViewJavascriptBridge 中暴露的初始化以外的所有接口,其内部实现都是通过 WebViewJavascriptBridgeBase 来实现的。这样做的好处就是**即使 WebViewJavascriptBridge 因为绑定了 WKWebView 返回了 WKWebViewJavascriptBridge 实例,只要接口一致,对 JSBridge 发送相同的消息,就会有相同的实现(都是由 WebViewJavascriptBridgeBase 类实现的)**。 233 | 234 | ## WebViewJavascriptBridgeBase - JS 调用 Native 实现原理剖析 235 | 236 | ![](webview-javascript-bridge/img_pier.jpg) 237 | 238 | 作为 WebViewJavascriptBridge 的实现层,WebViewJavascriptBridgeBase 的命名也可以体现出其是作为整座“桥梁”桥墩一般的存在,我们还是按照老规矩先看一下 WebViewJavascriptBridgeBase.h 暴露的信息,好对其有一个整体的印象: 239 | 240 | ``` obj-c 241 | typedef void (^WVJBResponseCallback)(id responseData); // 回调 block 242 | typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback); // 注册的 Handler block 243 | typedef NSDictionary WVJBMessage; // 消息类型 - 字典 244 | 245 | @protocol WebViewJavascriptBridgeBaseDelegate 246 | - (NSString*) _evaluateJavascript:(NSString*)javascriptCommand; 247 | @end 248 | 249 | @interface WebViewJavascriptBridgeBase : NSObject 250 | 251 | @property (weak, nonatomic) id delegate; // 代理,指向接口层类,用以给对应接口绑定的 WebView 组件发送执行 JS 消息 252 | @property (strong, nonatomic) NSMutableArray* startupMessageQueue; // 启动消息队列,可以理解为存放 WVJBMessage 253 | @property (strong, nonatomic) NSMutableDictionary* responseCallbacks; // 回调 blocks 字典,存放 WVJBResponseCallback 类型的 block 254 | @property (strong, nonatomic) NSMutableDictionary* messageHandlers; // 已注册的 handlers 字典,存放 WVJBHandler 类型的 block 255 | @property (strong, nonatomic) WVJBHandler messageHandler; // 没卵用 256 | 257 | + (void)enableLogging; // 开启日志 258 | + (void)setLogMaxLength:(int)length; // 设置日志最大长度 259 | - (void)reset; // 对应 WKJSBridge 的 reset 接口 260 | - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName; // 发送消息,入参依次是参数,回调 block,对应 JS 端注册的 HandlerName 261 | - (void)flushMessageQueue:(NSString *)messageQueueString; // 刷新消息队列,核心代码 262 | - (void)injectJavascriptFile; // 注入 JS 263 | - (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url; // 判定是否为 WebViewJavascriptBridgeURL 264 | - (BOOL)isQueueMessageURL:(NSURL*)urll; // 判定是否为队列消息 URL 265 | - (BOOL)isBridgeLoadedURL:(NSURL*)urll; // 判定是否为 bridge 载入 URL 266 | - (void)logUnkownMessage:(NSURL*)url; // 打印收到未知消息信息 267 | - (NSString *)webViewJavascriptCheckCommand; // JS bridge 检测命令 268 | - (NSString *)webViewJavascriptFetchQueyCommand; // JS bridge 获取查询命令 269 | - (void)disableJavscriptAlertBoxSafetyTimeout; // 禁用 JS AlertBox 安全时长以获取发送消息速度提升,不建议使用,理由见上文 270 | 271 | @end 272 | ``` 273 | 274 | 嘛~ 从 .h 文件中我们可以看到整个 WebViewJavascriptBridgeBase 所暴露出来的信息,属性层面上需要对以下 4 个属性加深印象,之后分析实现的过程中会带入这些属性: 275 | 276 | - `id delegate ` 代理,可以通过代理让当前 bridge 绑定的 WebView 组件执行 JS 代码 277 | - `NSMutableArray* startupMessageQueue;` 启动消息队列,存放 Obj-C 发送给 JS 的消息(可以理解为存放 `WVJBMessage` 类型) 278 | - `NSMutableDictionary* responseCallbacks;` 回调 blocks 字典,存放 `WVJBResponseCallback` 类型的 block 279 | - `NSMutableDictionary* messageHandlers;` Obj-C 端已注册的 handlers 字典,存放 `WVJBHandler` 类型的 block 280 | 281 | Emmmmm...接口层面看一下注释就好了,后面分析实现的时候会捎带讲解一些接口,剩下一些跟实现无关的接口内容感兴趣的同学推荐自己扒源码哈。 282 | 283 | 我们在对 WebViewJavascriptBridgeBase 整体有了一个初始印象之后就可以自己写一个页面,简单的嵌入一些 JS 跑一遍流程,在中间下断点扒源码,这样我们对于 Native 与 JS 的交互流程就可以一清二楚了。 284 | 285 | **下面模拟一遍 JS 通过 WebViewJavascriptBridge 调用 Native 功能的流程分析 WebViewJavascriptBridgeBase 的相关实现**(考虑现在的时间点决定以 WKWebView 为例讲解,即针对 WKWebViewJavascriptBridge 源码讲解): 286 | 287 | ### 1.监听假 Request 并注入 WebViewJavascriptBridge_JS 内的 JS 代码 288 | 289 | 上文说到 WebViewJavascriptBridge 的实现其实本质上是利用了我之前的文章[《iOS 与 JS 交互开发知识总结》](https://lision.me/ios-native-js/)中讲过的假 Request 方法实现的,那么我们就从监听假 Request 开始讲起吧。 290 | 291 | ``` obj-c 292 | // WKNavigationDelegate 协议方法,用于监听 Request 并决定是否允许导航 293 | - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { 294 | // webView 校验 295 | if (webView != _webView) { return; } 296 | NSURL *url = navigationAction.request.URL; 297 | __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate; 298 | 299 | // 核心代码 300 | if ([_base isWebViewJavascriptBridgeURL:url]) { // 判定 WebViewJavascriptBridgeURL 301 | if ([_base isBridgeLoadedURL:url]) { // 判定 BridgeLoadedURL 302 | // 注入 JS 代码 303 | [_base injectJavascriptFile]; 304 | } else if ([_base isQueueMessageURL:url]) { // 判定 QueueMessageURL 305 | // 刷新消息队列 306 | [self WKFlushMessageQueue]; 307 | } else { 308 | // 记录未知 bridge msg 日志 309 | [_base logUnkownMessage:url]; 310 | } 311 | decisionHandler(WKNavigationActionPolicyCancel); 312 | return; 313 | } 314 | 315 | // 调用 _webViewDelegate 对应的代理方法 316 | if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) { 317 | [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler]; 318 | } else { 319 | decisionHandler(WKNavigationActionPolicyAllow); 320 | } 321 | } 322 | ``` 323 | 324 | > Note: 之前说过 WebViewJavascriptBridge 会 hook 绑定的 WebView 的代理方法,这一点 WKWebViewJavascriptBridge 也一样,在加入自己的代码之后会判断是否有 `_webViewDelegate` 响应这个代理方法,如果有则调用。 325 | 326 | 我们还是把注意力放到注释中核心代码的位置,里面会先判断当前 url 是否为 bridge url: 327 | 328 | ``` obj-c 329 | // 相关宏定义 330 | #define kOldProtocolScheme @"wvjbscheme" 331 | #define kNewProtocolScheme @"https" 332 | #define kQueueHasMessage @"__wvjb_queue_message__" 333 | #define kBridgeLoaded @"__bridge_loaded__" 334 | ``` 335 | 336 | [WebViewJavascriptBridge GitHub 页面](https://github.com/marcuswestin/WebViewJavascriptBridge) 的使用方法中第 4 步明确指出要复制粘贴 `setupWebViewJavascriptBridge` 方法到前段 JS 中,我们先来看一下这段 JS 方法源码: 337 | 338 | ``` js 339 | function setupWebViewJavascriptBridge(callback) { 340 | if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); } 341 | if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); } 342 | window.WVJBCallbacks = [callback]; 343 | // 创建一个 iframe 344 | var WVJBIframe = document.createElement('iframe'); 345 | // 设置 iframe 为不显示 346 | WVJBIframe.style.display = 'none'; 347 | // 将 iframe 的 src 置为 'https://__bridge_loaded__' 348 | WVJBIframe.src = 'https://__bridge_loaded__'; 349 | // 将 iframe 加入到 document.documentElement 350 | document.documentElement.appendChild(WVJBIframe); 351 | setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0) 352 | } 353 | ``` 354 | 355 | 上面的代码创建了一个不显示的 iframe 并将其 src 置为 `https://__bridge_loaded__`,与上文中 `kBridgeLoaded` 宏定义一致,即用于 `isBridgeLoadedURL:` 方法中判定当前 url 是否为 BridgeLoadedURL。 356 | 357 | > Note: 假 Request 的发起有两种方式,-1:`location.href` -2:`iframe`。通过 `location.href` 有个问题,就是如果 JS 多次调用原生的方法也就是 `location.href` 的值多次变化,Native 端只能接受到最后一次请求,前面的请求会被忽略掉,所以这里 WebViewJavascriptBridge 选择使用 iframe,后面不再解释。 358 | 359 | 因为加入了 src 为 `https://__bridge_loaded__` 的 iframe 元素,我们上面截获 url 的代理方法就会拿到一个 `https://__bridge_loaded__` 的 url,由于 https 满足判定 WebViewJavascriptBridgeURL,将会进入核心代码区域接着会被判定为 BridgeLoadedURL 执行注入 JS 代码的方法,即 `[_base injectJavascriptFile];`。 360 | 361 | ``` obj-c 362 | - (void)injectJavascriptFile { 363 | // 获取到 WebViewJavascriptBridge_JS 的代码 364 | NSString *js = WebViewJavascriptBridge_js(); 365 | // 将获取到的 js 通过代理方法注入到当前绑定的 WebView 组件 366 | [self _evaluateJavascript:js]; 367 | // 如果当前已有消息队列则遍历并分发消息,之后清空消息队列 368 | if (self.startupMessageQueue) { 369 | NSArray* queue = self.startupMessageQueue; 370 | self.startupMessageQueue = nil; 371 | for (id queuedMessage in queue) { 372 | [self _dispatchMessage:queuedMessage]; 373 | } 374 | } 375 | } 376 | ``` 377 | 378 | 至此,第一步交互已完成。关于 WebViewJavascriptBridge_JS 内部的 JS 代码我们放到后面的章节解读,现在可以简单理解为 WebViewJavascriptBridge 在 JS 端的具体实现代码。 379 | 380 | ### 2.JS 端调用 `callHandler` 方法之后 Native 端究竟是如何响应的? 381 | 382 | [WebViewJavascriptBridge GitHub 页面](https://github.com/marcuswestin/WebViewJavascriptBridge) 中指出 JS 端的操作方式: 383 | 384 | ``` js 385 | setupWebViewJavascriptBridge(function(bridge) { 386 | 387 | /* Initialize your app here */ 388 | 389 | bridge.registerHandler('JS Echo', function(data, responseCallback) { 390 | console.log("JS Echo called with:", data) 391 | responseCallback(data) 392 | }) 393 | bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) { 394 | console.log("JS received response:", responseData) 395 | }) 396 | }) 397 | ``` 398 | 399 | 我们知道 JS 端调用 `setupWebViewJavascriptBridge` 方法会走我们刚才分析过的第一步,即监听假 Request 并注入 WebViewJavascriptBridge_JS 内的 JS 代码。那么当 JS 端调用 `bridge.callHandler` 时,Native 端究竟是如何做出响应的呢?这里我们需要先稍微解读一下之前注入的 WebViewJavascriptBridge_JS 中的 JS 代码: 400 | 401 | ``` js 402 | // 调用 iOS handler,参数校验之后调用 _doSend 函数 403 | function callHandler(handlerName, data, responseCallback) { 404 | if (arguments.length == 2 && typeof data == 'function') { 405 | responseCallback = data; 406 | data = null; 407 | } 408 | _doSend({ handlerName:handlerName, data:data }, responseCallback); 409 | } 410 | 411 | // 如有回调,则设置 message['callbackId'] 与 responseCallbacks[callbackId] 412 | // 将 msg 加入 sendMessageQueue 数组,设置 messagingIframe.src 413 | function _doSend(message, responseCallback) { 414 | if (responseCallback) { 415 | var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); 416 | responseCallbacks[callbackId] = responseCallback; 417 | message['callbackId'] = callbackId; 418 | } 419 | sendMessageQueue.push(message); 420 | messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; 421 | } 422 | 423 | // scheme 使用 https 之后通过 host 做匹配 424 | var CUSTOM_PROTOCOL_SCHEME = 'https'; 425 | var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__'; 426 | ``` 427 | 428 | 可以看到 JS 端的代码中有 `callHandler` 函数的实现,其内部将入参 `handlerName` 以及 `data` 以字典形式作为参数调用 `_doSend` 方法,我们看一下 `_doSend` 方法的实现: 429 | 430 | - `_doSend` 方法内部会先判断入参中是否有回调 431 | - 如果有回调则根据规则生成 `callbackId` 并且将回调 block 保存到 `responseCallbacks` 字典(囧~ JS 不叫字典的,我是为了 iOS 读者看着方便),之后给消息也加入一个键值对保存刚才生成的 `callbackId` 432 | - 之后给 `sendMessageQueue` 队列加入 `message` 433 | - 将 `messagingIframe.src` 设置为 `https://__wvjb_queue_message__` 434 | 435 | 好,点到为止,对于 WebViewJavascriptBridge_JS 内的 JS 端其他源码我们放着后面看。注意这里加入了一个 src 为 `https://__wvjb_queue_message__` 的 `messagingIframe`,它也是一个不可见的 iframe。这样 Native 端会收到一个 url 为 `https://__wvjb_queue_message__` 的 request,回到第 1 步中获取到假的 request 之后会进行各项判定,这次会满足 `[_base isQueueMessageURL:url]` 的判定调用 Native 的 `WKFlushMessageQueue` 方法。 436 | 437 | ``` obj-c 438 | - (void)WKFlushMessageQueue { 439 | // 执行 WebViewJavascriptBridge._fetchQueue(); 方法 440 | [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) { 441 | if (error != nil) { 442 | NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error); 443 | } 444 | // 刷新消息列表 445 | [_base flushMessageQueue:result]; 446 | }]; 447 | } 448 | 449 | - (NSString *)webViewJavascriptFetchQueyCommand { 450 | return @"WebViewJavascriptBridge._fetchQueue();"; 451 | } 452 | ``` 453 | 454 | 可见 Native 端会在刷新队列中调用 JS 端的 `WebViewJavascriptBridge._fetchQueue();` 方法,我们来看一下 JS 端此方法的具体实现: 455 | 456 | ``` js 457 | // 获取队列,在 iOS 端刷新消息队列时会调用此函数 458 | function _fetchQueue() { 459 | // 将 sendMessageQueue 转为 JSON 格式 460 | var messageQueueString = JSON.stringify(sendMessageQueue); 461 | // 重置 sendMessageQueue 462 | sendMessageQueue = []; 463 | // 返回 JSON 格式的 464 | return messageQueueString; 465 | } 466 | ``` 467 | 468 | 这个方法会把当前 JS 端 `sendMessageQueue` 消息队列以 JSON 的形式返回,而 Native 端会调用 `[_base flushMessageQueue:result];` 将拿到的 JSON 形式消息队列作为参数调用 `flushMessageQueue:` 方法,这个方法是整个框架 Native 端的精华所在,就是稍微有点长(笑)。 469 | 470 | ``` obj-c 471 | - (void)flushMessageQueue:(NSString *)messageQueueString { 472 | // 校验 messageQueueString 473 | if (messageQueueString == nil || messageQueueString.length == 0) { 474 | NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page."); 475 | return; 476 | } 477 | 478 | // 将 messageQueueString 通过 NSJSONSerialization 解为 messages 并遍历 479 | id messages = [self _deserializeMessageJSON:messageQueueString]; 480 | for (WVJBMessage* message in messages) { 481 | // 类型校验 482 | if (![message isKindOfClass:[WVJBMessage class]]) { 483 | NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message); 484 | continue; 485 | } 486 | [self _log:@"RCVD" json:message]; 487 | 488 | // 尝试取 responseId,如取到则表明是回调,从 _responseCallbacks 取匹配的回调 block 执行 489 | NSString* responseId = message[@"responseId"]; 490 | if (responseId) { // 取到 responseId 491 | WVJBResponseCallback responseCallback = _responseCallbacks[responseId]; 492 | responseCallback(message[@"responseData"]); 493 | [self.responseCallbacks removeObjectForKey:responseId]; 494 | } else { // 未取到 responseId,则表明是正常的 JS callHandler 调用 iOS 495 | WVJBResponseCallback responseCallback = NULL; 496 | // 尝试取 callbackId,示例 cb_1_1512035076293 497 | // 对应 JS 代码 var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); 498 | NSString* callbackId = message[@"callbackId"]; 499 | if (callbackId) { // 取到 callbackId,表示 js 端希望在调用 iOS native 代码后有回调 500 | responseCallback = ^(id responseData) { 501 | if (responseData == nil) { 502 | responseData = [NSNull null]; 503 | } 504 | 505 | // 将 callbackId 作为 msg 的 responseId 并设置 responseData,执行 _queueMessage 506 | WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData }; 507 | // _queueMessage 函数主要是把 msg 转为 JSON 格式,内含 responseId = callbackId 508 | // JS 端调用 WebViewJavascriptBridge._handleMessageFromObjC('msg_JSON'); 其中 'msg_JSON' 就是 JSON 格式的 msg 509 | [self _queueMessage:msg]; 510 | }; 511 | } else { // 未取到 callbackId 512 | responseCallback = ^(id ignoreResponseData) { 513 | // Do nothing 514 | }; 515 | } 516 | 517 | // 尝试以 handlerName 获取 iOS 端之前注册过的 handler 518 | WVJBHandler handler = self.messageHandlers[message[@"handlerName"]]; 519 | if (!handler) { // 没注册过,则跳过此 msg 520 | NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message); 521 | continue; 522 | } 523 | // 调用对应的 handler,以 message[@"data"] 为入参,以 responseCallback 为回调 524 | handler(message[@"data"], responseCallback); 525 | } 526 | } 527 | } 528 | ``` 529 | 530 | 嘛~ `flushMessageQueue:` 方法作为整个 Native 端的核心,有点长是可以理解的。我们简单理一下它的实现思路: 531 | 532 | - 入参校验 533 | - 将 JSON 形式的入参转换为 Native 对象,即消息队列,这里面消息类型是之前定义过的 WVJBMessage,即字典 534 | - 如果消息中含有 “responseId” 则表明是之前 Native 调用的 JS 方法回调过来的消息(因为 JS 端和 Native 端实现逻辑是对等的,所以这个地方不明白的可以参考下面的分析) 535 | - 如果消息中不含 “responseId” 则表明是 JS 端通过 `callHandler` 函数正常调用 Native 端过来的消息 536 | - 尝试获取消息中的 “callbackId”,如果 JS 本次消息需要 Native 响应之后回调才会有这个键值,具体参见上文中 JS 端 `_doSend` 部分源码分析。如取到 “callbackId” 则需生成一个回调 block,回调 block 内部将 “callbackId” 作为 msg 的 “responseId” 执行 `_queueMessage` 将消息发送给 JS 端(JS 端处理消息逻辑与 Native 端一致,所以上面使用 “responseId” 判断当前消息是否为回调方法传递过来的消息是很容易理解的) 537 | - 尝试以消息中的 “handlerName” 从 `messageHandlers`(上文提到过,是保存 Native 端注册过的 handler 的字典)取到对应的 handler block,如果取到则执行代码块,否则打印错误日志 538 | 539 | > Note: 这个消息处理的方法虽然长,但是逻辑清晰,而且有效的解决了 JS 与 Native 相互调用的过程中参数传递的问题(包括回调),此外 JS 端的消息处理逻辑与 Native 端保持一致,实现了逻辑对称,非常值得我们学习。 540 | 541 | ## WebViewJavascriptBridge_JS - Native 调用 JS 实现解读 542 | 543 | ![](webview-javascript-bridge/img_javascript.jpg) 544 | 545 | Emmmmm...这一章节主要讲 JS 端注入的代码,即 WebViewJavascriptBridge_JS 中的 JS 源码。由于我没做过前段,能力不足,水平有限,可能有谬误希望各位读者发现的话及时指正,感激不尽。预警,由于 JS 端和上文分析过的 Native 端逻辑对称且上文已经分析过部分 JS 端的函数,所以下面的 JS 源码没有另做拆分,为避免被大段 JS 代码糊脸不感兴趣的同学可以直接看代码后面的总结。 546 | 547 | ``` js 548 | ;(function() { 549 | // window.WebViewJavascriptBridge 校验,避免重复 550 | if (window.WebViewJavascriptBridge) { 551 | return; 552 | } 553 | 554 | // 懒加载 window.onerror,用于打印 error 日志 555 | if (!window.onerror) { 556 | window.onerror = function(msg, url, line) { 557 | console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line); 558 | } 559 | } 560 | 561 | // window.WebViewJavascriptBridge 声明 562 | window.WebViewJavascriptBridge = { 563 | registerHandler: registerHandler, 564 | callHandler: callHandler, 565 | disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout, 566 | _fetchQueue: _fetchQueue, 567 | _handleMessageFromObjC: _handleMessageFromObjC 568 | }; 569 | 570 | // 变量声明 571 | var messagingIframe; // 消息 iframe 572 | var sendMessageQueue = []; // 发送消息队列 573 | var messageHandlers = {}; // JS 端注册的消息处理 handlers 字典(囧,JS 其实叫对象) 574 | 575 | // scheme 使用 https 之后通过 host 做匹配 576 | var CUSTOM_PROTOCOL_SCHEME = 'https'; 577 | var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__'; 578 | 579 | var responseCallbacks = {}; // JS 端存放回调的字典 580 | var uniqueId = 1; // 唯一标示,用于回调时生成 callbackId 581 | var dispatchMessagesWithTimeoutSafety = true; // 默认启用安全时长 582 | 583 | // 通过禁用 AlertBoxSafetyTimeout 来提速网桥消息传递 584 | function disableJavscriptAlertBoxSafetyTimeout() { 585 | dispatchMessagesWithTimeoutSafety = false; 586 | } 587 | 588 | // 同 iOS 逻辑,注册 handler 其实是往 messageHandlers 字典中插入对应 name 的 block 589 | function registerHandler(handlerName, handler) { 590 | messageHandlers[handlerName] = handler; 591 | } 592 | 593 | // 调用 iOS handler,参数校验之后调用 _doSend 函数 594 | function callHandler(handlerName, data, responseCallback) { 595 | // 如果参数只有两个且第二个参数类型为 function,则表示没有参数传递,即 data 为空 596 | if (arguments.length == 2 && typeof data == 'function') { 597 | responseCallback = data; 598 | data = null; 599 | } 600 | // 将 handlerName 和 data 作为 msg 对象参数调用 _doSend 函数 601 | _doSend({ handlerName:handlerName, data:data }, responseCallback); 602 | } 603 | 604 | // _doSend 向 Native 端发送消息 605 | function _doSend(message, responseCallback) { 606 | // 如有回调,则设置 message['callbackId'] 与 responseCallbacks[callbackId] 607 | if (responseCallback) { 608 | var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); 609 | responseCallbacks[callbackId] = responseCallback; 610 | message['callbackId'] = callbackId; 611 | } 612 | // 将 msg 加入 sendMessageQueue 数组,设置 messagingIframe.src 613 | sendMessageQueue.push(message); 614 | messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; 615 | } 616 | 617 | // 获取队列,在 iOS 端刷新消息队列时会调用此函数 618 | function _fetchQueue() { 619 | // 内部将发送消息队列 sendMessageQueue 转为 JSON 格式并返回 620 | var messageQueueString = JSON.stringify(sendMessageQueue); 621 | sendMessageQueue = []; 622 | return messageQueueString; 623 | } 624 | 625 | // iOS 端 _dispatchMessage 函数会调用此函数 626 | function _handleMessageFromObjC(messageJSON) { 627 | // 调度从 Native 端获取到的消息 628 | _dispatchMessageFromObjC(messageJSON); 629 | } 630 | 631 | // 核心代码,调度从 Native 端获取到的消息,逻辑与 Native 端一致 632 | function _dispatchMessageFromObjC(messageJSON) { 633 | // 判断有没有禁用 AlertBoxSafetyTimeout,最终会调用 _doDispatchMessageFromObjC 函数 634 | if (dispatchMessagesWithTimeoutSafety) { 635 | setTimeout(_doDispatchMessageFromObjC); 636 | } else { 637 | _doDispatchMessageFromObjC(); 638 | } 639 | 640 | // 解析 msgJSON 得到 msg 641 | function _doDispatchMessageFromObjC() { 642 | var message = JSON.parse(messageJSON); 643 | var messageHandler; 644 | var responseCallback; 645 | 646 | // 如果有 responseId,则说明是回调,取对应的 responseCallback 执行,之后释放 647 | if (message.responseId) { 648 | responseCallback = responseCallbacks[message.responseId]; 649 | if (!responseCallback) { 650 | return; 651 | } 652 | responseCallback(message.responseData); 653 | delete responseCallbacks[message.responseId]; 654 | } else { // 没有 responseId,则表示正常的 iOS call handler 调用 js 655 | // 如 msg 包含 callbackId,说明 iOS 端需要回调,初始化对应的 responseCallback 656 | if (message.callbackId) { 657 | var callbackResponseId = message.callbackId; 658 | responseCallback = function(responseData) { 659 | _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData }); 660 | }; 661 | } 662 | 663 | // 从 messageHandlers 拿到对应的 handler 执行 664 | var handler = messageHandlers[message.handlerName]; 665 | if (!handler) { 666 | // 如未取到对应的 handler 则打印错误日志 667 | console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message); 668 | } else { 669 | handler(message.data, responseCallback); 670 | } 671 | } 672 | } 673 | } 674 | 675 | // messagingIframe 的声明,类型 iframe,样式不可见,src 设置 676 | messagingIframe = document.createElement('iframe'); 677 | messagingIframe.style.display = 'none'; 678 | messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; 679 | // messagingIframe 加入 document.documentElement 中 680 | document.documentElement.appendChild(messagingIframe); 681 | 682 | // 注册 disableJavscriptAlertBoxSafetyTimeout handler,Native 可以通过禁用 AlertBox 的安全时长来加速桥接消息 683 | registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout); 684 | 685 | setTimeout(_callWVJBCallbacks, 0); 686 | function _callWVJBCallbacks() { 687 | var callbacks = window.WVJBCallbacks; 688 | delete window.WVJBCallbacks; 689 | for (var i=0; i 通常一个缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。 44 | 45 | ``` obj-c 46 | @interface YYCache : NSObject 47 | 48 | @property (copy, readonly) NSString *name; 49 | @property (strong, readonly) YYMemoryCache *memoryCache; 50 | @property (strong, readonly) YYDiskCache *diskCache; 51 | 52 | - (BOOL)containsObjectForKey:(NSString *)key; 53 | - (nullable id)objectForKey:(NSString *)key; 54 | - (void)setObject:(nullable id)object forKey:(NSString *)key; 55 | - (void)removeObjectForKey:(NSString *)key; 56 | 57 | @end 58 | ``` 59 | 60 | 上面的代码我做了简化,只保留了最基本的代码(我认为作者在最初设计 YYCache 雏形时很可能也只是提供了这些基本的接口),其他的接口只是通过调用基本的接口再附加对应处理代码而成。 61 | 62 | > Note: 其实源码中作者用了一些技巧性的宏,例如 `NS_ASSUME_NONNULL_BEGIN` 与 `NS_ASSUME_NONNULL_END` 来通过编译器层检测入参是否为空并给予警告,参见 [Nullability and Objective-C](https://developer.apple.com/swift/blog/?id=25)。 63 | > 64 | > 类似上述的编码技巧还有很多,我并非不想与大家分享我 get 到的这些编码技巧,只是觉得它与本文的主题似乎不太相符。我准备在之后专门写一篇文章来与大家分享我在阅读各大源码库过程中 get 到的编码技巧(感兴趣的话可以 [关注我](https://weibo.com/5071795354/profile))。 65 | 66 | 从代码中我们可以看到 YYCache 中持有 YYMemoryCache 与 YYDiskCache,并且对外提供了一些接口。这些接口基本都是基于 Key 和 Value 设计的,类似于 iOS 原生的字典类接口(增删改查)。 67 | 68 | ## YYMemoryCache 细节剖析 69 | 70 | ![](yycache/yymemorycache.jpg) 71 | 72 | YYMemoryCache 是一个高速的内存缓存,用于存储键值对。它与 NSDictionary 相反,Key 被保留并且不复制。API 和性能类似于 NSCache,所有方法都是线程安全的。 73 | 74 | YYMemoryCache 对象与 NSCache 的不同之处在于: 75 | 76 | - YYMemoryCache 使用 LRU(least-recently-used) 算法来驱逐对象;NSCache 的驱逐方式是非确定性的。 77 | - YYMemoryCache 提供 age、cost、count 三种方式控制缓存;NSCache 的控制方式是不精确的。 78 | - YYMemoryCache 可以配置为在收到内存警告或者 App 进入后台时自动逐出对象。 79 | 80 | > Note: YYMemoryCache 中的 `Access Methods` 消耗时长通常是稳定的 `(O(1))`。 81 | 82 | ``` obj-c 83 | @interface YYMemoryCache : NSObject 84 | 85 | #pragma mark - Attribute 86 | @property (nullable, copy) NSString *name; // 缓存名称,默认为 nil 87 | @property (readonly) NSUInteger totalCount; // 缓存对象总数 88 | @property (readonly) NSUInteger totalCost; // 缓存对象总开销 89 | 90 | 91 | #pragma mark - Limit 92 | @property NSUInteger countLimit; // 缓存对象数量限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制 93 | @property NSUInteger costLimit; // 缓存开销数量限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制 94 | @property NSTimeInterval ageLimit; // 缓存时间限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制 95 | 96 | @property NSTimeInterval autoTrimInterval; // 缓存自动清理时间间隔,默认 5s 97 | 98 | @property BOOL shouldRemoveAllObjectsOnMemoryWarning; // 是否应该在收到内存警告时删除所有缓存内对象 99 | @property BOOL shouldRemoveAllObjectsWhenEnteringBackground; // 是否应该在 App 进入后台时删除所有缓存内对象 100 | 101 | @property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache); // 我认为这是一个 hook,便于我们在收到内存警告时自定义处理缓存 102 | @property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache); // 我认为这是一个 hook,便于我们在收到 App 进入后台时自定义处理缓存 103 | 104 | @property BOOL releaseOnMainThread; // 是否在主线程释放对象,默认 NO,有些对象(例如 UIView/CALayer)应该在主线程释放 105 | @property BOOL releaseAsynchronously; // 是否异步释放对象,默认 YES 106 | 107 | - (BOOL)containsObjectForKey:(id)key; 108 | 109 | - (nullable id)objectForKey:(id)key; 110 | 111 | - (void)setObject:(nullable id)object forKey:(id)key; 112 | - (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost; 113 | - (void)removeObjectForKey:(id)key; 114 | - (void)removeAllObjects; 115 | 116 | 117 | #pragma mark - Trim 118 | - (void)trimToCount:(NSUInteger)count; // 用 LRU 算法删除对象,直到 totalCount <= count 119 | - (void)trimToCost:(NSUInteger)cost; // 用 LRU 算法删除对象,直到 totalCost <= cost 120 | - (void)trimToAge:(NSTimeInterval)age; // 用 LRU 算法删除对象,直到所有到期对象全部被删除 121 | 122 | @end 123 | ``` 124 | 125 | YYMemoryCache 的定义代码比较简单~ 该有的注释我已经加到了上面,这里 LRU 算法的实现我准备单独拎出来放到后面和(`_YYLinkedMapNode` 与 `_YYLinkedMap`)一起讲。我们这里只需要再关注一下 YYMemoryCache 是如何做到线程安全的。 126 | 127 | ### YYMemoryCache 是如何做到线程安全的 128 | 129 | ``` obj-c 130 | @implementation YYMemoryCache { 131 | pthread_mutex_t _lock; // 线程锁,旨在保证 YYMemoryCache 线程安全 132 | _YYLinkedMap *_lru; // _YYLinkedMap,YYMemoryCache 通过它间接操作缓存对象 133 | dispatch_queue_t _queue; // 串行队列,用于 YYMemoryCache 的 trim 操作 134 | } 135 | ``` 136 | 137 | 没错,这里 ibireme 选择使用 `pthread_mutex` 线程锁来确保 YYMemoryCache 的线程安全。 138 | 139 | > 有趣的是,这里 ibireme 使用 `pthread_mutex` 是有一段小故事的。在最初 YYMemoryCache 这里使用的锁是 `OSSpinLock` 自旋锁(详见 [YYCache 设计思路](https://blog.ibireme.com/2015/10/26/yycache/) 备注-关于锁),后面有人在 Github 向作者提 [issue](https://github.com/ibireme/YYModel/issues/43) 反馈 `OSSpinLock` 不安全,经过作者的确认(详见 [不再安全的 OSSpinLock](https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/))最后选择用 `pthread_mutex` 替代 `OSSpinLock`。 140 | 141 | ![](yycache/lock_benchmark.jpg) 142 | 143 | 上面是 ibireme 在确认 `OSSpinLock` 不再安全之后为了寻找替代方案做的简单性能测试,对比了一下几种能够替代 `OSSpinLock` 锁的性能。在 [不再安全的 OSSpinLock](https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/) 文末的评论中,我找到了作者使用 `pthread_mutex` 的原因。 144 | 145 | > ibireme: 苹果员工说 libobjc 里 `spinlock` 是用了一些私有方法 (`mach_thread_switch`),贡献出了高线程的优先来避免优先级反转的问题,但是我翻了下 libdispatch 的源码倒是没发现相关逻辑,也可能是我忽略了什么。在我的一些测试中,`OSSpinLock` 和 `dispatch_semaphore` 都不会产生特别明显的死锁,所以我也无法确定用 `dispatch_semaphore` 代替 `OSSpinLock` 是否正确。能够肯定的是,用 `pthread_mutex` 是安全的。 146 | 147 | ### `_YYLinkedMapNode` 与 `_YYLinkedMap` 148 | 149 | 上文介绍了 YYMemoryCache,其实 YYMemoryCache 并不直接操作缓存对象,而是通过内部的 `_YYLinkedMapNode` 与 `_YYLinkedMap` 来间接的操作缓存对象。这两个类对于上文中提到的 LRU 缓存算法的理解至关重要,所以我把他们俩单独拎出来放在这里详细解读一下。 150 | 151 | ``` obj-c 152 | 153 | /** 154 | _YYLinkedMap 中的一个节点。 155 | 通常情况下我们不应该使用这个类。 156 | */ 157 | @interface _YYLinkedMapNode : NSObject { 158 | @package 159 | __unsafe_unretained _YYLinkedMapNode *_prev; // __unsafe_unretained 是为了性能优化,节点被 _YYLinkedMap 的 _dic 强引用 160 | __unsafe_unretained _YYLinkedMapNode *_next; // __unsafe_unretained 是为了性能优化,节点被 _YYLinkedMap 的 _dic 强引用 161 | id _key; 162 | id _value; 163 | NSUInteger _cost; // 记录开销,对应 YYMemoryCache 提供的 cost 控制 164 | NSTimeInterval _time; // 记录时间,对应 YYMemoryCache 提供的 age 控制 165 | } 166 | @end 167 | 168 | 169 | /** 170 | YYMemoryCache 内的一个链表。 171 | _YYLinkedMap 不是一个线程安全的类,而且它也不对参数做校验。 172 | 通常情况下我们不应该使用这个类。 173 | */ 174 | @interface _YYLinkedMap : NSObject { 175 | @package 176 | CFMutableDictionaryRef _dic; // 不要直接设置该对象 177 | NSUInteger _totalCost; 178 | NSUInteger _totalCount; 179 | _YYLinkedMapNode *_head; // MRU, 最常用节点,不要直接修改它 180 | _YYLinkedMapNode *_tail; // LRU, 最少用节点,不要直接修改它 181 | BOOL _releaseOnMainThread; // 对应 YYMemoryCache 的 releaseOnMainThread 182 | BOOL _releaseAsynchronously; // 对应 YYMemoryCache 的 releaseAsynchronously 183 | } 184 | 185 | // 链表操作,看接口名称应该不需要注释吧~ 186 | - (void)insertNodeAtHead:(_YYLinkedMapNode *)node; 187 | - (void)bringNodeToHead:(_YYLinkedMapNode *)node; 188 | - (void)removeNode:(_YYLinkedMapNode *)node; 189 | - (_YYLinkedMapNode *)removeTailNode; 190 | - (void)removeAll; 191 | 192 | @end 193 | 194 | ``` 195 | 196 | 为了方便大家阅读,我标注了必要的中文注释。其实对数据结构与算法不陌生的同学应该一眼就看的出来 `_YYLinkedMapNode` 与 `_YYLinkedMap` 这俩货的本质。没错,丫就是双向链表节点和双向链表。 197 | 198 | `_YYLinkedMapNode` 作为双向链表节点,除了基本的 `_prev`、`_next`,还有键值缓存基本的 `_key` 与 `_value`,**我们可以把 `_YYLinkedMapNode` 理解为 YYMemoryCache 中的一个缓存对象**。 199 | 200 | `_YYLinkedMap` 作为由 `_YYLinkedMapNode` 节点组成的双向链表,使用 `CFMutableDictionaryRef _dic` 字典存储 `_YYLinkedMapNode`。这样在确保 `_YYLinkedMapNode` 被强引用的同时,能够利用字典的 Hash 快速定位用户要访问的缓存对象,这样既符合了键值缓存的概念又省去了自己实现的麻烦(笑)。 201 | 202 | 嘛~ 总得来说 YYMemoryCache 是通过使用 `_YYLinkedMap` 双向链表来操作 `_YYLinkedMapNode` 缓存对象节点的。 203 | 204 | ### LRU(least-recently-used) 算法的实现 205 | 206 | 上文我们认清了 `_YYLinkedMap` 与 `_YYLinkedMapNode` 本质上就是双向链表和链表节点,这里我们简单讲一下 YYMemoryCache 是如何利用双向链表实现 LRU(least-recently-used) 算法的。 207 | 208 | #### 缓存替换策略 209 | 210 | 首先 LRU 是缓存替换策略([Cache replacement policies](https://en.wikipedia.org/wiki/Cache_replacement_policies))的一种,还有很多缓存替换策略诸如: 211 | 212 | - First In First Out (FIFO) 213 | - Last In First Out (LIFO) 214 | - Time aware Least Recently Used (TLRU) 215 | - Most Recently Used (MRU) 216 | - Pseudo-LRU (PLRU) 217 | - Random Replacement (RR) 218 | - Segmented LRU (SLRU) 219 | - Least-Frequently Used (LFU) 220 | - Least Frequent Recently Used (LFRU) 221 | - LFU with Dynamic Aging (LFUDA) 222 | - Low Inter-reference Recency Set (LIRS) 223 | - Adaptive Replacement Cache (ARC) 224 | - Clock with Adaptive Replacement (CAR) 225 | - Multi Queue (MQ) caching algorithm|Multi Queue (MQ) 226 | - Pannier: Container-based caching algorithm for compound objects 227 | 228 | 是不是被唬到了?不要担心,我这里会表述的尽量易懂。 229 | 230 | #### 缓存命中率 231 | 232 | ![](yycache/cache-hit-ratio.png) 233 | 234 | 为什么有这么多缓存替换策略,或者说搞这么多名堂究竟是为了什么呢? 235 | 236 | 答案是提高缓存命中率,那么何谓缓存命中率呢? 237 | 238 | Google 一下自然是有不少解释,不过很多都是 web 相关的,而且不说人话(很难理解),我个人非常讨厌各种不说人话的“高深”抽象概念。 239 | 240 | 这里抖了好几抖胆才敢谈一下我对于缓存命中率的理解(限于 YYCache 和 iOS 开发)。 241 | 242 | - 缓存命中 = 用户要访问的缓存对象在高速缓存中,我们直接在高速缓存中通过 Hash 将其找到并返回给用户。 243 | - 缓存命中率 = 用户要访问的缓存对象在高速缓存中被我们访问到的概率。 244 | 245 | 既然谈到了自己的理解,我索性说个够。 246 | 247 | - 缓存丢失 = 由于高速缓存数量有限(占据内存等原因),所以用户要访问的缓存对象很有可能被我们从有限的高速缓存中淘汰掉了,我们可能会将其存储于低速的磁盘缓存中(如果磁盘缓存还有资源的话),那么就要从磁盘缓存中获取该缓存对象以返回给用户,这种情况我理解为(高速)缓存未命中,即缓存丢失(并不是真的被我们丢掉了,但肯定是被我们从高速缓存淘汰掉了)。 248 | 249 | 缓存命中是 cache-hit,那么如果你玩游戏,可以理解为这次 hit miss 了(笑,有人找我开黑吗)。 250 | 251 | #### LRU 252 | 253 | 首先来讲一下 LRU 的概念让大家有一个基本的认识。LRU(least-recently-used) 翻译过来是“最近最少使用”,顾名思义这种缓存替换策略是基于用户最近最少访问过的缓存对象而建立。 254 | 255 | 我认为 LRU 缓存替换策略的核心思想在于:LRU 认为用户最新使用(访问)过的缓存对象为高频缓存对象,即用户很可能还会再次使用(访问)该缓存对象;而反之,用户很久之前使用(访问)过的缓存对象(期间一直没有再次访问)为低频缓存对象,即用户很可能不会再去使用(访问)该缓存对象,通常在资源不足时会先去释放低频缓存对象。 256 | 257 | #### `_YYLinkedMapNode` 与 `_YYLinkedMap` 实现 LRU 258 | 259 | YYCache 作者通过 `_YYLinkedMapNode` 与 `_YYLinkedMap` 双向链表实现 LRU 缓存替换策略的思路其实很简捷清晰,我们一步一步来看。 260 | 261 | 双向链表中有头结点和尾节点: 262 | 263 | - 头结点 = 链表中用户最近一次使用(访问)的缓存对象节点,MRU。 264 | - 尾节点 = 链表中用户已经很久没有再次使用(访问)的缓存对象节点,LRU。 265 | 266 | 如何让头结点和尾节点指向我们想指向的缓存对象节点?我们结合代码来看: 267 | 268 | - 在用户使用(访问)时更新缓存节点信息,并将其移动至双向链表头结点。 269 | 270 | ``` obj-c 271 | - (id)objectForKey:(id)key { 272 | // 判断入参 273 | if (!key) return nil; 274 | pthread_mutex_lock(&_lock); 275 | // 找到对应缓存节点 276 | _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); 277 | if (node) { 278 | // 更新缓存节点时间,并将其移动至双向链表头结点 279 | node->_time = CACurrentMediaTime(); 280 | [_lru bringNodeToHead:node]; 281 | } 282 | pthread_mutex_unlock(&_lock); 283 | // 返回找到的缓存节点 value 284 | return node ? node->_value : nil; 285 | } 286 | ``` 287 | 288 | - 在用户设置缓存对象时,判断入参 key 对应的缓存对象节点是否存在?存在则更新缓存对象节点并将节点移动至链表头结点;不存在则根据入参生成新的缓存对象节点并插入链表表头。 289 | 290 | ``` obj-c 291 | - (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost { 292 | // 判断入参,省略 293 | ... 294 | pthread_mutex_lock(&_lock); 295 | // 判断入参 key 对应的缓存对象节点是否存在 296 | _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); 297 | NSTimeInterval now = CACurrentMediaTime(); 298 | if (node) { 299 | // 存在则更新缓存对象节点并将节点移动至链表头结点 300 | _lru->_totalCost -= node->_cost; 301 | _lru->_totalCost += cost; 302 | node->_cost = cost; 303 | node->_time = now; 304 | node->_value = object; 305 | [_lru bringNodeToHead:node]; 306 | } else { 307 | // 不存在则根据入参生成新的缓存对象节点并插入链表表头 308 | node = [_YYLinkedMapNode new]; 309 | node->_cost = cost; 310 | node->_time = now; 311 | node->_key = key; 312 | node->_value = object; 313 | [_lru insertNodeAtHead:node]; 314 | } 315 | // 判断插入、更新节点之后是否超过了限制 cost、count,如果超过则 trim,省略 316 | ... 317 | pthread_mutex_unlock(&_lock); 318 | } 319 | ``` 320 | 321 | - 在资源不足时,从双线链表的尾节点(LRU)开始清理缓存,释放资源。 322 | 323 | ``` obj-c 324 | // 这里拿 count 资源举例,cost、age 自己举一反三 325 | - (void)_trimToCount:(NSUInteger)countLimit { 326 | // 判断 countLimit 为 0,则全部清空缓存,省略 327 | // 判断 _lru->_totalCount <= countLimit,没有超出资源限制则不作处理,省略 328 | ... 329 | 330 | NSMutableArray *holder = [NSMutableArray new]; 331 | while (!finish) { 332 | if (pthread_mutex_trylock(&_lock) == 0) { 333 | if (_lru->_totalCount > countLimit) { 334 | // 从双线链表的尾节点(LRU)开始清理缓存,释放资源 335 | _YYLinkedMapNode *node = [_lru removeTailNode]; 336 | if (node) [holder addObject:node]; 337 | } else { 338 | finish = YES; 339 | } 340 | pthread_mutex_unlock(&_lock); 341 | } else { 342 | // 使用 usleep 以微秒为单位挂起线程,在短时间间隔挂起线程 343 | // 对比 sleep 用 usleep 能更好的利用 CPU 时间 344 | usleep(10 * 1000); //10 ms 345 | } 346 | } 347 | 348 | // 判断是否需要在主线程释放,采取释放缓存对象操作 349 | if (holder.count) { 350 | dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); 351 | dispatch_async(queue, ^{ 352 | // 异步释放,我们单独拎出来讲 353 | [holder count]; // release in queue 354 | }); 355 | } 356 | } 357 | ``` 358 | 359 | 嘛~ 是不是感觉敲简单?上面代码去掉了可能会分散大家注意力的代码,我们这里仅仅讨论 LRU 的实现,其余部分的具体实现源码也非常简单,我觉得没必要贴出来单独讲解,感兴趣的同学可以自己去 [YYCache](https://github.com/ibireme/YYCache) 下载源码查阅。 360 | 361 | #### 异步释放技巧 362 | 363 | 关于上面的异步释放缓存对象的代码,我觉得还是有必要单独拎出来讲一下的: 364 | 365 | ``` obj-c 366 | dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); 367 | dispatch_async(queue, ^{ 368 | // 异步释放,我们单独拎出来讲 369 | [holder count]; // release in queue 370 | }); 371 | ``` 372 | 373 | 这个技巧 ibireme 在他的另一篇文章 [iOS 保持界面流畅的技巧](https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/) 中有提及: 374 | 375 | > Note: 对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。 376 | 377 | 而上面代码中的 YYMemoryCacheGetReleaseQueue 这个队列源码为: 378 | 379 | ``` obj-c 380 | // 静态内联 dispatch_queue_t 381 | static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() { 382 | return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); 383 | } 384 | ``` 385 | 386 | 在源码中可以看到 YYMemoryCacheGetReleaseQueue 是一个低优先级 `DISPATCH_QUEUE_PRIORITY_LOW` 队列,猜测这样设计的原因是可以让 iOS 在系统相对空闲时再来异步释放缓存对象。 387 | 388 | ## YYDiskCache 细节剖析 389 | 390 | ![](yycache/yydiskcache.jpg) 391 | 392 | YYDiskCache 是一个线程安全的磁盘缓存,用于存储由 SQLite 和文件系统支持的键值对(类似于 NSURLCache 的磁盘缓存)。 393 | 394 | YYDiskCache 具有以下功能: 395 | 396 | - 它使用 LRU(least-recently-used) 来删除对象。 397 | - 支持按 cost,count 和 age 进行控制。 398 | - 它可以被配置为当没有可用的磁盘空间时自动驱逐缓存对象。 399 | - 它可以自动抉择每个缓存对象的存储类型(sqlite/file)以便提供更好的性能表现。 400 | 401 | > Note: 您可以编译最新版本的 sqlite 并忽略 iOS 系统中的 libsqlite3.dylib 来获得 2x〜4x 的速度提升。 402 | 403 | ``` obj-c 404 | @interface YYDiskCache : NSObject 405 | 406 | #pragma mark - Attribute 407 | @property (nullable, copy) NSString *name; // 缓存名称,默认为 nil 408 | @property (readonly) NSString *path; // 缓存路径 409 | 410 | @property (readonly) NSUInteger inlineThreshold; // 阈值,大于阈值则存储类型为 file;否则存储类型为 sqlite 411 | 412 | @property (nullable, copy) NSData *(^customArchiveBlock)(id object); // 用来替换 NSKeyedArchiver,你可以使用该代码块以支持没有 conform `NSCoding` 协议的对象 413 | @property (nullable, copy) id (^customUnarchiveBlock)(NSData *data); // 用来替换 NSKeyedUnarchiver,你可以使用该代码块以支持没有 conform `NSCoding` 协议的对象 414 | 415 | @property (nullable, copy) NSString *(^customFileNameBlock)(NSString *key); // 当一个对象将以 file 的形式保存时,该代码块用来生成指定文件名。如果为 nil,则默认使用 md5(key) 作为文件名 416 | 417 | #pragma mark - Limit 418 | @property NSUInteger countLimit; // 缓存对象数量限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制 419 | @property NSUInteger costLimit; // 缓存开销数量限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制 420 | @property NSTimeInterval ageLimit; // 缓存时间限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制 421 | @property NSUInteger freeDiskSpaceLimit; // 缓存应该保留的最小可用磁盘空间(以字节为单位),默认无限制,超过限制则会在后台逐出一些对象以满足限制 422 | 423 | @property NSTimeInterval autoTrimInterval; // 缓存自动清理时间间隔,默认 60s 424 | @property BOOL errorLogsEnabled; // 是否开启错误日志 425 | 426 | #pragma mark - Initializer 427 | - (nullable instancetype)initWithPath:(NSString *)path 428 | inlineThreshold:(NSUInteger)threshold NS_DESIGNATED_INITIALIZER; 429 | 430 | - (BOOL)containsObjectForKey:(NSString *)key; 431 | 432 | - (nullable id)objectForKey:(NSString *)key; 433 | 434 | - (void)setObject:(nullable id)object forKey:(NSString *)key; 435 | 436 | - (void)removeObjectForKey:(NSString *)key; 437 | - (void)removeAllObjects; 438 | 439 | - (NSInteger)totalCount; 440 | - (NSInteger)totalCost; 441 | 442 | #pragma mark - Trim 443 | - (void)trimToCount:(NSUInteger)count; 444 | - (void)trimToCost:(NSUInteger)cost; 445 | - (void)trimToAge:(NSTimeInterval)age; 446 | 447 | #pragma mark - Extended Data 448 | + (nullable NSData *)getExtendedDataFromObject:(id)object; 449 | + (void)setExtendedData:(nullable NSData *)extendedData toObject:(id)object; 450 | 451 | @end 452 | ``` 453 | YYDiskCache 结构与 YYMemoryCache 类似,由于很多接口都是基于基本的接口做了扩展所得,这里贴的代码省略了一些接口。代码还是一如既往的干净简洁,相信各位都能看懂。 454 | 455 | YYDiskCache 是基于 sqlite 和 file 来做的磁盘缓存,我们的缓存对象可以自由的选择存储类型,下面简单对比一下: 456 | 457 | - sqlite: 对于小数据(例如 NSNumber)的存取效率明显高于 file。 458 | - file: 对于较大数据(例如高质量图片)的存取效率优于 sqlite。 459 | 460 | 所以 YYDiskCache 使用两者配合,灵活的存储以提高性能。 461 | 462 | ### NSMapTable 463 | 464 | NSMapTable 是类似于字典的集合,但具有更广泛的可用内存语义。NSMapTable 是 iOS6 之后引入的类,它基于 NSDictionary 建模,但是具有以下差异: 465 | 466 | - 键/值可以选择 “weakly” 持有,以便于在回收其中一个对象时删除对应条目。 467 | - 它可以包含任意指针(其内容不被约束为对象)。 468 | - 您可以将 NSMapTable 实例配置为对任意指针进行操作,而不仅仅是对象。 469 | 470 | > Note: 配置映射表时,请注意,只有 NSMapTableOptions 中列出的选项才能保证其余的 API 能够正常工作,包括复制,归档和快速枚举。 虽然其他 NSPointerFunctions 选项用于某些配置,例如持有任意指针,但并不是所有选项的组合都有效。使用某些组合,NSMapTableOptions 可能无法正常工作,甚至可能无法正确初始化。 471 | 472 | 更多信息详见 [NSMapTable 官方文档](https://developer.apple.com/documentation/foundation/nsmaptable?language=objc)。 473 | 474 | 需要特殊说明的是,YYDiskCache 内部是基于一个单例 NSMapTable 管理的,这点有别于 YYMemoryCache。 475 | 476 | ``` obj-c 477 | static NSMapTable *_globalInstances; // 引用管理所有的 YYDiskCache 实例 478 | static dispatch_semaphore_t _globalInstancesLock; // YYDiskCache 使用 dispatch_semaphore 保障 NSMapTable 线程安全 479 | 480 | static void _YYDiskCacheInitGlobal() { 481 | static dispatch_once_t onceToken; 482 | dispatch_once(&onceToken, ^{ 483 | _globalInstancesLock = dispatch_semaphore_create(1); 484 | _globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0]; 485 | }); 486 | } 487 | 488 | static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) { 489 | if (path.length == 0) return nil; 490 | _YYDiskCacheInitGlobal(); 491 | dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER); 492 | id cache = [_globalInstances objectForKey:path]; 493 | dispatch_semaphore_signal(_globalInstancesLock); 494 | return cache; 495 | } 496 | 497 | static void _YYDiskCacheSetGlobal(YYDiskCache *cache) { 498 | if (cache.path.length == 0) return; 499 | _YYDiskCacheInitGlobal(); 500 | dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER); 501 | [_globalInstances setObject:cache forKey:cache.path]; 502 | dispatch_semaphore_signal(_globalInstancesLock); 503 | } 504 | ``` 505 | 506 | 每当一个 YYDiskCache 被初始化时,其实会先到 NSMapTable 中获取对应 path 的 YYDiskCache 实例,如果获取不到才会去真正的初始化一个 YYDiskCache 实例,并且将其引用在 NSMapTable 中,这样做也会提升不少性能。 507 | 508 | ``` obj-c 509 | - (instancetype)initWithPath:(NSString *)path 510 | inlineThreshold:(NSUInteger)threshold { 511 | // 判断是否可以成功初始化,省略 512 | ... 513 | 514 | // 先从 NSMapTable 单例中根据 path 获取 YYDiskCache 实例,如果获取到就直接返回该实例 515 | YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path); 516 | if (globalCache) return globalCache; 517 | 518 | // 没有获取到则初始化一个 YYDiskCache 实例 519 | // 要想初始化一个 YYDiskCache 首先要初始化一个 YYKVStorage 520 | YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type]; 521 | if (!kv) return nil; 522 | 523 | // 根据刚才得到的 kv 和 path 入参初始化一个 YYDiskCache 实例,代码太长省略 524 | ... 525 | 526 | // 开启递归清理,会根据 _autoTrimInterval 对 YYDiskCache trim 527 | [self _trimRecursively]; 528 | // 向 NSMapTable 单例注册新生成的 YYDiskCache 实例 529 | _YYDiskCacheSetGlobal(self); 530 | 531 | // App 生命周期通知相关代码,省略 532 | ... 533 | return self; 534 | } 535 | ``` 536 | 537 | 我在 [YYCache 设计思路](https://blog.ibireme.com/2015/10/26/yycache/) 中找到了作者使用 dispatch_semaphore 作为 YYDiskCache 锁的原因: 538 | 539 | > dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。 540 | 541 | ### YYKVStorageItem 与 YYKVStorage 542 | 543 | 刚才在 YYDiskCache 的初始化源码中,我们不难发现一个类 YYKVStorage。与 YYMemoryCache 相对应的,YYDiskCache 也不会直接操作缓存对象(sqlite/file),而是通过 YYKVStorage 来间接的操作缓存对象。 544 | 545 | 从这一点上不难发现,YYKVStorage 等价于 YYMemoryCache 中的双向链表 `_YYLinkedMap`,而对应于 `_YYLinkedMap` 中的节点 `_YYLinkedMapNode`,YYKVStorage 中也有一个类 YYKVStorageItem 充当着与缓存对象一对一的角色。 546 | 547 | ``` obj-c 548 | // YYKVStorageItem 是 YYKVStorage 中用来存储键值对和元数据的类 549 | // 通常情况下,我们不应该直接使用这个类 550 | @interface YYKVStorageItem : NSObject 551 | @property (nonatomic, strong) NSString *key; ///< key 552 | @property (nonatomic, strong) NSData *value; ///< value 553 | @property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline) 554 | @property (nonatomic) int size; ///< value's size in bytes 555 | @property (nonatomic) int modTime; ///< modification unix timestamp 556 | @property (nonatomic) int accessTime; ///< last access unix timestamp 557 | @property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data) 558 | @end 559 | 560 | 561 | /** 562 | YYKVStorage 是基于 sqlite 和文件系统的键值存储。 563 | 通常情况下,我们不应该直接使用这个类。 564 | 565 | @warning 566 | 这个类的实例是 *非* 线程安全的,你需要确保 567 |   只有一个线程可以同时访问该实例。如果你真的 568 |   需要在多线程中处理大量的数据,应该分割数据 569 |   到多个 KVStorage 实例(分片)。 570 | */ 571 | @interface YYKVStorage : NSObject 572 | 573 | #pragma mark - Attribute 574 | @property (nonatomic, readonly) NSString *path; /// storage 路径 575 | @property (nonatomic, readonly) YYKVStorageType type; /// storage 类型 576 | @property (nonatomic) BOOL errorLogsEnabled; /// 是否开启错误日志 577 | 578 | #pragma mark - Initializer 579 | - (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER; 580 | 581 | #pragma mark - Save Items 582 | - (BOOL)saveItem:(YYKVStorageItem *)item; 583 | ... 584 | 585 | #pragma mark - Remove Items 586 | - (BOOL)removeItemForKey:(NSString *)key; 587 | ... 588 | 589 | #pragma mark - Get Items 590 | - (nullable YYKVStorageItem *)getItemForKey:(NSString *)key; 591 | ... 592 | 593 | #pragma mark - Get Storage Status 594 | - (BOOL)itemExistsForKey:(NSString *)key; 595 | - (int)getItemsCount; 596 | - (int)getItemsSize; 597 | 598 | @end 599 | ``` 600 | 601 | 代码美哭了有木有!?这种代码根本不需要翻译,我觉得相比于逐行的翻译,直接看代码更舒服。这里我们只需要看一下 YYKVStorageType 这个枚举,他决定着 YYKVStorage 的存储类型。 602 | 603 | #### YYKVStorageType 604 | 605 | ``` obj-c 606 | /** 607 | 存储类型,指示“YYKVStorageItem.value”存储在哪里。 608 | 609 | @discussion 610 | 通常,将数据写入 sqlite 比外部文件更快,但是 611 |   读取性能取决于数据大小。在我的测试(环境 iPhone 6 64G), 612 |   当数据较大(超过 20KB)时从外部文件读取数据比 sqlite 更快。 613 | */ 614 | typedef NS_ENUM(NSUInteger, YYKVStorageType) { 615 | YYKVStorageTypeFile = 0, // value 以文件的形式存储于文件系统 616 | YYKVStorageTypeSQLite = 1, // value 以二进制形式存储于 sqlite 617 | YYKVStorageTypeMixed = 2, // value 将根据你的选择基于上面两种形式混合存储 618 | }; 619 | ``` 620 | 621 | 在 YYKVStorageType 的注释中标记了作者写 YYCache 时做出的测试结论,大家也可以基于自己的环境去测试验证作者的说法(这一点是可以讨论的,我们可以根据自己的测试来设置 YYDiskCache 中的 inlineThreshold 阈值)。 622 | 623 | > 如果想要了解更多的信息可以点击 [Internal Versus External BLOBs in SQLite](http://www.sqlite.org/intern-v-extern-blob.html) 查阅 SQLite 官方文档。 624 | 625 | #### YYKVStorage 性能优化细节 626 | 627 | 上文说到 YYKVStorage 可以基于 SQLite 和文件系统做磁盘存储,这里再提一些我阅读源码发现到的有趣细节: 628 | 629 | ``` obj-c 630 | @implementation YYKVStorage { 631 | ... 632 | CFMutableDictionaryRef _dbStmtCache; // 焦点集中在这里 633 | ... 634 | } 635 | ``` 636 | 637 | 可以看到 `CFMutableDictionaryRef _dbStmtCache;` 是 YYKVStorage 中的私有成员,它是一个可变字典充当着 sqlite3_stmt 缓存的角色。 638 | 639 | ``` obj-c 640 | - (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql { 641 | if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL; 642 | // 先尝试从 _dbStmtCache 根据入参 sql 取出已缓存 sqlite3_stmt 643 | sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql)); 644 | if (!stmt) { 645 | // 如果没有缓存再从新生成一个 sqlite3_stmt 646 | int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); 647 | // 生成结果异常则根据错误日志开启标识打印日志 648 | if (result != SQLITE_OK) { 649 | if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); 650 | return NULL; 651 | } 652 | // 生成成功则放入 _dbStmtCache 缓存 653 | CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt); 654 | } else { 655 | sqlite3_reset(stmt); 656 | } 657 | return stmt; 658 | } 659 | ``` 660 | 661 | 这样就可以省去一些重复生成 sqlite3_stmt 的开销。 662 | 663 | > sqlite3_stmt: 该对象的实例表示已经编译成二进制形式并准备执行的单个 SQL 语句。 664 | 665 | 更多关于 SQLite 的信息请点击 [SQLite 官方文档](http://www.sqlite.org/docs.html) 查阅。 666 | 667 | ## 优秀的缓存应该具备哪些特质 668 | 669 | ![](yycache/good-cache.jpg) 670 | 671 | 嘛~ 我们回到文章最初提到的问题,优秀的缓存应该具备哪些特质? 672 | 673 | 如果跟着文章一步步读到这里,相信很容易举出以下几点: 674 | 675 | - 内存缓存和磁盘缓存 676 | - 线程安全 677 | - 缓存控制 678 | - 缓存替换策略 679 | - 缓存命中率 680 | - 性能 681 | 682 | 我们简单的总结一下 YYCache 源码中是如何体现这些特质的。 683 | 684 | ### 内存缓存和磁盘缓存 685 | 686 | YYCache 是由内存缓存 YYMemoryCache 与磁盘缓存 YYDiskCache 相互配合组成的,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。这样的设计支持用户在缓存不同对象时都能够有很好的体验。 687 | 688 | 在 YYCache 中使用接口访问缓存对象时,会先去尝试从内存缓存 YYMemoryCache 中访问,如果访问不到(没有使用该 key 缓存过对象或者该对象已经从容量有限的 YYMemoryCache 中淘汰掉)才会去从 YYDiskCache 访问,如果访问到(表示之前确实使用该 key 缓存过对象,该对象已经从容量有限的 YYMemoryCache 中淘汰掉成立)会先在 YYMemoryCache 中更新一次该缓存对象的访问信息之后才返回给接口。 689 | 690 | ### 线程安全 691 | 692 | 如果说 YYCache 这个类是一个纯逻辑层的缓存类(指 YYCache 的接口实现全部是调用其他类完成),那么 YYMemoryCache 与 YYDiskCache 还是做了一些事情的(并没有 YYCache 当甩手掌柜那么轻松),其中最显而易见的就是 YYMemoryCache 与 YYDiskCache 为 YYCache 保证了线程安全。 693 | 694 | YYMemoryCache 使用了 `pthread_mutex` 线程锁来确保线程安全,而 YYDiskCache 则选择了更适合它的 `dispatch_semaphore`,上文已经给出了作者选择这些锁的原因。 695 | 696 | ### 缓存控制 697 | 698 | YYCache 提供了三种控制维度,分别是:cost、count、age。这已经满足了绝大多数开发者的需求,我们在自己设计缓存时也可以根据自己的使用环境提供合适的控制方式。 699 | 700 | ### 缓存替换策略 701 | 702 | 在上文解析 YYCache 源码的时候,介绍了缓存替换策略的概念并且列举了很多经典的策略。YYCache 使用了双向链表(`_YYLinkedMapNode` 与 `_YYLinkedMap`)实现了 LRU(least-recently-used) 策略,旨在提高 YYCache 的缓存命中率。 703 | 704 | ### 缓存命中率 705 | 706 | 这一概念是在上文解析 `_YYLinkedMapNode` 与 `_YYLinkedMap` 小节介绍的,我们在自己设计缓存时不一定非要使用 LRU 策略,可以根据我们的实际使用环境选择最适合我们自己的缓存替换策略。 707 | 708 | ### 性能 709 | 710 | 其实性能这个东西是隐而不见的,又是到处可见的(笑)。它从我们最开始设计一个缓存架构时就被带入,一直到我们具体的实现细节中慢慢成形,最后成为了我们设计出来的缓存优秀与否的决定性因素。 711 | 712 | 上文中剖析了太多 YYCache 中对于性能提升的实现细节: 713 | 714 | - 异步释放缓存对象 715 | - 锁的选择 716 | - 使用 NSMapTable 单例管理的 YYDiskCache 717 | - YYKVStorage 中的 `_dbStmtCache` 718 | - 甚至使用 CoreFoundation 来换取微乎其微的性能提升 719 | 720 | 看到这里是不是恍然大悟,性能是怎么来的?就是这样对于每一个细节的极致追求一点一滴积少成多抠出来的。 721 | 722 | ## 总结 723 | 724 | - 文章系统的解读了 YYCache 源码,相信可以让各位读者对 YYCache 的整体架构有一个清晰的认识。 725 | - 文章结合作者 [YYCache 设计思路](https://blog.ibireme.com/2015/10/26/yycache/) 中的内容对 YYCache 具体功能点实现源码做了深入剖析,再用我自己的理解表述出来,希望可以对读者理解 YYCache 中具体功能的实现提供帮助。 726 | - 根据我自己的源码理解,把我认为做的不错的提升性能的源码细节单独拎出来做出详细分析。 727 | - 总结归纳出“一个优秀缓存需要具备哪些特质?”这一问题的答案,希望大家在面试中如果被问及“如何设计一个缓存”这类问题时可以游刃有余。额,至少可以为大家提供一些回答思路,抛砖引玉(笑)。 728 | 729 | 文章写得比较用心(是我个人的原创文章,转载请注明 [https://lision.me/](https://lision.me/)),如果发现错误会优先在我的 [个人博客](https://lision.me/) 中更新。如果有任何问题欢迎在我的微博 [@Lision](https://weibo.com/lisioncode) 联系我~ 730 | 731 | 希望我的文章可以为你带来价值~ 732 | -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yycache/cache-hit-ratio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yycache/cache-hit-ratio.png -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yycache/good-cache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yycache/good-cache.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yycache/how-to-design-a-good-cache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yycache/how-to-design-a-good-cache.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yycache/lock_benchmark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yycache/lock_benchmark.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yycache/performance-yydiskcache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yycache/performance-yydiskcache.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yycache/performance-yymemorycache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yycache/performance-yymemorycache.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yycache/yycache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yycache/yycache.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yycache/yydiskcache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yycache/yydiskcache.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yycache/yymemorycache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yycache/yymemorycache.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yyimage.md: -------------------------------------------------------------------------------- 1 | # YYImage 设计思路,实现细节剖析 2 | 3 | ![](yyimage/yyimage_h.jpg) 4 | 5 | ## 前言 6 | 7 | 图片的历史早于文字,是最原始的信息传递方式。[六书](https://zh.wikipedia.org/wiki/%E5%85%AD%E6%9B%B8)中的象形文构造思想就是用文字的线条或笔画,把要表达物体的外形特征,具体地勾画出来。 8 | 9 | > [许慎](https://zh.wikipedia.org/wiki/%E8%A8%B1%E6%85%8E)《[说文解字](https://zh.wikipedia.org/wiki/%E8%AF%B4%E6%96%87%E8%A7%A3%E5%AD%97)》云:“象形者,画成其物,随体诘诎,日、月是也。” 10 | 11 | 现代社会的信息传递中,图片仍然是不可或缺的一环,不论是报纸、杂志、漫画等实体刊物还是生活中超市地铁广告活动,都会有专门的配图抓人眼球。 12 | 13 | 在移动端 App 中,图片通常占据着重要的视觉空间,作为 iOS 开发来讲,所有的 App 都有精心设计的 AppIcon 陈列在 SpringBoard 中,打开任意一款主流 App 都少不了琳琅满目的图片搭配。 14 | 15 | [YYImage](https://github.com/ibireme/YYImage) 是一款功能强大的 iOS 图像框架(该项目是 [YYKit](https://github.com/ibireme/YYKit) 组件之一),支持目前市场上所有主流的图片格式的显示与编/解码,并且提供高效的动态内存缓存管理,以保证高性能低内存的动画播放。 16 | 17 | YYKit 的作者 [@ibireme](https://weibo.com/239801242) 对于 iOS 图片处理写有两篇非常不错的文章,推荐各位读者在阅读本文之前查阅。 18 | 19 | - [移动端图片格式调研](https://blog.ibireme.com/2015/11/02/mobile_image_benchmark/) 20 | - [iOS 处理图片的一些小 Tip](https://blog.ibireme.com/2015/11/02/ios_image_tips/) 21 | 22 | 本文引用代码均为 YYImage v1.0.4 版本源码,文章旨在剖析 YYImage 的架构思想以及设计思路并对笔者在阅读源码过程中发现的有趣实现细节探究分享,不会逐行翻译源码,建议对源码实现感兴趣的同学结合 YYImage v1.0.4 版本源码食用本文~ 23 | 24 | ![](yyimage/xiaomai.gif) 25 | 26 | ## 索引 27 | 28 | - YYImage 简介 29 | - YYImage, YYFrameImage, YYSpriteSheetImage 30 | - YYAnimatedImageView 31 | - YYImageCoder 32 | - 总结 33 | - 扩展阅读 34 | 35 | ## YYImage 简介 36 | 37 | ![](yyimage/yyimage.jpg) 38 | 39 | YYImage 是一款功能强大的 iOS 图像框架,支持当前市场主流的静/动态图像编/解码与动态图像的动画播放显示,其具有以下特性: 40 | 41 | - 支持以下类型动画图像的播放/编码/解码: WebP, APNG, GIF。 42 | - 支持以下类型静态图像的显示/编码/解码: WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。 43 | - 支持以下类型图片的渐进式/逐行扫描/隔行扫描解码: PNG, GIF, JPEG, BMP。 44 | - 支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画。 45 | - 高效的动态内存缓存管理,以保证高性能低内存的动画播放。 46 | - 完全兼容 UIImage 和 UIImageView,使用方便。 47 | - 保留可扩展的接口,以支持自定义动画。 48 | - 每个类和方法都有完善的文档注释。 49 | 50 | ### YYImage 架构分析 51 | 52 | 通过 YYImage 源码可以按照其与 UIKit 的对应关系划分为三个层级: 53 | 54 | | 层级 | UIKit | YYImage | 55 | | :---: | :---: | :---: | 56 | | 图像层 | UIImage | YYImage, YYFrameImage, YYSpriteSheetImage | 57 | | 视图层 | UIImageView | YYAnimatedImageView | 58 | | 编/解码层 | ImageIO.framework | YYImageCoder | 59 | 60 | - 图像层,把不同类型的图像信息封装成类并提供初始化和其他便捷接口。 61 | - 视图层,负责图像层内容的显示(包含动态图像的动画播放)工作。 62 | - 编/解码层,提供图像底层支持,使整个框架得以支持市场主流的图片格式。 63 | 64 | > Note: [ImageIO.framework](https://developer.apple.com/documentation/imageio) 是 iOS 底层实现的图片编/解码库,负责管理颜色和访问图像元数据。其内部的实现使用了第三方编/解码库(如 libpng 等)并对第三方库进行调整优化。除此之外,iOS 还专门针对 JPEG 的编/解码开发了 AppleJPEG.framework,实现了性能更高的硬编码和硬解码。 65 | 66 | ![](yyimage/yyimage_struct.png) 67 | 68 | ## YYImage, YYFrameImage, YYSpriteSheetImage 69 | 70 | 先来介绍 YYImage 库中图像层的三个类,它们分别是: 71 | 72 | - YYImage 73 | - YYFrameImage 74 | - YYSpriteSheetImage 75 | 76 | ### YYImage 77 | 78 | YYImage 是一个显示动态图片数据的高级别类,其继承自 UIImage 并对 UIImage 做了扩展以支持 WebP,APNG 和 GIF 格式的图片解码。它还支持 NSCoding 协议可以对多帧图像数据进行 archive 和 unarchive 操作。 79 | 80 | ``` obj-c 81 | @interface YYImage : UIImage 82 | 83 | + (nullable YYImage *)imageNamed:(NSString *)name; // 不同于 UIImage,此方法无缓存 84 | + (nullable YYImage *)imageWithContentsOfFile:(NSString *)path; 85 | + (nullable YYImage *)imageWithData:(NSData *)data; 86 | + (nullable YYImage *)imageWithData:(NSData *)data scale:(CGFloat)scale; 87 | 88 | @property (nonatomic, readonly) YYImageType animatedImageType; // 图像数据类型 89 | @property (nullable, nonatomic, readonly) NSData *animatedImageData; // 动态图像的元数据 90 | @property (nonatomic, readonly) NSUInteger animatedImageMemorySize; // 多帧图像内存占用量 91 | @property (nonatomic) BOOL preloadAllAnimatedImageFrames; // 预加载所有帧(到内存) 92 | 93 | @end 94 | ``` 95 | 96 | YYImage 提供了类似 UIImage 的初始化方法,公开了一些属性便于我们检测和控制其内存使用。 97 | 98 | 值得一提的是 YYImage 的 `imageNamed:` 初始化方法并不支持缓存。因为其 `imageNamed:` 内部实现并不同于 UIImage 的 `imageNamed:` 方法,YYImage 中的实现流程如下: 99 | 100 | - 推测出给定图像资源路径 101 | - 拿到路径中的图像数据(NSData) 102 | - 调用 YYImage 的 `initWithData:scale:` 方法初始化 103 | 104 | YYImage 的私有变量部分也比较简单,相信大家可以根据上面暴露出的属性和接口猜得到哈。 105 | 106 | ``` obj-c 107 | @implementation YYImage { 108 | YYImageDecoder *_decoder; // 解码器 109 | NSArray *_preloadedFrames; // 预加载的图像帧 110 | dispatch_semaphore_t _preloadedLock; // 预加载锁 111 | NSUInteger _bytesPerFrame; // 内存占用量 112 | } 113 | ``` 114 | 115 | 其内部有一把锁 `dispatch_semaphore_t`,我们知道 `dispatch_semaphore_t` 当信号量为 1 时可以当做锁来使用,在不阻塞时其作为锁的效率非常高。这里使用 `_preloadedLock` 的主要目的是保证 `_preloadedFrames` 的读写,由于 `_preloadedFrames` 的读写过程是在内存中完成的,操作耗时不会太多,所以不会长时间阻塞,这种情况使用 `dispatch_semaphore_t` 非常合适。 116 | 117 | 嘛~ `_preloadedFrames` 对应 `preloadAllAnimatedImageFrames` 属性,开启预加载所有帧到内存的话,`_preloadedFrames` 作为一个数组会保存所有帧的图像。`_bytesPerFrame` 则对应 `animatedImageMemorySize` 属性,在初始化 YYImage 时,如果帧总数超过 1 则会计算 `_bytesPerFrame` 的大小。 118 | 119 | ``` obj-c 120 | if (decoder.frameCount > 1) { 121 | _decoder = decoder; 122 | _bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage); 123 | _animatedImageMemorySize = _bytesPerFrame * decoder.frameCount; 124 | } 125 | ``` 126 | 127 | 其实 YYImage 中还有一些实现也比较有趣,比如 `animatedImageDurationAtIndex:` 的实现中如果取到 <= 10 ms 的时长会替换为 100 ms,并在 [注释](https://github.com/ibireme/YYImage/blob/master/YYImage/YYImage.m#L246) 中解释了为什么(一定要点进去看啊,笑~)。 128 | 129 | ### YYFrameImage 130 | 131 | YYFrameImage 是专门用来显示基于帧的动画图像类,其也是 UIImage 的子类。YYFrameImage 仅支持系统图片格式例如 png 和 jpeg。 132 | 133 | > Note: 使用 YYFrameImage 显示动画图像同样要基于 YYAnimatedImageView 播放。 134 | 135 | ``` obj-c 136 | @interface YYFrameImage : UIImage 137 | 138 | - (nullable instancetype)initWithImagePaths:(NSArray *)paths 139 | oneFrameDuration:(NSTimeInterval)oneFrameDuration 140 | loopCount:(NSUInteger)loopCount; 141 | - (nullable instancetype)initWithImagePaths:(NSArray *)paths 142 | frameDurations:(NSArray *)frameDurations 143 | loopCount:(NSUInteger)loopCount; 144 | - (nullable instancetype)initWithImageDataArray:(NSArray *)dataArray 145 | oneFrameDuration:(NSTimeInterval)oneFrameDuration 146 | loopCount:(NSUInteger)loopCount; 147 | - (nullable instancetype)initWithImageDataArray:(NSArray *)dataArray 148 | frameDurations:(NSArray *)frameDurations 149 | loopCount:(NSUInteger)loopCount; 150 | 151 | @end 152 | ``` 153 | 154 | YYFrameImage 可以把静态图片类型如 png 和 jpeg 格式的静态图像用帧切换的方式以动态图片的形式显示,并且提供了 4 个常用的初始化方法方便我们使用。 155 | 156 | YYFrameImage 内部有一些基本的变量分别对应于其暴露的 4 个常用初始化接口: 157 | 158 | ``` obj-c 159 | @implementation YYFrameImage { 160 | NSUInteger _loopCount; 161 | NSUInteger _oneFrameBytes; 162 | NSArray *_imagePaths; 163 | NSArray *_imageDatas; 164 | NSArray *_frameDurations; 165 | } 166 | ``` 167 | 168 | YYFrameImage 的实现代码非常简单,初始化方法大致可以分为以下步骤: 169 | 170 | - 入参校验 171 | - 根据入参取到首张图片 172 | - 用首图初始化 `_oneFrameBytes` ,如入参初始化 `_imageDatas` ,`_frameDurations` 和 `_loopCount` 173 | - 用 `UIImage` 的 `initWithCGImage:scale:orientation:` 初始化并返回初始化结果 174 | 175 | ### YYSpriteSheetImage 176 | 177 | ![](yyimage/ss_wukong.png) 178 | 179 | YYSpriteSheetImage 是用来做 Spritesheet 动画显示的图像类,它也是 UIImage 的子类。 180 | 181 | 关于 Spritesheet 可能做过游戏开发或者以前鼓捣过简单网页游戏 Demo 的同学会很熟悉,其动画原理是把一个动画过程分解为多个动画帧,按照顺序将这些动画帧排布在一张大的画布中,播放动画时只需要按照每一帧图像的尺寸大小以及对应索引去画布中提取对应的帧替换显示以达到人眼判定动画的效果,点击 [ 182 | An Introduction to Spritesheet Animation](https://gamedevelopment.tutsplus.com/tutorials/an-introduction-to-spritesheet-animation--gamedev-13099) 或者 [What is a sprite sheet?](https://www.codeandweb.com/what-is-a-sprite-sheet) 了解更多关于 Spritesheet 动画的信息。 183 | 184 | > Note: 关于 SpriteSheet 素材的制作有一款工具 [SpriteSheetMaker](https://www.codeandweb.com/sprite-sheet-maker) 推荐使用。 185 | 186 | ``` obj-c 187 | @interface YYSpriteSheetImage : UIImage 188 | 189 | // 初始化方法,这个第一次接触 Spritesheet 的同学可能会觉得比较繁琐 190 | - (nullable instancetype)initWithSpriteSheetImage:(UIImage *)image 191 | contentRects:(NSArray *)contentRects 192 | frameDurations:(NSArray *)frameDurations 193 | loopCount:(NSUInteger)loopCount; 194 | 195 | @property (nonatomic, readonly) NSArray *contentRects; // 帧位置信息 196 | @property (nonatomic, readonly) NSArray *frameDurations; // 帧持续时长 197 | @property (nonatomic, readonly) NSUInteger loopCount; // 循环数 198 | 199 | // 根据索引找到对应帧 CALayer 的位置 200 | - (CGRect)contentsRectForCALayerAtIndex:(NSUInteger)index; 201 | 202 | @end 203 | ``` 204 | 205 | 其中初始化方法的入参为 SpriteSheet 画布(包含所有动画帧的大图)image,每一帧的位置 contentRects,每一帧对应的持续显示时间 frameDurations,循环次数 loopCount,初始化示例在 YYImage 源文件 [YYSpriteSheetImage.h](https://github.com/ibireme/YYImage/blob/master/YYImage/YYSpriteSheetImage.h#L32) 注释中有写。 206 | 207 | > Note: 下文中要讲的 YYAnimatedImageView 中定义了 YYAnimatedImage 协议,这个协议中有一个可选方法 `animatedImageContentsRectAtIndex:` 就是为 YYSpriteSheetImage 量身打造的。 208 | 209 | 这里需要提一下 `contentsRectForCALayerAtIndex:` 接口会根据索引找到对应帧的 CALayer 位置,该接口返回一个由 0.0~1.0 之间的数值组成的图层定位 LayerRect,如果在查找位置过程中发现异常则返回 CGRectMake(0, 0, 1, 1),其内部实现大体步骤: 210 | 211 | - 校验入参索引是否超过 SpriteSheet 分割帧总数,超过返回 CGRectMake(0, 0, 1, 1) 212 | - 没超过则通过 YYAnimatedImage 协议的 `animatedImageContentsRectAtIndex:` 方法找到对应索引的真实位置 RealRect 213 | - 通过真实位置 RealRect 与 SpriteSheet 画布的比算错 0.0~1.0 之间的值,得到指定索引帧的逻辑定位 LogicRect 214 | - 通过 `CGRectIntersection` 方法计算逻辑定位 LogicRect 与 CGRectMake(0, 0, 1, 1) 的交集,确保逻辑定位没有超出画布的部分 215 | - 将处理后的逻辑定位 LogicRect 作为图层定位 LayerRect 返回 216 | 217 | 返回的 LayerRect 作为对应索引帧的画布内相对位置存在,结合画布就可以定位到对应帧图像的具体尺寸和位置。 218 | 219 | ## YYAnimatedImageView 220 | 221 | ![](yyimage/blood_wheel_eye.jpeg) 222 | 223 | 人眼中呈现的动画是由一幅幅内容连贯的图像以较短时间按顺序替换形成的,所以要显示动画只需要知道动画顺序中每一帧图像以及对应的显示时间等信息即可。YYImage 中对应于 UIImage 层级的内容(YYImage, YYFrameImage, YYSpriteSheetImage)在上文已经介绍过了,虽然它们之间存在内容和形式上的差异,但是对于人眼动画呈现的原理却是不变的。 224 | 225 | YYAnimatedImageView 是 YYImage 的重要组成,它是 UIImageView 的子类,负责 YYImage 图像层中不同的图像类的视图显示(包含动态图像的动画播放),其内部包含 YYAnimatedImage 协议以及 YYAnimatedImageView 自身两部分。 226 | 227 | ### YYAnimatedImage 协议 228 | 229 | 上文提到不论是 YYImage, YYFrameImage, YYSpriteSheetImage 还是以后可能会扩展的图像类,虽然它们之间存在内容和形式上的差异,但是对于人眼动画呈现的原理却是不变的。 230 | 231 | YYAnimatedImage 协议就是在不影响原来图像类的情况下把不同图像类之间的共性找出来(求同存异?笑),以统一化的接口将人眼动画呈现所需的基本信息输出给 YYAnimatedImageView 使用的协议。 232 | 233 | > Note: 作为图像类须遵循 YYAnimatedImage 协议以便可以使用 YYAnimatedImageView 播放动画。 234 | 235 | ``` obj-c 236 | @protocol YYAnimatedImage 237 | 238 | @required 239 | // 动画帧总数 240 | - (NSUInteger)animatedImageFrameCount; 241 | // 动画循环次数,0 表示无限循环 242 | - (NSUInteger)animatedImageLoopCount; 243 | // 每帧字节数(在内存中),可能用于优化内存缓冲区大小 244 | - (NSUInteger)animatedImageBytesPerFrame; 245 | // 返回给定特殊索引对应的帧图像,这个方法可能在异步线程中调用 246 | - (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index; 247 | // 返回给定特殊索引对应的帧图像对应的显示持续时长 248 | - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index; 249 | 250 | @optional 251 | // 针对 Spritesheet 动画的方法,用于显示某一帧图像在 Spritesheet 画布中的位置 252 | - (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index; 253 | 254 | @end 255 | ``` 256 | 257 | > 上文提到过可选实现接口 `animatedImageContentsRectAtIndex:` 是专为 Spritesheet 动画设计的。 258 | 259 | 像这样规定一个协议,使不相关的类遵循此协议拥有统一的功能接口方便另一个类调用的设计思想我们在自己日常项目的开发过程中很多场景都可以用到,例如可以封装一个 TableView,设计一个 TableViewCell 协议,让所有 TableViewCell 都实现这个协议以拥有统一的功能接口,然后我们封装的 TableView 类就可以统一的使用这些 TableViewCell 显示数据啦,省去了反复写相同功能 UITableView 的劳动力(实际应用场景很多,这里只是简单举例,抛砖引玉)。 260 | 261 | ### YYAnimatedImageView 262 | 263 | 上文提到过 YYAnimatedImageView 作为 YYImage 框架中的图片视图层,上接图像层,下启编/解码底层,是枢纽一般的存在(承上启下啊有木有?),我们需要重点研究其内部实现: 264 | 265 | ``` obj-c 266 | @interface YYAnimatedImageView : UIImageView 267 | 268 | // 如果 image 为多帧组成时,自动赋值为 YES,可以在显示和隐藏时自动播放和停止动画 269 | @property (nonatomic) BOOL autoPlayAnimatedImage; 270 | // 当前显示的帧(从 0 起始),设置新值后会立即显示对应帧,如果新值无效则此方法无效 271 | @property (nonatomic) NSUInteger currentAnimatedImageIndex; 272 | // 当前是否在播放动画 273 | @property (nonatomic, readonly) BOOL currentIsPlayingAnimation; 274 | // 动画定时器所在的 runloop mode,默认为 NSRunLoopCommonModes,关乎动画定时器的触发 275 | @property (nonatomic, copy) NSString *runloopMode; 276 | // 内部缓存区的最大值(in bytes),默认为 0(动态),如果有值将会把缓存区限制为值大小,当收到内存警告或者 App 进入后台时,缓存区将会立即释放并且在适时的时候回复原状 277 | @property (nonatomic) NSUInteger maxBufferSize; 278 | 279 | @end 280 | ``` 281 | 282 | 额...出乎意料的简单呢~ 只有一些属性暴露出来以便我们在使用过程中实时查看动画的播放状态以及内存使用情况。笔者看源码总结出一条经验,即**如果某个组件在库中占据重要地位,其 .h 文件中暴露的内容越是简单,其 .m 内部实现就越是复杂**。 283 | 284 | 通过 `runloopMode` 属性大家用猜的也应该可以猜出 YYAnimatedImageView 内部实现动画的原理离不开 RunLoop,而且极有可能是用定时器 NSTimer 或者 CADisplayLink 实现的。下面我们来对 YYAnimatedImageView 的实现剖析,验证一下我们刚才的猜想。 285 | 286 | #### YYAnimatedImageView 的实现剖析 287 | 288 | YYAnimatedImageView 内部实现源码很有趣,有很多值得分享的地方。不过为了不把文章写成 MarkDown 编辑器文(笑~)笔者不会逐行翻译源码。读者如果想要知道实现的细节建议结合文章去翻阅源码。相信有了文章梳理的思路源码看起来应该不会有太大的困难,文章还是重在传播实现思想和一些值得分享的技巧。 289 | 290 | 我们先简单看一下 YYAnimatedImageView 的内部结构,方便后面分析实现思路时大家脑中对 YYAnimatedImageView 的结构提前有一个大概的认识。 291 | 292 | ``` obj-c 293 | @interface YYAnimatedImageView() { 294 | @package 295 | UIImage *_curAnimatedImage; ///< 当前图像 296 | 297 | dispatch_once_t _onceToken; ///< 用于确保初始化代码只执行一次 298 | dispatch_semaphore_t _lock; ///< 信号量锁(用于 _buffer) 299 | NSOperationQueue *_requestQueue; ///< 图片请求队列,串行 300 | 301 | CADisplayLink *_link; ///< 帧转换 302 | NSTimeInterval _time; ///< 上一帧之后的时间 303 | 304 | UIImage *_curFrame; ///< 当前帧 305 | NSUInteger _curIndex; ///< 当前帧索引 306 | NSUInteger _totalFrameCount; ///< 帧总数 307 | 308 | BOOL _loopEnd; ///< 是否在循环末尾 309 | NSUInteger _curLoop; ///< 当前循环次数 310 | NSUInteger _totalLoop; ///< 总循环次数, 0 表示无穷 311 | 312 | NSMutableDictionary *_buffer; ///< 帧缓冲区 313 | BOOL _bufferMiss; ///< 是否丢帧,在上面 _link 定时执行的 step 函数中从帧缓冲区读取下一帧图片时如果没读到,则视为丢帧 314 | NSUInteger _maxBufferCount; ///< 最大缓冲计数 315 | NSInteger _incrBufferCount; ///< 当前允许的缓存区计数(将逐步增加) 316 | 317 | CGRect _curContentsRect; ///< 针对 YYSpriteSheetImage 318 | BOOL _curImageHasContentsRect; ///< 图像类是否实现了 animatedImageContentsRectAtIndex: 接口 319 | } 320 | @property (nonatomic, readwrite) BOOL currentIsPlayingAnimation; 321 | - (void)calcMaxBufferCount; // 动态调节缓冲区最大限制 _maxBufferCount 322 | @end 323 | ``` 324 | 325 | 可以看到 YYAnimatedImageView 内部结构比 .h 中暴露的属性要复杂的多,而 `CADisplayLink *_link` 属性也证实了我们之前关于 .h 中 `runloopMode` 属性的猜想。 326 | 327 | YYAnimatedImageView 内部的初始化没什么特别之处,初始化函数中会设置图片,当判定图片有更改时会依照下面 4 步去处理: 328 | 329 | - 改变图片 330 | - 重置动画 331 | - 初始化动画参数 332 | - 重绘 333 | 334 | > Note: 这样可以保证 YYAnimatedImageView 的图片更改时都会执行上面的步骤为新的图片初始化配套的新动画参数并且重绘,而重置动画实现中会使用到上面的 `dispatch_once_t _onceToken;` 以确保某些内部变量的创建以及对 App 内存警告和进入后台的通知观察代码只执行一次。 335 | 336 | YYAnimatedImageView 使图片动起来是依靠 `CADisplayLink *_link;` 变量切换帧图像,其内部的实现逻辑可以简单理解为: 337 | 338 | - 根据当前帧索引推出下一帧索引 339 | - 使用下一帧索引去帧缓冲区尝试获取对应帧图像 340 | - 如果找到对应帧图像则使用其重绘 341 | - 如果没找到则根据条件向图片请求队列加入请求操作(向图片缓冲区录入之后的帧图像数据) 342 | 343 | 嘛~ 这里面有一些值得一提的实现细节哈! 344 | 345 | > - YYAnimatedImageView 实现中当 `_curIndex` 即当前帧索引修改时在修改代码前后加入了 `willChangeValueForKey:` 与 `didChangeValueForKey:` 方法以支持 KVO 346 | > - 对帧缓冲区 `_buffer` 的操作都使用 `_lock` 上锁 347 | > - 通过将图片请求队列 `_requestQueue` 的 `maxConcurrentOperationCount` 设置为 1 使图片请求队列成为串行队列(最大并发数为 1) 348 | > - 图片请求队列中加入的操作均为 `_YYAnimatedImageViewFetchOperation` 349 | > - 为了避免使用 `CADisplayLink` 可能造成的循环引用设计了 `_YYImageWeakProxy` 350 | 351 | 先看一下 `_YYAnimatedImageViewFetchOperation` 的源码: 352 | 353 | ``` obj-c 354 | @interface _YYAnimatedImageViewFetchOperation : NSOperation 355 | @property (nonatomic, weak) YYAnimatedImageView *view; 356 | @property (nonatomic, assign) NSUInteger nextIndex; 357 | @property (nonatomic, strong) UIImage *curImage; 358 | @end 359 | 360 | @implementation _YYAnimatedImageViewFetchOperation 361 | - (void)main {//...} 362 | @end 363 | ``` 364 | 365 | `_YYAnimatedImageViewFetchOperation` 继承自 NSOperation 类,是自定义操作类,作者将其操作内容实现写在了 `main` 中,代码太长而且我觉得贴出来不仅不会帮助读者理解反而会因为片面的源码实现影响读者对 YYAnimatedImageView 的整体实现思路理解(因为大量贴源码会使文章生涩很多,而且会把读者注意力转移到某一个实现),这里简单描述一下 `main` 函数内部实现逻辑: 366 | 367 | - 判断帧缓冲区大小 368 | - 扫描下一帧以及当前允许缓冲范围内之后的帧图片 369 | - 如果发现丢失的帧则尝试重新获取帧图像并加入到帧缓冲 370 | 371 | 嘛~ 不贴源码归不贴源码,该注意的细节还是需要列出来的(笑)。 372 | 373 | > - 操作中对于 `view` 缓冲区的操作也都上了锁 374 | > - 操作由于是放入图片请求队列中进行的,内部有对 `isCancelled` 做判断,如果操作已经被取消(发生在更改图片、停止动画、手动更改当前帧、收到内存警告或 App 进入后台等)则需要及时跳出 375 | > - 对于新的线程优先级只在 `main` 方法范围内有效,所以推荐把操作的实现放在 `main` 中而非 `start`(如需覆盖 start 方法时,需要关注 `isExecuting` 和 `isFinished` 两个 key paths) 376 | 377 | YYAnimatedImageView 内部设计了 `_YYImageWeakProxy` 来避免使用 NSTimer 或者 CADisplayLink 可能造成的循环引用问题,`_YYImageWeakProxy` 内部实现也比较简单,继承自 NSProxy,关于 NSProxy 可以查看[官方文档](https://developer.apple.com/documentation/foundation/nsproxy)以了解更多。 378 | 379 | ``` obj-c 380 | @interface _YYImageWeakProxy : NSProxy 381 | @property (nonatomic, weak, readonly) id target; 382 | - (instancetype)initWithTarget:(id)target; 383 | + (instancetype)proxyWithTarget:(id)target; 384 | @end 385 | 386 | @implementation _YYImageWeakProxy 387 | // ... 388 | - (id)forwardingTargetForSelector:(SEL)selector { 389 | return _target; 390 | } 391 | - (void)forwardInvocation:(NSInvocation *)invocation { 392 | void *null = NULL; 393 | [invocation setReturnValue:&null]; 394 | } 395 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { 396 | return [NSObject instanceMethodSignatureForSelector:@selector(init)]; 397 | } 398 | // ... 399 | @end 400 | ``` 401 | 402 | 上面贴出的源码省略了比较基础的实现部分,`_YYImageWeakProxy` 内部弱引用一个对象 target,对于 `_YYImageWeakProxy` 的一些基本操作包含 `hash` 和 `isEqual` 这些统统都转到 target 上,并且使用 `forwardingTargetForSelector:` 消息重定向将不能响应的运行时消息也重定向给 target 来响应。 403 | 404 | Emmmmm..那么问题来了,既然都消息重定向给 target 了还要消息转发干嘛?因为要避免循环引用问题所以对 target 使用弱引用,期间无法保证 target 一定存在,所以 `forwardingTargetForSelector:` 方法可能返回 nil,接着在 Runtime 消息转发中借用 init 消息返回空以“吞掉”异常。 405 | 406 | > Note: 消息转发产生的开销要比动态方法解析和消息重定向大。 407 | 408 | ## YYImageCoder 409 | 410 | ![image_coder](/content/images/2017/12/image_coder.jpg) 411 | 412 | YYImageCoder 作为 YYImage 的编/解码器,对应于 iOS 中的 ImageIO.framework 图片编/解码库,正是因为有了 YYImageCoder 的存在,YYImage 才得以支持如此多的图片格式,所以说 YYImageCoder 是 YYImage 的底层核心。 413 | 414 | YYImageCoder 内部定义了许多 YYImage 中用到的核心数据结构: 415 | 416 | - YYImageType,所有的支持的图片格式做了枚举定义 417 | - YYImageDisposeMethod,指定在画布上渲染下一个帧之前如何处理当前帧所使用的区域方法 418 | - YYImageBlendOperation,指定当前帧的透明像素如何与前一个画布的透明像素混合操作 419 | - YYImageFrame,一帧图像数据 420 | - YYImageEncoder,图像编码器 421 | - YYImageDecoder,图像解码器 422 | - UIImage+YYImageCoder,UIImage 的分类,里面提供了一些方便使用的方法 423 | 424 | 其中 YYImageFrame 是对一帧图像数据的封装,便于在 YYImageCoder 编/解码过程中使用。 425 | 426 | YYImageCoder 内部图像编码器 YYImageEncoder 和图像解码器 YYImageDecoder 其实是分开来的,我们下面分别对它们做分析。 427 | 428 | ### YYImageEncoder 429 | 430 | 先来讲一下 YYImageEncoder,其在 YYImageCoder 中担任编码器的角色。 431 | 432 | ``` obj-c 433 | @interface YYImageEncoder : NSObject 434 | 435 | @property (nonatomic, readonly) YYImageType type; ///< 图像类型 436 | @property (nonatomic) NSUInteger loopCount; ///< 循环次数,0 无限循环,仅适用于 GIF/APNG/WebP 格式 437 | @property (nonatomic) BOOL lossless; ///< 无损标记,仅适用于 WebP. 438 | @property (nonatomic) CGFloat quality; ///< 压缩质量,0.0~1.0,仅适用于 JPG/JP2/WebP. 439 | 440 | // 禁止适用 init、new 初始化编码器(我没忘记我说过这些编码技巧会在之后统一写一篇文章汇总) 441 | - (instancetype)init UNAVAILABLE_ATTRIBUTE; 442 | + (instancetype)new UNAVAILABLE_ATTRIBUTE; 443 | 444 | // 根据给定图片类型创建编码器 445 | - (nullable instancetype)initWithType:(YYImageType)type NS_DESIGNATED_INITIALIZER; 446 | // 添加图像 447 | - (void)addImage:(UIImage *)image duration:(NSTimeInterval)duration; 448 | // 添加图像数据 449 | - (void)addImageWithData:(NSData *)data duration:(NSTimeInterval)duration; 450 | // 添加文件路径 451 | - (void)addImageWithFile:(NSString *)path duration:(NSTimeInterval)duration; 452 | // 开始图像编码并尝试返回编码后的数据 453 | - (nullable NSData *)encode; 454 | // 编码并将得到的数据保存到给定路径文件中 455 | - (BOOL)encodeToFile:(NSString *)path; 456 | // 便捷方法,对一个单帧图像编码 457 | + (nullable NSData *)encodeImage:(UIImage *)image type:(YYImageType)type quality:(CGFloat)quality; 458 | // 便捷方法,从解码器中编码图像数据 459 | + (nullable NSData *)encodeImageWithDecoder:(YYImageDecoder *)decoder type:(YYImageType)type quality:(CGFloat)quality; 460 | 461 | @end 462 | ``` 463 | 464 | 可以看到 YYImageEncoder 内部的一些属性和接口都比较基本,关于其内部实现我们需要先看一下私有变量: 465 | 466 | ``` obj-c 467 | @implementation YYImageEncoder { 468 | NSMutableArray *_images; // 已添加到编码器的图片(数组) 469 | NSMutableArray *_durations; // 对应的图片帧显示持续时长(数组) 470 | } 471 | ``` 472 | 473 | #### YYImageEncoder 的实现思路 474 | 475 | YYImageEncoder 的初始化部分没有多复杂,根据图片的类型按照编码最优的参数做初始化而已。关于 YYImageEncoder 对于图片的编码工作,其实作者根据要支持的图片类型和对应图片类型的编码方式做了底层封装,再根据当前图片的类型选择对应的底层编码方法执行。 476 | 477 | 关于不同图片类型的图片编码格式可以查阅本文文末的扩展阅读章节,结合扩展阅读的内容查阅 YYImage 这部分源码可以理解作者对于底层图片格式信息的结构封装以及编/解码操作具体实现。 478 | 479 | 关于 YYImageEncoder 的一些简单使用示例可以查看 [YYImageCoder.h](https://github.com/ibireme/YYImage/blob/master/YYImage/YYImageCoder.h#L216) 了解。 480 | 481 | ### YYImageDecoder 482 | 483 | YYImageDecoder 在 YYImageCoder 中担任解码器的角色,其与上述 YYImageEncoder 对应,一个负责图像编码一个负责图像解码,不过 YYImageDecoder 的实现比 YYImageEncoder 更为复杂。 484 | 485 | ``` obj-c 486 | @interface YYImageDecoder : NSObject 487 | 488 | @property (nullable, nonatomic, readonly) NSData *data; ///< 图像数据 489 | @property (nonatomic, readonly) YYImageType type; ///< 图像数据类型 490 | @property (nonatomic, readonly) CGFloat scale; ///< 图像大小 491 | @property (nonatomic, readonly) NSUInteger frameCount; ///< 图像帧数量 492 | @property (nonatomic, readonly) NSUInteger loopCount; ///< 图像循环次数,0 无限循环 493 | @property (nonatomic, readonly) NSUInteger width; ///< 图像画布宽度 494 | @property (nonatomic, readonly) NSUInteger height; ///< 图像画布高度 495 | @property (nonatomic, readonly, getter=isFinalized) BOOL finalized; 496 | 497 | // 创建一个图像解码器 498 | - (instancetype)initWithScale:(CGFloat)scale NS_DESIGNATED_INITIALIZER; 499 | // 用新数据增量更新图像 500 | - (BOOL)updateData:(nullable NSData *)data final:(BOOL)final; 501 | // 方便用一个特殊的数据创建对应的解码器 502 | + (nullable instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale; 503 | // 解码并返回给定索引对应的帧数据 504 | - (nullable YYImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay; 505 | // 返回给定索引对应的帧持续显示时长 506 | - (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index; 507 | // 返回给定索引对应帧的属性信息,去 ImageIO.framework 的 "CGImageProperties.h" 文件中了解更多 508 | - (nullable NSDictionary *)framePropertiesAtIndex:(NSUInteger)index; 509 | // 返回图片的属性信息,去 ImageIO.framework 的 "CGImageProperties.h" 文件中了解更多 510 | - (nullable NSDictionary *)imageProperties; 511 | 512 | @end 513 | ``` 514 | 515 | 可以看到 YYImageDecoder 暴露了一些关于解码图像的属性并提供了初始化解码器方法、图像解码方法以及访问图像帧信息的方法。不过上文也说过 YYImageDecoder 的实现比较复杂,我们接着看一下其内部变量结构: 516 | 517 | ``` obj-c 518 | @implementation YYImageDecoder { 519 | pthread_mutex_t _lock; // 递归锁 520 | 521 | BOOL _sourceTypeDetected; // 是否推测图像源类型 522 | CGImageSourceRef _source; // 图像源 523 | yy_png_info *_apngSource; // 如果判定图像为 YYImageTypePNG 则会以 APNG 更新图像源 524 | #if YYIMAGE_WEBP_ENABLED 525 | WebPDemuxer *_webpSource; // 如果判定图像为 YYImageTypeWebP 则会议 WebP 更新图像源 526 | #endif 527 | 528 | UIImageOrientation _orientation; // 绘制方向 529 | dispatch_semaphore_t _framesLock; // 针对于图像帧的锁 530 | NSArray *_frames; ///< Array<_YYImageDecoderFrame *>, without image 531 | BOOL _needBlend; // 是否需要混合 532 | NSUInteger _blendFrameIndex; // 从帧索引混合到当前帧 533 | CGContextRef _blendCanvas; // 混合画布 534 | } 535 | ``` 536 | 537 | `_YYImageDecoderFrame` 继承自 YYImageFrame 类作为 YYImageCoder 图像解码器 YYImageDecoder 使用的内部框架类存在,是对于一帧图像的数据封装提供了便于编/解码时需要访问的数据。 538 | 539 | #### YYImageDecoder 内锁的选择 540 | 541 | 可以看到作者在 YYImageDecoder 内部使用了两种锁: 542 | 543 | - `pthread_mutex_t _lock;` 544 | - `dispatch_semaphore_t _framesLock;` 545 | 546 | `pthread_mutex_t` 在解码器初始化过程中被以 `PTHREAD_MUTEX_RECURSIVE` 类型设置为了递归锁。 547 | 548 | ``` obj-c 549 | pthread_mutexattr_t attr; 550 | pthread_mutexattr_init (&attr); 551 | pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); 552 | pthread_mutex_init (&_lock, &attr); 553 | pthread_mutexattr_destroy (&attr); 554 | ``` 555 | 556 | > Note: 一般情况下一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。 557 | > 558 | > 然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己。辛运的是 `pthread_mutex` 支持递归锁,也就是允许一个线程递归的申请锁,只要把 attr 的类型改成 `PTHREAD_MUTEX_RECURSIVE` 即可。 559 | 560 | 作者使用 `dispatch_semaphore_t` 作为图像帧数组的锁是因为 `dispatch_semaphore_t` 更加轻量且对于图像帧数组的临界操作比较快,不会造成长时间的阻塞,这种情况下 `dispatch_semaphore_t` 具有性能优势(Emmmmmm..老生常谈了,熟悉的同学不要抱怨,照顾一下后面的同学)。 561 | 562 | #### YYImageDecoder 内的实现思路 563 | 564 | YYImageDecoder 内在初始化时会初始化锁并更新图像源数据,在更新图像源时调用 `_updateSource` 方法根据当前图像类型以作者对该类型封装好的底层数据结构和对应图像类型解码规则做解码,解码之后设置对应属性。 565 | 566 | 关于作者对不同格式的图像数据的底层封装源码感兴趣的读者可以参考本文文末的扩展阅读章节内容自行查阅。 567 | 568 | 关于 YYImageDecoder 的一些简单使用示例可以查看 [YYImageCoder.h](https://github.com/ibireme/YYImage/blob/master/YYImage/YYImageCoder.h#L106) 了解。 569 | 570 | ## 总结 571 | 572 | - 文章系统的分析了 YYImage 源码,希望各位读者在阅读本文之后可以对 YYImage 整体架构和设计思路有清晰的认识。 573 | - 文章对 YYImage 的 Image 层级的三类图像(YYImage, YYFrameImage, YYSpriteSheetImage)分别解读,希望可以对各位读者关于这三类图像的组成原理和呈现动画的方式的理解有所帮助。 574 | - 文章深入剖析了 YYAnimatedImageView 的内部实现,提炼出其设计思路以供读者探究。 575 | - 笔者把自己在阅读源码中发现的值得分享的实现细节结合源码单独拎出来分析,希望各位读者可以在自己平时工作中遇到相似情况时能够多一些思路,封装项目组件时可以用到这些技巧。 576 | 577 | 文章写得比较用心(是我个人的原创文章,转载请注明出处 [https://lision.me/](https://lision.me/)),如果发现错误会优先在我的 [个人博客](https://lision.me/) 中更新。能力不足,水平有限,如果有任何问题欢迎在我的微博 [@Lision](https://weibo.com/lisioncode) 联系我,另外我的 [GitHub 主页](https://github.com/Lision) 里有很多有趣的小玩意哦~ 578 | 579 | 最后,希望我的文章可以为你带来价值~ 580 | 581 | ## 扩展阅读 582 | 583 | - [libpng 官网关于 PNG 结构的官方说明](http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html) 584 | - [APNG 的维基百科](https://wiki.mozilla.org/APNG_Specification) 585 | - [WebP 开发者文档](https://developers.google.com/speed/webp/docs/api) -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yyimage/blood_wheel_eye.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yyimage/blood_wheel_eye.jpeg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yyimage/image_coder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yyimage/image_coder.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yyimage/ss_wukong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yyimage/ss_wukong.png -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yyimage/xiaomai.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yyimage/xiaomai.gif -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yyimage/yyimage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yyimage/yyimage.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yyimage/yyimage_h.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yyimage/yyimage_h.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yyimage/yyimage_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yyimage/yyimage_struct.png -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x01.md: -------------------------------------------------------------------------------- 1 | # 揭秘 YYModel 的魔法(上) 2 | 3 | ![](yymodel_x01/design-model-x01.jpg) 4 | 5 | ## 前言 6 | 7 | iOS 开发中少不了各种各样的模型,不论是采用 MVC、MVP 还是 MVVM 设计模式都逃不过 Model。 8 | 9 | 那么大家在使用 Model 的时候肯定遇到过一个问题,即接口传递过来的数据(一般是 JSON 格式)需要转换为 iOS 内我们能直接使用的模型(类)。iOS 开发早期第三方框架没有那么多,大家可能会手写相关代码,但是随着业务的扩展,模型的增多,这些没什么技术含量的代码只是在重复的浪费我们的劳动力而已。 10 | 11 | 这时候就需要一种工具来帮助我们把劳动力从这些无意义的繁琐代码中解放出来,于是 GitHub 上出现了很多解决此类问题的第三方库,诸如 Mantle、JSONModel、MJExtension 以及 YYModel 等等。 12 | 13 | 这些库的神奇之处在于它们提供了模型与 JSON 数据的自动转换功能,仿佛具有魔法一般!本文将通过剖析 YYModel 源码一步一步破解这“神奇”的魔法。 14 | 15 | [YYModel](https://github.com/ibireme/YYModel) 是一个高性能 iOS/OSX 模型转换框架(该项目是 [YYKit](https://github.com/ibireme/YYKit) 组件之一)。YYKit 在我之前的文章【[从 YYCache 源码 Get 到如何设计一个优秀的缓存](https://lision.me/yycache/)】中已经很详细的介绍过了,感兴趣的同学可以点进去了解一下。 16 | 17 | YYModel 是一个非常轻量级的 JSON 模型自动转换库,代码风格良好且思路清晰,可以从源码中看到作者对 Runtime 深厚的理解。难能可贵的是 YYModel 在其轻量级的代码下还保留着自动类型转换,类型安全,无侵入等特性,并且具有接近手写解析代码的超高性能。 18 | 19 | > 处理 GithubUser 数据 10000 次耗时统计 (iPhone 6): 20 | 21 | ![](yymodel_x01/yymodel-performance.png) 22 | 23 | ## 索引 24 | 25 | - YYModel 简介 26 | - YYClassInfo 剖析 27 | - NSObject+YYModel 探究 28 | - JSON 与 Model 相互转换 29 | - 总结 30 | 31 | ## YYModel 简介 32 | 33 | ![](yymodel_x01/yymodel.png) 34 | 35 | 撸了一遍 YYModel 的源码,果然是非常轻量级的 JSON 模型自动转换库,加上 YYModel.h 一共也只有 5 个文件。 36 | 37 | 抛开 YYModel.h 来看,其实只有 YYClassInfo 和 NSObject+YYModel 两个模块。 38 | 39 | - YYClassInfo 主要将 Runtime 层级的一些结构体封装到 NSObject 层级以便调用。 40 | - NSObject+YYModel 负责提供方便调用的接口以及实现具体的模型转换逻辑(借助 YYClassInfo 中的封装)。 41 | 42 | ## YYClassInfo 剖析 43 | 44 | ![](yymodel_x01/yyclassinfo.jpg) 45 | 46 | 前面说到 YYClassInfo 主要将 Runtime 层级的一些结构体封装到 NSObject 层级以便调用,我觉得如果需要与 Runtime 层级的结构体做对比的话,没什么比表格来的更简单直观了: 47 | 48 | | YYClassInfo | Runtime | 49 | | :---: | :---: | 50 | | YYClassIvarInfo | `objc_ivar` | 51 | | YYClassMethodInfo | `objc_method` | 52 | | YYClassPropertyInfo | `property_t` | 53 | | YYClassInfo | `objc_class` | 54 | 55 | > Note: 本次比较基于 [Runtime 源码](https://opensource.apple.com/tarballs/objc4/) 723 版本。 56 | 57 | 安~ 既然是剖析肯定不会列个表格这样子哈。 58 | 59 | ### YYClassIvarInfo && objc_ivar 60 | 61 | 我把 YYClassIvarInfo 看做是作者对 Runtime 层 `objc_ivar` 结构体的封装,`objc_ivar` 是 Runtime 中表示变量的结构体。 62 | 63 | - YYClassIvarInfo 64 | 65 | ``` obj-c 66 | @interface YYClassIvarInfo : NSObject 67 | @property (nonatomic, assign, readonly) Ivar ivar; ///< 变量,对应 objc_ivar 68 | @property (nonatomic, strong, readonly) NSString *name; ///< 变量名称,对应 ivar_name 69 | @property (nonatomic, assign, readonly) ptrdiff_t offset; ///< 变量偏移量,对应 ivar_offset 70 | @property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 变量类型编码,通过 ivar_getTypeEncoding 函数得到 71 | @property (nonatomic, assign, readonly) YYEncodingType type; ///< 变量类型,通过 YYEncodingGetType 方法从类型编码中得到 72 | 73 | - (instancetype)initWithIvar:(Ivar)ivar; 74 | @end 75 | ``` 76 | - `objc_ivar` 77 | 78 | ``` obj-c 79 | struct objc_ivar { 80 | char * _Nullable ivar_name OBJC2_UNAVAILABLE; // 变量名称 81 | char * _Nullable ivar_type OBJC2_UNAVAILABLE; // 变量类型 82 | int ivar_offset OBJC2_UNAVAILABLE; // 变量偏移量 83 | #ifdef __LP64__ // 如果已定义 __LP64__ 则表示正在构建 64 位目标 84 | int space OBJC2_UNAVAILABLE; // 变量空间 85 | #endif 86 | } 87 | ``` 88 | 89 | > Note: 日常开发中 NSString 类型的属性我们都会用 copy 来修饰,而 YYClassIvarInfo 中的 `name` 和 `typeEncoding` 属性都用 strong 修饰。因为其内部是先通过 Runtime 方法拿到 `const char *` 之后通过 `stringWithUTF8String` 方法转为 NSString 的。所以即便是 NSString 这类属性在确定其不会在初始化之后被修改的情况下,使用 strong 做一次单纯的强引用在性能上讲比 copy 要高一些。 90 | 91 | 囧~ 不知道讲的这么细会不会反而引起反感,如果对文章有什么建议可以联系我 [@薛定谔的猹](https://weibo.com/5071795354/profile) 。 92 | 93 | > Note: 类型编码,关于 YYClassIvarInfo 中的 YYEncodingType 类型属性 type 的解析代码篇幅很长,而且没有搬出来的必要,可以参考官方文档 [Type Encodings](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html) 和 [Declared Properties](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html) 阅读这部分源码。 94 | 95 | ### YYClassMethodInfo && objc_method 96 | 97 | 相应的,YYClassMethodInfo 则是作者对 Runtime 中 `objc_method` 的封装,`objc_method` 在 Runtime 是用来定义方法的结构体。 98 | 99 | - YYClassMethodInfo 100 | 101 | ``` obj-c 102 | @interface YYClassMethodInfo : NSObject 103 | @property (nonatomic, assign, readonly) Method method; ///< 方法 104 | @property (nonatomic, strong, readonly) NSString *name; ///< 方法名称 105 | @property (nonatomic, assign, readonly) SEL sel; ///< 方法选择器 106 | @property (nonatomic, assign, readonly) IMP imp; ///< 方法实现,指向实现方法函数的函数指针 107 | @property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 方法参数和返回类型编码 108 | @property (nonatomic, strong, readonly) NSString *returnTypeEncoding; ///< 返回值类型编码 109 | @property (nullable, nonatomic, strong, readonly) NSArray *argumentTypeEncodings; ///< 参数类型编码数组 110 | 111 | - (instancetype)initWithMethod:(Method)method; 112 | @end 113 | ``` 114 | 115 | - `objc_method` 116 | 117 | ``` obj-c 118 | struct objc_method { 119 | SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名称 120 | char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法类型 121 | IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法实现(函数指针) 122 | } 123 | ``` 124 | 125 | 可以看到基本也是一一对应的关系,除了类型编码的问题作者为了方便使用在封装时进行了扩展。 126 | 127 | 为了照顾对 Runtime 还没有一定了解的读者,我这里简单的解释一下 `objc_method` 结构体(都是我自己的认知,欢迎讨论): 128 | 129 | - SEL,selector 在 Runtime 中的表现形式,可以理解为方法选择器 130 | 131 | ``` obj-c 132 | typedef struct objc_selector *SEL; 133 | ``` 134 | 135 | - IMP,函数指针,指向具体实现逻辑的函数 136 | 137 | ``` obj-c 138 | #if !OBJC_OLD_DISPATCH_PROTOTYPES 139 | typedef void (*IMP)(void /* id, SEL, ... */ ); 140 | #else 141 | typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 142 | #endif 143 | ``` 144 | 145 | 关于更多 Runtime 相关的知识由于篇幅原因(真的写不完)就不在这篇文章介绍了,我推荐大家去鱼神的文章 [Objective-C Runtime](http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/) 学习(因为我最早接触 Runtime 就是通过这篇文章,笑~)。 146 | 147 | 有趣的是,鱼神的文章中对 SEL 的描述有一句“其实它就是个映射到方法的 C 字符串”,但是他在文章中没有介绍出处。本着对自己文章质量负责的原则,对于一切没有出处的表述都应该持有怀疑的态度,所以我下面讲一下自己的对于 SEL 的理解。 148 | 149 | 撸了几遍 Runtime 源码,发现不论是 objc-runtime-new 还是 objc-runtime-old 中都用 SEL 类型作为方法结构体的 name 属性类型,而且通过以下源码: 150 | 151 | ``` obj-c 152 | OBJC_EXPORT SEL _Nonnull sel_registerName(const char * _Nonnull str) 153 | OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0); 154 | 155 | OBJC_EXPORT const char * _Nonnull sel_getName(SEL _Nonnull sel) 156 | OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0); 157 | ``` 158 | 159 | 可以看到通过一个 `const char *` 类型的字符串即可在 Runtime 系统中注册并返回一个 SEL,方法的名称则会映射到这个 SEL。 160 | 161 | > 官方注释: 162 | > Registers a method with the Objective-C runtime system, maps the method name to a selector, and returns the selector value. 163 | 164 | 所以我觉得 SEL 和 `char *` 的的确确是有某种一一对应的映射关系,不过 SEL 的本质是否是 `char *` 就要打一个问号了。因为我在调试 SEL 阶段发现 SEL 内还有一个当前 SEL 的指针,与 `char *` 不同的是当 `char *` 赋值之后当前 `char *` 变量指针指向字符串首字符,而 SEL 则是 ,即我们无法直接看到它。 165 | 166 | 所以我做了一个无聊的测试,用相同的字符串初始化一个 `char *` 实例与一个 SEL 实例,之后尝试打印它们,有趣的是不论我使用 `%s` 还是 `%c` 都可以从两个实例中得到相同的打印输出,不知道鱼神是否做过相同的测试(笑~) 167 | 168 | 嘛~ 经过验证我们可以肯定 SEL 和 `char *` 存在某种映射关系,可以相互转换。同时猜测 SEL 本质上就是 `char *`,如果有哪位知道 SEL 与 `char *` 确切关系的可以留言讨论哟。 169 | 170 | ### YYClassPropertyInfo && property_t 171 | 172 | YYClassPropertyInfo 是作者对 `property_t` 的封装,`property_t` 在 Runtime 中是用来表示属性的结构体。 173 | 174 | - YYClassPropertyInfo 175 | 176 | ``` obj-c 177 | @interface YYClassPropertyInfo : NSObject 178 | @property (nonatomic, assign, readonly) objc_property_t property; ///< 属性 179 | @property (nonatomic, strong, readonly) NSString *name; ///< 属性名称 180 | @property (nonatomic, assign, readonly) YYEncodingType type; ///< 属性类型 181 | @property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 属性类型编码 182 | @property (nonatomic, strong, readonly) NSString *ivarName; ///< 变量名称 183 | @property (nullable, nonatomic, assign, readonly) Class cls; ///< 类型 184 | @property (nullable, nonatomic, strong, readonly) NSArray *protocols; ///< 属性相关协议 185 | @property (nonatomic, assign, readonly) SEL getter; ///< getter 方法选择器 186 | @property (nonatomic, assign, readonly) SEL setter; ///< setter 方法选择器 187 | 188 | - (instancetype)initWithProperty:(objc_property_t)property; 189 | @end 190 | ``` 191 | 192 | - `property_t` 193 | 194 | ``` obj-c 195 | struct property_t { 196 | const char *name; // 名称 197 | const char *attributes; // 修饰 198 | }; 199 | ``` 200 | 201 | 为什么说 YYClassPropertyInfo 是作者对 `property_t` 的封装呢? 202 | 203 | ``` obj-c 204 | // runtime.h 205 | typedef struct objc_property *objc_property_t; 206 | 207 | // objc-private.h 208 | #if __OBJC2__ 209 | typedef struct property_t *objc_property_t; 210 | #else 211 | typedef struct old_property *objc_property_t; 212 | #endif 213 | 214 | // objc-runtime-new.h 215 | struct property_t { 216 | const char *name; 217 | const char *attributes; 218 | }; 219 | ``` 220 | 221 | 这里唯一值得注意的就是 getter 与 setter 方法了。 222 | 223 | ``` obj-c 224 | // 先尝试获取属性的 getter 与 setter 225 | case 'G': { 226 | type |= YYEncodingTypePropertyCustomGetter; 227 | if (attrs[i].value) { 228 | _getter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]); 229 | } 230 | } break; 231 | case 'S': { 232 | type |= YYEncodingTypePropertyCustomSetter; 233 | if (attrs[i].value) { 234 | _setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]); 235 | } 236 | } break; 237 | 238 | // 如果没有则按照标准规则自己造 239 | if (!_getter) { 240 | _getter = NSSelectorFromString(_name); 241 | } 242 | if (!_setter) { 243 | _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]); 244 | } 245 | ``` 246 | 247 | ### YYClassInfo && objc_class 248 | 249 | 最后作者用 YYClassInfo 封装了 `objc_class`,`objc_class` 在 Runtime 中表示一个 Objective-C 类。 250 | 251 | - YYClassInfo 252 | 253 | ``` obj-c 254 | @interface YYClassInfo : NSObject 255 | @property (nonatomic, assign, readonly) Class cls; ///< 类 256 | @property (nullable, nonatomic, assign, readonly) Class superCls; ///< 超类 257 | @property (nullable, nonatomic, assign, readonly) Class metaCls; ///< 元类 258 | @property (nonatomic, readonly) BOOL isMeta; ///< 元类标识,自身是否为元类 259 | @property (nonatomic, strong, readonly) NSString *name; ///< 类名称 260 | @property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< 父类(超类)信息 261 | @property (nullable, nonatomic, strong, readonly) NSDictionary *ivarInfos; ///< 变量信息 262 | @property (nullable, nonatomic, strong, readonly) NSDictionary *methodInfos; ///< 方法信息 263 | @property (nullable, nonatomic, strong, readonly) NSDictionary *propertyInfos; ///< 属性信息 264 | 265 | - (void)setNeedUpdate; 266 | - (BOOL)needUpdate; 267 | 268 | + (nullable instancetype)classInfoWithClass:(Class)cls; 269 | + (nullable instancetype)classInfoWithClassName:(NSString *)className; 270 | 271 | @end 272 | ``` 273 | 274 | - `objc_class` 275 | 276 | ``` obj-c 277 | // objc.h 278 | typedef struct objc_class *Class; 279 | 280 | // runtime.h 281 | struct objc_class { 282 | Class _Nonnull isa OBJC_ISA_AVAILABILITY; // isa 指针 283 | 284 | #if !__OBJC2__ 285 | Class _Nullable super_class OBJC2_UNAVAILABLE; // 父类(超类)指针 286 | const char * _Nonnull name OBJC2_UNAVAILABLE; // 类名 287 | long version OBJC2_UNAVAILABLE; // 版本 288 | long info OBJC2_UNAVAILABLE; // 信息 289 | long instance_size OBJC2_UNAVAILABLE; // 初始尺寸 290 | struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 变量列表 291 | struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; // 方法列表 292 | struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; // 缓存 293 | struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 协议列表 294 | #endif 295 | 296 | } OBJC2_UNAVAILABLE; 297 | ``` 298 | 299 | 额... 看来想完全避开 Runtime 的知识来讲 YYModel 源码是不现实的。这里简单介绍一下 Runtime 中关于 Class 的知识以便阅读,已经熟悉这方面知识的同学就当温习一下好了。 300 | 301 | ![](yymodel_x01/class-diagram.jpg) 302 | 303 | - isa 指针,用于找到所属类,类对象的 isa 一般指向对应元类。 304 | - 元类,由于 objc_class 继承于 objc_object,即类本身同时也是一个对象,所以 Runtime 库设计出元类用以表述类对象自身所具备的元数据。 305 | - cache,实际上当一个对象收到消息时并不会直接在 isa 指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了。为了优化方法调用的效率,加入了 cache,也就是说在收到消息时,会先去 cache 中查找,找不到才会去像上图所示遍历查找,相信苹果为了提升缓存命中率,应该也花了一些心思(笑~)。 306 | - version,我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。 307 | 308 | > 关于 Version 的官方描述: 309 | > Classes derived from the Foundation framework NSObject class can set the class-definition version number using the setVersion: class method, which is implemented using the class_setVersion function. 310 | 311 | #### YYClassInfo 的初始化细节 312 | 313 | 关于 YYClassInfo 的初始化细节我觉得还是有必要分享出来的。 314 | 315 | ``` obj-c 316 | + (instancetype)classInfoWithClass:(Class)cls { 317 | // 判空入参 318 | if (!cls) return nil; 319 | 320 | // 单例缓存 classCache 与 metaCache,对应缓存类和元类 321 | static CFMutableDictionaryRef classCache; 322 | static CFMutableDictionaryRef metaCache; 323 | static dispatch_once_t onceToken; 324 | static dispatch_semaphore_t lock; 325 | dispatch_once(&onceToken, ^{ 326 | classCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); 327 | metaCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); 328 | // 这里把 dispatch_semaphore 当做锁来使用(当信号量只有 1 时) 329 | lock = dispatch_semaphore_create(1); 330 | }); 331 | 332 | // 初始化之前,首先会根据当前 YYClassInfo 是否为元类去对应的单例缓存中查找 333 | // 这里使用了上面的 dispatch_semaphore 加锁,保证单例缓存的线程安全 334 | dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); 335 | YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void *)(cls)); 336 | // 如果找到了,且找到的信息需要更新的话则执行更新操作 337 | if (info && info->_needUpdate) { 338 | [info _update]; 339 | } 340 | dispatch_semaphore_signal(lock); 341 | 342 | // 如果没找到,才会去老实初始化 343 | if (!info) { 344 | info = [[YYClassInfo alloc] initWithClass:cls]; 345 | if (info) { // 初始化成功 346 | // 线程安全 347 | dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); 348 | // 根据初始化信息选择向对应的类/元类缓存注入信息,key = cls,value = info 349 | CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info)); 350 | dispatch_semaphore_signal(lock); 351 | } 352 | } 353 | 354 | return info; 355 | } 356 | ``` 357 | 358 | 总结一下初始化的主要步骤: 359 | 360 | - 创建单例缓存,类缓存和元类缓存 361 | - 使用 `dispatch_semaphore` 作为锁保证缓存线程安全 362 | - 初始化前先去缓存中查找是否已经向缓存中注册过当前要初始化的 YYClassInfo 363 | - 如果查找到缓存对象,则判断缓存对象是否需要更新并执行相关操作 364 | - 如果缓存中未找到缓存对象则初始化 365 | - 初始化成功后向缓存中注册该 YYClassInfo 实例 366 | 367 | 其中,使用缓存可以有效减少我们在 JSON 模型转换时反复初始化 YYClassInfo 带来的开销,而 `dispatch_semaphore` 在信号量为 1 时是可以当做锁来使用的,虽然它在阻塞时效率超低,但是对于代码中的缓存阻塞这里属于低频事件,使用 `dispatch_semaphore` 在非阻塞状态下性能很高,这里锁的选择非常合适。 368 | 369 | #### 关于 YYClassInfo 的更新 370 | 371 | 首先 YYClassInfo 是作者对应 `objc_class` 封装出来的类,所以理应在其对应的 `objc_class` 实例发生变化时更新。那么 `objc_class` 什么时候会发生变化呢? 372 | 373 | 嘛~ 比如你使用了 `class_addMethod` 方法为你的模型类加入了一个方法等等。 374 | 375 | YYClassInfo 有一个私有 BOOL 类型参数 `_needUpdate` 用以表示当前的 YYClassInfo 实例是否需要更新,并且提供了 `- (void)setNeedUpdate;` 接口方便我们在更改了自己的模型类时调用其将 `_needUpdate` 设置为 YES,当 `_needUpdate` 为 YES 时后面就不用我说了,相关的代码在上一节初始化中有哦。 376 | 377 | ``` obj-c 378 | if (info && info->_needUpdate) { 379 | [info _update]; 380 | } 381 | ``` 382 | 383 | 简单介绍一下 `_update`,它是 YYClassInfo 的私有方法,它的实现逻辑简单介绍就是清空当前 YYClassInfo 实例变量,方法以及属性,之后再重新初始化它们。由于 `_update` 实现源码并没有什么特别之处,我这里就不贴源码了。 384 | 385 | 嘛~ 对 YYClassInfo 的剖析到这里就差不多了。 386 | 387 | ## NSObject+YYModel 探究 388 | 389 | ![](yymodel_x01/nsobject-yymodel.jpg) 390 | 391 | 如果说 YYClassInfo 主要是作者对 Runtime 层在 JSON 模型转换中需要用到的结构体的封装,那么 NSObject+YYModel 在 YYModel 中担当的责任则是利用 YYClassInfo 层级封装好的类切实的执行 JSON 模型之间的转换逻辑,并且提供了无侵入性的接口。 392 | 393 | 第一次阅读 NSObject+YYModel.m 的源码可能会有些不适应,这很正常。因为其大量使用了 Runtime 函数与 CoreFoundation 库,加上各种类型编码和递归解析,代码量也有 1800 多行了。 394 | 395 | 我简单把 NSObject+YYModel.m 的源码做了一下划分,这样划分之后代码看起来一样很简单清晰: 396 | 397 | - 类型编码解析 398 | - 数据结构定义 399 | - 递归模型转换 400 | - 接口相关代码 401 | 402 | ### 类型编码解析 403 | 404 | 类型编码解析代码主要集中在 NSObject+YYModel.m 的上面部分,涉及到 YYEncodingNSType 枚举的定义,配套 `YYClassGetNSType` 函数将 NS 类型转为 YYEncodingNSType 还有 `YYEncodingTypeIsCNumber` 函数判断类型是否可以直接转为 C 语言数值类型的函数。 405 | 406 | 此外还有将 id 指针转为对应 NSNumber 的函数 `YYNSNumberCreateFromID`,将 NSString 转为 NSDate 的 `YYNSDateFromString` 函数,这类函数主要是方便在模型转换时使用。 407 | 408 | ``` obj-c 409 | static force_inline NSDate *YYNSDateFromString(__unsafe_unretained NSString *string) { 410 | typedef NSDate* (^YYNSDateParseBlock)(NSString *string); 411 | // YYNSDateFromString 支持解析的最长时间字符串 412 | #define kParserNum 34 413 | // 这里创建了一个单例时间解析代码块数组 414 | // 为了避免重复创建这些 NSDateFormatter,它的初始化开销不小 415 | static YYNSDateParseBlock blocks[kParserNum + 1] = {0}; 416 | static dispatch_once_t onceToken; 417 | dispatch_once(&onceToken, ^{ 418 | // 这里拿 `yyyy-MM-dd` 举例分析 419 | { 420 | /* 421 | 2014-01-20 // Google 422 | */ 423 | NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 424 | formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; 425 | formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; 426 | formatter.dateFormat = @"yyyy-MM-dd"; 427 | // 这里使用 blocks[10] 是因为 `yyyy-MM-dd` 的长度就是 10 428 | blocks[10] = ^(NSString *string) { return [formatter dateFromString:string]; }; 429 | } 430 | 431 | // 其他的格式都是一样类型的代码,省略 432 | ... 433 | }); 434 | 435 | if (!string) return nil; 436 | if (string.length > kParserNum) return nil; 437 | // 根据入参的长度去刚才存满各种格式时间解析代码块的单例数组取出对应的代码块执行 438 | YYNSDateParseBlock parser = blocks[string.length]; 439 | if (!parser) return nil; 440 | return parser(string); 441 | #undef kParserNum 442 | } 443 | ``` 444 | 445 | > Note: 在 iOS 7 之前 NSDateFormatter 是**非线程安全**的。 446 | 447 | 除此之外还用 YYNSBlockClass 指向了 NSBlock 类,实现过程也比较巧妙。 448 | 449 | ``` obj-c 450 | static force_inline Class YYNSBlockClass() { 451 | static Class cls; 452 | static dispatch_once_t onceToken; 453 | dispatch_once(&onceToken, ^{ 454 | void (^block)(void) = ^{}; 455 | cls = ((NSObject *)block).class; 456 | // 轮询父类直到父类指向 NSObject 停止 457 | while (class_getSuperclass(cls) != [NSObject class]) { 458 | cls = class_getSuperclass(cls); 459 | } 460 | }); 461 | return cls; // 拿到的就是 "NSBlock" 462 | } 463 | ``` 464 | 465 | 关于 `force_inline` 这种代码技巧,我说过我在写完 YYModel 或者攒到足够多的时候会主动拿出来与大家分享这些代码技巧,不过这里大家通过字面也不难理解,就是强制内联。 466 | 467 | 嘛~ 关于内联函数应该不需要我多说(笑)。 468 | 469 | ### 数据结构定义 470 | 471 | NSObject+YYModel 中重新定义了两个类,通过它们来使用 YYClassInfo 中的封装。 472 | 473 | | NSObject+YYModel | YYClassInfo | 474 | | :---: | :---: | 475 | | `_YYModelPropertyMeta` | YYClassPropertyInfo | 476 | | `_YYModelMeta` | YYClassInfo | 477 | 478 | #### _YYModelPropertyMeta 479 | 480 | `_YYModelPropertyMeta` 表示模型对象中的属性信息,它包含 YYClassPropertyInfo。 481 | 482 | ``` obj-c 483 | @interface _YYModelPropertyMeta : NSObject { 484 | @package 485 | NSString *_name; ///< 属性名称 486 | YYEncodingType _type; ///< 属性类型 487 | YYEncodingNSType _nsType; ///< 属性在 Foundation 框架中的类型 488 | BOOL _isCNumber; ///< 是否为 CNumber 489 | Class _cls; ///< 属性类 490 | Class _genericCls; ///< 属性包含的泛型类型,没有则为 nil 491 | SEL _getter; ///< getter 492 | SEL _setter; ///< setter 493 | BOOL _isKVCCompatible; ///< 如果可以使用 KVC 则返回 YES 494 | BOOL _isStructAvailableForKeyedArchiver; ///< 如果可以使用 archiver/unarchiver 归/解档则返回 YES 495 | BOOL _hasCustomClassFromDictionary; ///< 类/泛型自定义类型,例如需要在数组中实现不同类型的转换需要用到 496 | 497 | /* 498 | property->key: _mappedToKey:key _mappedToKeyPath:nil _mappedToKeyArray:nil 499 | property->keyPath: _mappedToKey:keyPath _mappedToKeyPath:keyPath(array) _mappedToKeyArray:nil 500 | property->keys: _mappedToKey:keys[0] _mappedToKeyPath:nil/keyPath _mappedToKeyArray:keys(array) 501 | */ 502 | NSString *_mappedToKey; ///< 映射 key 503 | NSArray *_mappedToKeyPath; ///< 映射 keyPath,如果没有映射到 keyPath 则返回 nil 504 | NSArray *_mappedToKeyArray; ///< key 或者 keyPath 的数组,如果没有映射多个键的话则返回 nil 505 | YYClassPropertyInfo *_info; ///< 属性信息,详见上文 YYClassPropertyInfo && property_t 章节 506 | _YYModelPropertyMeta *_next; ///< 如果有多个属性映射到同一个 key 则指向下一个模型属性元 507 | } 508 | @end 509 | ``` 510 | 511 | #### _YYModelMeta 512 | 513 | `_YYModelMeta` 表示模型的类信息,它包含 YYClassInfo。 514 | 515 | ``` obj-c 516 | @interface _YYModelMeta : NSObject { 517 | @package 518 | YYClassInfo *_classInfo; 519 | /// Key:被映射的 key 与 keyPath, Value:_YYModelPropertyMeta. 520 | NSDictionary *_mapper; 521 | /// Array<_YYModelPropertyMeta>, 当前模型的所有 _YYModelPropertyMeta 数组 522 | NSArray *_allPropertyMetas; 523 | /// Array<_YYModelPropertyMeta>, 被映射到 keyPath 的 _YYModelPropertyMeta 数组 524 | NSArray *_keyPathPropertyMetas; 525 | /// Array<_YYModelPropertyMeta>, 被映射到多个 key 的 _YYModelPropertyMeta 数组 526 | NSArray *_multiKeysPropertyMetas; 527 | /// 映射 key 与 keyPath 的数量,等同于 _mapper.count 528 | NSUInteger _keyMappedCount; 529 | /// 模型 class 类型 530 | YYEncodingNSType _nsType; 531 | 532 | // 忽略 533 | ... 534 | } 535 | @end 536 | ``` 537 | 538 | ### 递归模型转换 539 | 540 | NSObject+YYModel.m 内写了一些(间接)递归模型转换相关的函数,如 `ModelToJSONObjectRecursive` 之类的,由于涉及繁杂的模型编码解析以及代码量比较大等原因我不准备放在这里详细讲解。 541 | 542 | 我认为这种逻辑并不复杂但是牵扯较多的函数代码与结构/类型定义代码不同,后者更适合列出源码让读者对数据有全面清醒的认识,而前者结合功能实例讲更容易使读者对整条功能的流程有一个更透彻的理解。 543 | 544 | 所以我准备放到后面 JSON 与 Model 相互转换时一起讲。 545 | 546 | ### 接口相关代码 547 | 548 | 嘛~ 理由同上。 549 | 550 | ## 半章总结 551 | 552 | - 文章对 YYModel 源码进行了系统解读,有条理的介绍了 YYModel 的结构,相信会让各位对 YYModel 的代码结构有一个清晰的认识。 553 | - 深入剖析了 YYClassInfo 的 4 个类,并详细讲解了它们与 Runtime 层级结构体的对应。 554 | - 在剖析 YYClassInfo 章节中分享了一些我在阅读源码的过程中发现的并且觉得值得分享的处理细节,比如为什么作者选择用 `strong` 来修饰 NSString 等。顺便还对 SEL 与 `char *` 的关系做了实验得出了我的推论。 555 | - 把 YYClassInfo 的初始化以及更新细节单独拎出来做了分析。 556 | - 探究 NSObject+YYModel 源码(分享了一些实现细节)并对其实现代码做了划分,希望能够对读者阅读 YYModel 源码时提供一些小小的帮助。 557 | 558 | 嘛~ 上篇差不多就这样了。我写的上一篇 YYKit 源码系列文章[【从 YYCache 源码 Get 到如何设计一个优秀的缓存】](https://lision.me/yycache/)收到了不少的好评和支持(掘金里一位读者 [@ios123456](https://juejin.im/user/5912c8b2da2f600053723275) 的评论更是暖化了我),这些美好的东西让我更加坚定了继续用心创作文章的决心。 559 | 560 | 文章写得比较用心(是我个人的原创文章,转载请注明 [https://lision.me/](https://lision.me/)),如果发现错误会优先在我的 [个人博客](https://lision.me/) 中更新。如果有任何问题欢迎在我的微博 [@Lision](https://weibo.com/lisioncode) 联系我~ 561 | 562 | 希望我的文章可以为你带来价值~ -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x01/class-diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x01/class-diagram.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x01/design-model-x01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x01/design-model-x01.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x01/nsobject-yymodel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x01/nsobject-yymodel.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x01/yyclassinfo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x01/yyclassinfo.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x01/yymodel-performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x01/yymodel-performance.png -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x01/yymodel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x01/yymodel.png -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x02.md: -------------------------------------------------------------------------------- 1 | # 揭秘 YYModel 的魔法(下) 2 | 3 | ![](yymodel_x02/design-model-x02.jpg) 4 | 5 | ## 前言 6 | 7 | 在上文[《揭秘 YYModel 的魔法(上)》](https://lision.me/yymodel_x01/) 中主要剖析了 [YYModel](https://github.com/ibireme/YYModel) 的源码结构,并且分享了 YYClassInfo 与 NSObject+YYModel 内部有趣的实现细节。 8 | 9 | 紧接上篇,本文将解读 YYModel 关于 JSON 模型转换的源码,旨在揭秘 JSON 模型自动转换魔法。 10 | 11 | ## 索引 12 | 13 | - JSON 与 Model 相互转换 14 | - 总结 15 | 16 | ## JSON 与 Model 相互转换 17 | 18 | JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,它易于人们阅读和编写,同时也易于机器解析和生成。它是基于 [JavaScript Programming Language](http://www.crockford.com/javascript/), [Standard ECMA-262 3rd Edition - December 1999](http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf) 的一个子集。JSON 采用完全独立于语言的文本格式,但是也使用了类似于 C 语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。这些特性使 JSON 成为理想的数据交换语言,点击 [这里](https://www.json.org/json-zh.html) 了解更多关于 JSON 的信息。 19 | 20 | Model 是 [面向对象编程](https://zh.wikipedia.org/zh-hans/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)(Object Oriented Programming,简称 OOP)程序设计思想中的对象,OOP 把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。一般我们会根据业务需求来创建对象,在一些设计模式中(如 MVC 等)对象一般作为模型(Model),即对象建模。 21 | 22 | JSON 与 Model 相互转换按转换方向分为两种: 23 | 24 | - JSON to Model 25 | - Model to JSON 26 | 27 | ![](yymodel_x02/switch.jpg) 28 | 29 | ### JSON to Model 30 | 31 | 我们从 YYModel 的接口开始解读。 32 | 33 | ``` obj-c 34 | + (instancetype)yy_modelWithJSON:(id)json { 35 | // 将 json 转为字典 dic 36 | NSDictionary *dic = [self _yy_dictionaryWithJSON:json]; 37 | // 再通过 dic 得到 model 并返回 38 | return [self yy_modelWithDictionary:dic]; 39 | } 40 | ``` 41 | 42 | 上面接口把 JSON 转 Model 很简单的分为了两个子任务: 43 | 44 | - JSON to NSDictionary 45 | - NSDictionary to Model 46 | 47 | ![](yymodel_x02/j2d2m.jpg) 48 | 49 | #### JSON to NSDictionary 50 | 51 | 我们先看一下 `_yy_dictionaryWithJSON` 是怎么将 json 转为 NSDictionary 的。 52 | 53 | ``` obj-c 54 | + (NSDictionary *)_yy_dictionaryWithJSON:(id)json { 55 | // 入参判空 56 | if (!json || json == (id)kCFNull) return nil; 57 | 58 | NSDictionary *dic = nil; 59 | NSData *jsonData = nil; 60 | // 根据 json 的类型对应操作 61 | if ([json isKindOfClass:[NSDictionary class]]) { 62 | // 如果是 NSDictionary 类则直接赋值 63 | dic = json; 64 | } else if ([json isKindOfClass:[NSString class]]) { 65 | // 如果是 NSString 类则用 UTF-8 编码转 NSData 66 | jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding]; 67 | } else if ([json isKindOfClass:[NSData class]]) { 68 | // 如果是 NSData 则直接赋值给 jsonData 69 | jsonData = json; 70 | } 71 | 72 | // jsonData 不为 nil,则表示上面的 2、3 情况中的一种 73 | if (jsonData) { 74 | // 利用 NSJSONSerialization 方法将 jsonData 转为 dic 75 | dic = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL]; 76 | // 判断转换结果 77 | if (![dic isKindOfClass:[NSDictionary class]]) dic = nil; 78 | } 79 | 80 | return dic; 81 | } 82 | ``` 83 | 84 | 这个函数主要是根据入参的类型判断如何将其转为 NSDictionary 类型并返回。 85 | 86 | 其中 `kCFNull` 是 CoreFoundation 中 CFNull 的单例对象。如同 Foundation 框架中的 NSNull 一样,CFNull 是用来表示集合对象中的空值(不允许为 NULL)。CFNull 对象既不允许被创建也不允许被销毁,而是通过定义一个 CFNull 常量,即 `kCFNull`,在需要空值时使用。 87 | 88 | > 官方文档: 89 | > The CFNull opaque type defines a unique object used to represent null values in collection objects (which don’t allow NULL values). CFNull objects are neither created nor destroyed. Instead, a single CFNull constant object—[kCFNull](https://developer.apple.com/documentation/corefoundation/kcfnull)—is defined and is used wherever a null value is needed. 90 | 91 | NSJSONSerialization 是用于将 JSON 和等效的 Foundation 对象之间相互转换的对象。它在 iOS 7 以及 macOS 10.9(包含 iOS 7 和 macOS 10.9)之后是线程安全的。 92 | 93 | 代码中将 NSString 转为 NSData 用到了 NSUTF8StringEncoding,其中编码类型必须属于 JSON 规范中列出的 5 种支持的编码类型: 94 | 95 | - UTF-8 96 | - UTF-16LE 97 | - UTF-16BE 98 | - UTF-32LE 99 | - UTF-32BE 100 | 101 | 而用于解析的最高效的编码是 UTF-8 编码,所以作者这里使用 NSUTF8StringEncoding。 102 | 103 | > 官方注释: 104 | > The data must be in one of the 5 supported encodings listed in the JSON specification: UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE. The data may or may not have a BOM. The most efficient encoding to use for parsing is UTF-8, so if you have a choice in encoding the data passed to this method, use UTF-8. 105 | 106 | #### NSDictionary to Model 107 | 108 | 现在我们要从 `yy_modelWithJSON` 接口中探究 `yy_modelWithDictionary` 是如何将 NSDictionary 转为 Model 的。 109 | 110 | 敲黑板!做好准备,这一小节介绍的代码是 YYModel 的精华哦~。 111 | 112 | ``` obj-c 113 | + (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary { 114 | // 入参校验 115 | if (!dictionary || dictionary == (id)kCFNull) return nil; 116 | if (![dictionary isKindOfClass:[NSDictionary class]]) return nil; 117 | 118 | // 使用当前类生成一个 _YYModelMeta 模型元类 119 | Class cls = [self class]; 120 | _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls]; 121 | // 这里 _hasCustomClassFromDictionary 用于标识是否需要自定义返回类 122 | // 属于模型转换附加功能,可以不用投入太多关注 123 | if (modelMeta->_hasCustomClassFromDictionary) { 124 | cls = [cls modelCustomClassForDictionary:dictionary] ?: cls; 125 | } 126 | 127 | // 调用 yy_modelSetWithDictionary 为新建的类实例 one 赋值,赋值成功则返回 one 128 | NSObject *one = [cls new]; 129 | // 所以这个函数中我们应该把注意力集中在 yy_modelSetWithDictionary 130 | if ([one yy_modelSetWithDictionary:dictionary]) return one; 131 | 132 | return nil; 133 | } 134 | ``` 135 | 136 | 代码中根据 `_hasCustomClassFromDictionary` 标识判断是否需要自定义返回模型的类型。这段代码属于 YYModel 的附加功能,为了不使大家分心,这里仅做简单介绍。 137 | 138 | 如果我们要在 JSON 转 Model 的过程中根据情况创建不同类型的实例,则可以在 Model 中实现接口: 139 | 140 | ``` obj-c 141 | + (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary; 142 | ``` 143 | 144 | 来满足需求。当模型元初始化时会检测当前模型类是否可以响应上面的接口,如果可以响应则会把 `_hasCustomClassFromDictionary` 标识为 YES,所以上面才会出现这些代码: 145 | 146 | ``` obj-c 147 | if (modelMeta->_hasCustomClassFromDictionary) { 148 | cls = [cls modelCustomClassForDictionary:dictionary] ?: cls; 149 | } 150 | ``` 151 | 152 | 嘛~ 我觉得这些附加的东西在阅读源码时很大程度上会分散我们的注意力,这次先详细的讲解一下,以后遇到类似的代码我们会略过,内部的实现大都与上述案例原理相同,感兴趣的同学可以自己研究哈。 153 | 154 | 我们应该把注意力集中在 `yy_modelSetWithDictionary` 上,这个函数(其实也是 NSObject+YYModel 暴露的接口)是根据字典初始化模型的实现方法。它的代码比较长,如果不想看可以跳过,在后面有解释。 155 | 156 | ``` obj-c 157 | - (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic { 158 | // 入参校验 159 | if (!dic || dic == (id)kCFNull) return NO; 160 | if (![dic isKindOfClass:[NSDictionary class]]) return NO; 161 | 162 | // 根据自身类生成 _YYModelMeta 模型元类 163 | _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)]; 164 | // 如果模型元类键值映射数量为 0 则 return NO,表示构建失败 165 | if (modelMeta->_keyMappedCount == 0) return NO; 166 | 167 | // 忽略,该标识对应 modelCustomWillTransformFromDictionary 接口 168 | if (modelMeta->_hasCustomWillTransformFromDictionary) { 169 | // 该接口类似 modelCustomTransformFromDictionary 接口,不过是在模型转换之前调用的 170 | dic = [((id)self) modelCustomWillTransformFromDictionary:dic]; 171 | if (![dic isKindOfClass:[NSDictionary class]]) return NO; 172 | } 173 | 174 | // 初始化模型设置上下文 ModelSetContext 175 | ModelSetContext context = {0}; 176 | context.modelMeta = (__bridge void *)(modelMeta); 177 | context.model = (__bridge void *)(self); 178 | context.dictionary = (__bridge void *)(dic); 179 | 180 | // 判断模型元键值映射数量与 JSON 所得字典的数量关系 181 | if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) { 182 | // 一般情况下他们的数量相等 183 | // 特殊情况比如有的属性元会映射字典中的多个 key 184 | 185 | // 为字典中的每个键值对调用 ModelSetWithDictionaryFunction 186 | // 这句话是核心代码,一般情况下就是靠 ModelSetWithDictionaryFunction 通过字典设置模型 187 | CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context); 188 | // 判断模型中是否存在映射 keyPath 的属性元 189 | if (modelMeta->_keyPathPropertyMetas) { 190 | // 为每个映射 keyPath 的属性元执行 ModelSetWithPropertyMetaArrayFunction 191 | CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas, 192 | CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)), 193 | ModelSetWithPropertyMetaArrayFunction, 194 | &context); 195 | } 196 | // 判断模型中是否存在映射多个 key 的属性元 197 | if (modelMeta->_multiKeysPropertyMetas) { 198 | // 为每个映射多个 key 的属性元执行 ModelSetWithPropertyMetaArrayFunction 199 | CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas, 200 | CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)), 201 | ModelSetWithPropertyMetaArrayFunction, 202 | &context); 203 | } 204 | } else { // 模型元键值映射数量少,则认为不存在映射多个 key 的属性元 205 | // 直接为每个 modelMeta 属性元执行 ModelSetWithPropertyMetaArrayFunction 206 | CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas, 207 | CFRangeMake(0, modelMeta->_keyMappedCount), 208 | ModelSetWithPropertyMetaArrayFunction, 209 | &context); 210 | } 211 | 212 | // 忽略,该标识对应接口 modelCustomTransformFromDictionary 213 | if (modelMeta->_hasCustomTransformFromDictionary) { 214 | // 该接口用于当默认 JSON 转 Model 不适合模型对象时做额外的逻辑处理 215 | // 我们也可以用这个接口来验证模型转换的结果 216 | return [((id)self) modelCustomTransformFromDictionary:dic]; 217 | } 218 | 219 | return YES; 220 | } 221 | ``` 222 | 223 | 代码已经注明必要中文注释,关于两处自定义扩展接口我们不再多说,由于代码比较长我们先来梳理一下 `yy_modelSetWithDictionary` 主要做了哪些事? 224 | 225 | - 入参校验 226 | - 初始化模型元以及映射表校验 227 | - 初始化模型设置上下文 `ModelSetContext` 228 | - 为字典中的每个键值对调用 `ModelSetWithDictionaryFunction` 229 | - 检验转换结果 230 | 231 | 模型设置上下文 `ModelSetContext` 其实就是一个包含模型元,模型实例以及待转换字典的结构体。 232 | 233 | ``` obj-c 234 | typedef struct { 235 | void *modelMeta; ///< 模型元 236 | void *model; ///< 模型实例,指向输出的模型 237 | void *dictionary; ///< 待转换字典 238 | } ModelSetContext; 239 | ``` 240 | 241 | 大家肯定都注意到了 `ModelSetWithDictionaryFunction` 函数,不论走哪条逻辑分支,最后都是调用这个函数把字典的 key(keypath)对应的 value 取出并赋值给 Model 的,那么我们就来看看这个函数的实现。 242 | 243 | ``` obj-c 244 | // 字典键值对建模 245 | static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) { 246 | // 拿到入参上下文 247 | ModelSetContext *context = _context; 248 | // 取出上下文中模型元 249 | __unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta); 250 | // 根据入参 _key 从模型元中取出映射表对应的属性元 251 | __unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge id)(_key)]; 252 | // 拿到待赋值模型 253 | __unsafe_unretained id model = (__bridge id)(context->model); 254 | // 遍历 propertyMeta,直到 propertyMeta->_next == nil 255 | while (propertyMeta) { 256 | // 当前遍历的 propertyMeta 有 setter 方法,则调用 ModelSetValueForProperty 赋值 257 | if (propertyMeta->_setter) { 258 | // 核心方法,拎出来讲 259 | ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta); 260 | } 261 | propertyMeta = propertyMeta->_next; 262 | }; 263 | } 264 | ``` 265 | 266 | `ModelSetWithDictionaryFunction` 函数的实现逻辑就是先通过模型设置上下文拿到带赋值模型,之后遍历当前的属性元(直到 propertyMeta->_next == nil),找到 `setter` 不为空的属性元通过 `ModelSetValueForProperty` 方法赋值。 267 | 268 | `ModelSetValueForProperty` 函数是为模型中的属性赋值的实现方法,也是整个 YYModel 的核心代码。别紧张,这个函数写得很友好的,也就 300 多行而已 😜(无关紧要的内容我会尽量忽略掉),不过忽略的太多会影响代码阅读的连续性,如果嫌长可以不看,文章后面会总结一下这个函数的实现逻辑。 269 | 270 | ``` obj-c 271 | static void ModelSetValueForProperty(__unsafe_unretained id model, 272 | __unsafe_unretained id value, 273 | __unsafe_unretained _YYModelPropertyMeta *meta) { 274 | // 如果属性是一个 CNumber,即输入 int、uint…… 275 | if (meta->_isCNumber) { 276 | // 转为 NSNumber 之后赋值 277 | NSNumber *num = YYNSNumberCreateFromID(value); 278 | // 这里 ModelSetNumberToProperty 封装了给属性元赋值 NSNumber 的操作 279 | ModelSetNumberToProperty(model, num, meta); 280 | if (num) [num class]; // hold the number 281 | } else if (meta->_nsType) { 282 | // 如果属性属于 nsType,即 NSString、NSNumber…… 283 | if (value == (id)kCFNull) { // 为空,则赋值 nil(通过属性元 _setter 方法使用 objc_msgSend 将 nil 赋值) 284 | ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil); 285 | } else { // 不为空 286 | switch (meta->_nsType) { 287 | // NSString 或 NSMutableString 288 | case YYEncodingTypeNSString: 289 | case YYEncodingTypeNSMutableString: { 290 | // 处理可能的 value 类型:NSString,NSNumber,NSData,NSURL,NSAttributedString 291 | // 对应的分支就是把 value 转为 NSString 或者 NSMutableString,之后调用 setter 赋值 292 | ... 293 | } break; 294 | 295 | // NSValue,NSNumber 或 NSDecimalNumber 296 | case YYEncodingTypeNSValue: 297 | case YYEncodingTypeNSNumber: 298 | case YYEncodingTypeNSDecimalNumber: { 299 | // 对属性元的类型分情况赋值(中间可能会涉及到类型之间的转换) 300 | ... 301 | } break; 302 | 303 | // NSData 或 NSMutableData 304 | case YYEncodingTypeNSData: 305 | case YYEncodingTypeNSMutableData: { 306 | // 对属性元的类型分情况赋值(中间可能会涉及到类型之间的转换) 307 | ... 308 | } break; 309 | 310 | // NSDate 311 | case YYEncodingTypeNSDate: { 312 | // 考虑可能的 value 类型:NSDate 或 NSString 313 | // 转换为 NSDate 之后赋值 314 | ... 315 | } break; 316 | 317 | // NSURL 318 | case YYEncodingTypeNSURL: { 319 | // 考虑可能的 value 类型:NSURL 或 NSString 320 | // 转换为 NSDate 之后赋值(这里对 NSString 的长度判断是否赋值 nil) 321 | ... 322 | } break; 323 | 324 | // NSArray 或 NSMutableArray 325 | case YYEncodingTypeNSArray: 326 | case YYEncodingTypeNSMutableArray: { 327 | // 对属性元的泛型判断 328 | if (meta->_genericCls) { // 如果存在泛型 329 | NSArray *valueArr = nil; 330 | // value 所属 NSArray 则直接赋值,如果所属 NSSet 类则转为 NSArray 331 | if ([value isKindOfClass:[NSArray class]]) valueArr = value; 332 | else if ([value isKindOfClass:[NSSet class]]) valueArr = ((NSSet *)value).allObjects; 333 | 334 | // 遍历刚才通过 value 转换来的 valueArr 335 | if (valueArr) { 336 | NSMutableArray *objectArr = [NSMutableArray new]; 337 | for (id one in valueArr) { 338 | // 遇到 valueArr 中的元素属于泛型类,直接加入 objectArr 339 | if ([one isKindOfClass:meta->_genericCls]) { 340 | [objectArr addObject:one]; 341 | } else if ([one isKindOfClass:[NSDictionary class]]) { 342 | // 遇到 valueArr 中的元素是字典类, 343 | Class cls = meta->_genericCls; 344 | // 忽略 345 | if (meta->_hasCustomClassFromDictionary) { 346 | cls = [cls modelCustomClassForDictionary:one]; 347 | if (!cls) cls = meta->_genericCls; // for xcode code coverage 348 | } 349 | // 还记得我们直接的起点 yy_modelSetWithDictionary,将字典转模型 350 | // 我觉得这应该算是一个间接递归调用 351 | // 如果设计出的模型是无限递归(从前有座山,山上有座庙的故事),那么肯定会慢 352 | NSObject *newOne = [cls new]; 353 | [newOne yy_modelSetWithDictionary:one]; 354 | // 转化成功机也加入 objectArr 355 | if (newOne) [objectArr addObject:newOne]; 356 | } 357 | } 358 | // 最后将得到的 objectArr 赋值给属性 359 | ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, objectArr); 360 | } 361 | } else { 362 | // 没有泛型,嘛~ 判断一下 value 的可能所属类型 NSArray 或 NSSet 363 | // 转换赋值(涉及 mutable) 364 | ... 365 | } 366 | } break; 367 | 368 | // NSDictionary 或 NSMutableDictionary 369 | case YYEncodingTypeNSDictionary: 370 | case YYEncodingTypeNSMutableDictionary: { 371 | // 跟上面数组的处理超相似,泛型的间接递归以及无泛型的类型转换(mutable 的处理) 372 | ... 373 | } break; 374 | 375 | // NSSet 或 NSMutableSet 376 | case YYEncodingTypeNSSet: 377 | case YYEncodingTypeNSMutableSet: { 378 | // 跟上面数组的处理超相似,泛型的间接递归以及无泛型的类型转换(mutable 的处理) 379 | ... 380 | } 381 | 382 | default: break; 383 | } 384 | } 385 | } else { // 属性元不属于 CNumber 和 nsType 386 | BOOL isNull = (value == (id)kCFNull); 387 | switch (meta->_type & YYEncodingTypeMask) { 388 | // id 389 | case YYEncodingTypeObject: { 390 | if (isNull) { // 空,赋值 nil 391 | ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil); 392 | } else if ([value isKindOfClass:meta->_cls] || !meta->_cls) { 393 | // 属性元与 value 从属于同一个类,则直接赋值 394 | ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)value); 395 | } else if ([value isKindOfClass:[NSDictionary class]]) { 396 | // 嘛~ value 从属于 397 | NSObject *one = nil; 398 | // 如果属性元有 getter 方法,则通过 getter 获取到实例 399 | if (meta->_getter) { 400 | one = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter); 401 | } 402 | if (one) { 403 | // 用 yy_modelSetWithDictionary 输出化属性实例对象 404 | [one yy_modelSetWithDictionary:value]; 405 | } else { 406 | Class cls = meta->_cls; 407 | // 略过 408 | if (meta->_hasCustomClassFromDictionary) { 409 | cls = [cls modelCustomClassForDictionary:value]; 410 | if (!cls) cls = meta->_genericCls; // for xcode code coverage 411 | } 412 | // 用 yy_modelSetWithDictionary 输出化属性实例对象,赋值 413 | one = [cls new]; 414 | [one yy_modelSetWithDictionary:value]; 415 | ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)one); 416 | } 417 | } 418 | } break; 419 | 420 | // Class 421 | case YYEncodingTypeClass: { 422 | if (isNull) { // 空,赋值(Class)NULL,由于 Class 其实是 C 语言定义的结构体,所以使用 NULL 423 | // 关于 nil,Nil,NULL,NSNull,kCFNull 的横向比较,我会单独拎出来在下面介绍 424 | ((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)NULL); 425 | } else { 426 | // 判断 value 可能的类型 NSString 或判断 class_isMetaClass(object_getClass(value)) 427 | // 如果满足条件则赋值 428 | ... 429 | } 430 | } break; 431 | 432 | // SEL 433 | case YYEncodingTypeSEL: { 434 | // 判空,赋值(SEL)NULL 435 | // 否则转换类型 SEL sel = NSSelectorFromString(value); 然后赋值 436 | ... 437 | } break; 438 | 439 | // block 440 | case YYEncodingTypeBlock: { 441 | // 判空,赋值(void (^)())NULL 442 | // 否则判断类型 [value isKindOfClass:YYNSBlockClass()] 之后赋值 443 | ... 444 | } break; 445 | 446 | // struct、union、char[n],关于 union 共同体感兴趣的同学可以自己 google,这里简单介绍一下 447 | // union 共同体,类似 struct 的存在,但是 union 每个成员会用同一个存储空间,只能存储最后一个成员的信息 448 | case YYEncodingTypeStruct: 449 | case YYEncodingTypeUnion: 450 | case YYEncodingTypeCArray: { 451 | if ([value isKindOfClass:[NSValue class]]) { 452 | // 涉及 Type Encodings 453 | const char *valueType = ((NSValue *)value).objCType; 454 | const char *metaType = meta->_info.typeEncoding.UTF8String; 455 | // 比较 valueType 与 metaType 是否相同,相同(strcmp(a, b) 返回 0)则赋值 456 | if (valueType && metaType && strcmp(valueType, metaType) == 0) { 457 | [model setValue:value forKey:meta->_name]; 458 | } 459 | } 460 | } break; 461 | 462 | // void* 或 char* 463 | case YYEncodingTypePointer: 464 | case YYEncodingTypeCString: { 465 | if (isNull) { // 判空,赋值(void *)NULL 466 | ((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, (void *)NULL); 467 | } else if ([value isKindOfClass:[NSValue class]]) { 468 | // 涉及 Type Encodings 469 | NSValue *nsValue = value; 470 | if (nsValue.objCType && strcmp(nsValue.objCType, "^v") == 0) { 471 | ((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, nsValue.pointerValue); 472 | } 473 | } 474 | } 475 | 476 | default: break; 477 | } 478 | } 479 | } 480 | ``` 481 | 482 | 额 😓 我是真的已经忽略掉很多代码了,没办法还是有点长。其实代码逻辑还是很简单的,只是模型赋值涉及的编码类型等琐碎逻辑比较多导致代码量比较大,我们一起来总结一下核心代码的实现逻辑。 483 | 484 | - 根据属性元类型划分代码逻辑 485 | - 如果属性元是 CNumber 类型,即 int、uint 之类,则使用 `ModelSetNumberToProperty` 赋值 486 | - 如果属性元属于 NSType 类型,即 NSString、NSNumber 之类,则根据类型转换中可能涉及到的对应类型做逻辑判断并赋值(可以去上面代码中查看具体实现逻辑) 487 | - 如果属性元不属于 CNumber 和 NSType,则猜测为 id,Class,SEL,Block,struct、union、char[n],void* 或 char* 类型并且做出相应的转换和赋值 488 | 489 | 嘛~ 其实上面的代码除了长以外逻辑还是很简单的,总结起来就是根据可能出现的类型去做出对应的逻辑操作,建议各位有时间还是去读下源码,尤其是自己项目中用到 YYModel 的同学。相信看完之后会对 YYModel 属性赋值一清二楚,这样在使用 YYModel 的日常中出现任何问题都可以心中有数,改起代码自然如有神助哈。 490 | 491 | 额...考虑到 NSDictionary to Model 的整个过程代码量不小,我花了一些时间将其逻辑总结归纳为一张图: 492 | 493 | ![](yymodel_x02/d2m.jpg) 494 | 495 | 希望可以尽自己的努力让文章的表述变得更直白。 496 | 497 | ### Model to JSON 498 | 499 | ![](yymodel_x02/m2j.jpg) 500 | 501 | 相比于 JSON to Model 来说,Model to JSON 更简单一些。其中因为 NSJSONSerialization 在对 JSON 的转换时做了一些规定: 502 | 503 | - 顶级对象是 NSArray 或者 NSDictionary 类型 504 | - 所有的对象都是 NSString, NSNumber, NSArray, NSDictionary, 或 NSNull 的实例 505 | - 所有字典中的 key 都是一个 NSString 实例 506 | - Numbers 是除去无穷大和 NaN 的其他表示 507 | 508 | > Note: 上文出自 [NSJSONSerialization 官方文档](https://developer.apple.com/documentation/foundation/nsjsonserialization)。 509 | 510 | 知道了这一点后,我们就可以从 YYModel 的 Model to JSON 接口 `yy_modelToJSONObject` 处开始解读源码了。 511 | 512 | ``` obj-c 513 | - (id)yy_modelToJSONObject { 514 | // 递归转换模型到 JSON 515 | id jsonObject = ModelToJSONObjectRecursive(self); 516 | if ([jsonObject isKindOfClass:[NSArray class]]) return jsonObject; 517 | if ([jsonObject isKindOfClass:[NSDictionary class]]) return jsonObject; 518 | 519 | return nil; 520 | } 521 | ``` 522 | 523 | 嘛~ 一共 4 行代码,只需要关注一下第一行代码中的 `ModelToJSONObjectRecursive` 方法,`Objective-C` 的语言特性决定了从函数名称即可无需注释看懂代码,这个方法从名字上就可以 get 到它是通过递归方法使 Model 转换为 JSON 的。 524 | 525 | ``` obj-c 526 | // 递归转换模型到 JSON,如果转换异常则返回 nil 527 | static id ModelToJSONObjectRecursive(NSObject *model) { 528 | // 判空或者可以直接返回的对象,则直接返回 529 | if (!model || model == (id)kCFNull) return model; 530 | if ([model isKindOfClass:[NSString class]]) return model; 531 | if ([model isKindOfClass:[NSNumber class]]) return model; 532 | // 如果 model 从属于 NSDictionary 533 | if ([model isKindOfClass:[NSDictionary class]]) { 534 | // 如果可以直接转换为 JSON 数据,则返回 535 | if ([NSJSONSerialization isValidJSONObject:model]) return model; 536 | NSMutableDictionary *newDic = [NSMutableDictionary new]; 537 | // 遍历 model 的 key 和 value 538 | [((NSDictionary *)model) enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { 539 | NSString *stringKey = [key isKindOfClass:[NSString class]] ? key : key.description; 540 | if (!stringKey) return; 541 | // 递归解析 value 542 | id jsonObj = ModelToJSONObjectRecursive(obj); 543 | if (!jsonObj) jsonObj = (id)kCFNull; 544 | newDic[stringKey] = jsonObj; 545 | }]; 546 | return newDic; 547 | } 548 | // 如果 model 从属于 NSSet 549 | if ([model isKindOfClass:[NSSet class]]) { 550 | // 如果能够直接转换 JSON 对象,则直接返回 551 | // 否则遍历,按需要递归解析 552 | ... 553 | } 554 | if ([model isKindOfClass:[NSArray class]]) { 555 | // 如果能够直接转换 JSON 对象,则直接返回 556 | // 否则遍历,按需要递归解析 557 | ... 558 | } 559 | // 对 NSURL, NSAttributedString, NSDate, NSData 做相应处理 560 | if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString; 561 | if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string; 562 | if ([model isKindOfClass:[NSDate class]]) return [YYISODateFormatter() stringFromDate:(id)model]; 563 | if ([model isKindOfClass:[NSData class]]) return nil; 564 | 565 | // 用 [model class] 初始化一个模型元 566 | _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:[model class]]; 567 | // 如果映射表为空,则不做解析直接返回 nil 568 | if (!modelMeta || modelMeta->_keyMappedCount == 0) return nil; 569 | // 性能优化细节,使用 __unsafe_unretained 来避免在下面遍历 block 中直接使用 result 指针造成的不必要 retain 与 release 开销 570 | NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:64]; 571 | __unsafe_unretained NSMutableDictionary *dic = result; 572 | // 遍历模型元属性映射字典 573 | [modelMeta->_mapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyMappedKey, _YYModelPropertyMeta *propertyMeta, BOOL *stop) { 574 | // 如果遍历当前属性元没有 getter 方法,跳过 575 | if (!propertyMeta->_getter) return; 576 | 577 | id value = nil; 578 | // 如果属性元属于 CNumber,即其 type 是 int、float、double 之类的 579 | if (propertyMeta->_isCNumber) { 580 | // 从属性中利用 getter 方法得到对应的值 581 | value = ModelCreateNumberFromProperty(model, propertyMeta); 582 | } else if (propertyMeta->_nsType) { // 属性元属于 nsType,即 NSString 之类 583 | // 利用 getter 方法拿到 value 584 | id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter); 585 | // 对拿到的 value 递归解析 586 | value = ModelToJSONObjectRecursive(v); 587 | } else { 588 | // 根据属性元的 type 做相应处理 589 | switch (propertyMeta->_type & YYEncodingTypeMask) { 590 | // id,需要递归解析,如果解析失败则返回 nil 591 | case YYEncodingTypeObject: { 592 | id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter); 593 | value = ModelToJSONObjectRecursive(v); 594 | if (value == (id)kCFNull) value = nil; 595 | } break; 596 | // Class,转 NSString,返回 Class 名称 597 | case YYEncodingTypeClass: { 598 | Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter); 599 | value = v ? NSStringFromClass(v) : nil; 600 | } break; 601 | // SEL,转 NSString,返回给定 SEL 的字符串表现形式 602 | case YYEncodingTypeSEL: { 603 | SEL v = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter); 604 | value = v ? NSStringFromSelector(v) : nil; 605 | } break; 606 | default: break; 607 | } 608 | } 609 | // 如果 value 还是没能解析,则跳过 610 | if (!value) return; 611 | 612 | // 当前属性元是 KeyPath 映射,即 a.b.c 之类 613 | if (propertyMeta->_mappedToKeyPath) { 614 | NSMutableDictionary *superDic = dic; 615 | NSMutableDictionary *subDic = nil; 616 | // _mappedToKeyPath 是 a.b.c 根据 '.' 拆分成的字符串数组,遍历 _mappedToKeyPath 617 | for (NSUInteger i = 0, max = propertyMeta->_mappedToKeyPath.count; i < max; i++) { 618 | NSString *key = propertyMeta->_mappedToKeyPath[i]; 619 | // 遍历到结尾 620 | if (i + 1 == max) { 621 | // 如果结尾的 key 为 nil,则使用 value 赋值 622 | if (!superDic[key]) superDic[key] = value; 623 | break; 624 | } 625 | 626 | // 用 subDic 拿到当前 key 对应的值 627 | subDic = superDic[key]; 628 | // 如果 subDic 存在 629 | if (subDic) { 630 | // 如果 subDic 从属于 NSDictionary 631 | if ([subDic isKindOfClass:[NSDictionary class]]) { 632 | // 将 subDic 的 mutable 版本赋值给 superDic[key] 633 | subDic = subDic.mutableCopy; 634 | superDic[key] = subDic; 635 | } else { 636 | break; 637 | } 638 | } else { 639 | // 将 NSMutableDictionary 赋值给 superDic[key] 640 | // 注意这里使用 subDic 间接赋值是有原因的,原因就在下面 641 | subDic = [NSMutableDictionary new]; 642 | superDic[key] = subDic; 643 | } 644 | // superDic 指向 subDic,这样在遍历 _mappedToKeyPath 时即可逐层解析 645 | // 这就是上面先把 subDic 转为 NSMutableDictionary 的原因 646 | superDic = subDic; 647 | subDic = nil; 648 | } 649 | } else { 650 | // 如果不是 KeyPath 则检测 dic[propertyMeta->_mappedToKey],如果为 nil 则赋值 value 651 | if (!dic[propertyMeta->_mappedToKey]) { 652 | dic[propertyMeta->_mappedToKey] = value; 653 | } 654 | } 655 | }]; 656 | 657 | // 忽略,对应 modelCustomTransformToDictionary 接口 658 | if (modelMeta->_hasCustomTransformToDictionary) { 659 | // 用于在默认的 Model 转 JSON 过程不适合当前 Model 类型时提供自定义额外过程 660 | // 也可以用这个方法来验证转换结果 661 | BOOL suc = [((id)model) modelCustomTransformToDictionary:dic]; 662 | if (!suc) return nil; 663 | } 664 | 665 | return result; 666 | } 667 | ``` 668 | 669 | 额...代码还是有些长,不过相比于之前 JSON to Model 方向上由 `yy_modelSetWithDictionary`,`ModelSetWithDictionaryFunction` 和 `ModelSetValueForProperty` 三个方法构成的间接递归来说算是非常简单了,那么总结一下上面的代码逻辑。 670 | 671 | - 判断入参,如果满足条件可以直接返回 672 | - 如果 Model 从属于 NSType,则根据不同的类型做逻辑处理 673 | - 如果上面条件不被满足,则用 Model 的 Class 初始化一个模型元 _YYModelMeta 674 | - 判断模型元的映射关系,遍历映射表拿到对应键值对并存入字典中并返回 675 | 676 | > Note: 这里有一个性能优化的细节,用 `__unsafe_unretained` 修饰的 dic 指向我们最后要 return 的 NSMutableDictionary *result,看作者的注释:`// avoid retain and release in block` 是为了避免直接使用 `result` 在后面遍历映射表的代码块中不必要的 retain 和 release 操作以节省开销。 677 | 678 | ## 总结 679 | 680 | - 文章紧接上文[《揭秘 YYModel 的魔法(上)》](https://lision.me/yymodel_x01/)中对 YYModel 代码结构的讲解后将重点放到了对 JSON 模型相互转换的实现逻辑上。 681 | - 从 JSON 模型的转换方向上划分,将 YYModel 的 JSON 模型转换过程正反方向剖析揭秘,希望可以解开大家对 JSON 模型自动转换的疑惑。 682 | 683 | 文章写得比较用心(是我个人的原创文章,转载请注明 [https://lision.me/](https://lision.me/)),如果发现错误会优先在我的 [个人博客](https://lision.me/) 中更新。如果有任何问题欢迎在我的微博 [@Lision](https://weibo.com/lisioncode) 联系我~ 684 | 685 | 希望我的文章可以为你带来价值~ 686 | -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x02/d2m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x02/d2m.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x02/design-model-x02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x02/design-model-x02.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x02/j2d2m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x02/j2d2m.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x02/m2j.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x02/m2j.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x02/switch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x02/switch.jpg -------------------------------------------------------------------------------- /Categroy/iOS/YYKit/yymodel_x02/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Categroy/iOS/YYKit/yymodel_x02/wechat.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ELSEWHERE 2 | 3 | ![](Resources/pixiv.jpg) 4 | 5 | --- 6 | 7 | ## 简介 8 | 9 | Emmmmm... 之前把自己的 Blog 从 GitHub 迁徙到了 VPS,无奈由于 VPS 在国外,时常有朋友反映访问不了(其实多半是被运营商误伤所致),所以准备把 Blog 中与技术相关的文章同步到此项目中。 10 | 11 | 至于 Blog 中的一些个人吐槽嘛,就不同步过来了,也不会有人感兴趣才对(笑)。 12 | 13 | ## 索引 14 | 15 | ### 📱 iOS 16 | 17 | | Project | Tag | Article | 18 | | :---: | :---: | :---: | 19 | | YYKit | 源码阅读 | [《从 YYCache 源码 Get 到如何设计一个优秀的缓存》](Categroy/iOS/YYKit/yycache.md)
[《揭秘 YYModel 的魔法(上)》](Categroy/iOS/YYKit/yymodel_x01.md)
[《揭秘 YYModel 的魔法(下)》](Categroy/iOS/YYKit/yymodel_x02.md)
[《YYImage 设计思路,实现细节剖析》](Categroy/iOS/YYKit/yyimage.md) | 20 | | WebViewJavascriptBridge | 源码阅读 | [《WebViewJavascriptBridge 源码中 Get 到的“桥梁美学”》](Categroy/iOS/WebViewJavascriptBridge/webview-javascript-bridge.md) | 21 | | Aspects | 源码阅读 | [《从 Aspects 源码中我学到了什么?》](Categroy/iOS/Aspects/aspects.md) | 22 | | Tips | 技巧积累 | [《巧用 LLVM 特性: Objective-C Class Properties 解耦》](Categroy/iOS/Tips/oc-class-properties.md) | 23 | | WWDC | 底层实现 | [《深入理解 iOS Rendering Process》](Categroy/iOS/WWDC/ios-rendering-process.md) | 24 | 25 | ## 勘误 26 | 27 | 如果在阅读中发现任何问题,欢迎 Issue/PR,包括但不限于: 28 | 29 | - 文字/单词拼写错误 30 | - 描述语义错误 31 | - 技术内容谬误 32 | 33 | 预先在这里谢谢各位大神了哟~ 34 | 35 | ## 许可 36 | 37 | ![](Resources/license.png) 38 | 39 | 此项目内所有文章均由 [@Lision](https://weibo.com/lisioncode) 创作,采用 [Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) 许可。 -------------------------------------------------------------------------------- /Resources/license.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Resources/license.png -------------------------------------------------------------------------------- /Resources/pixiv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lision/ELSEWHERE/3fdbc88f08bdbe9aa6f1356f4a2a83e62f844dee/Resources/pixiv.jpg --------------------------------------------------------------------------------