可可笔记
首发于可可笔记
Objective-C Category

Objective-C Category

如果让我来形容 Objective-C 最大的特点是什么,我想答案应该是强大的 Runtime,而 Category 也是很有趣的内容之一。Category 是 Objective-C 上经久不衰的话题,在众多文章上面都有讨论,本文比较基础,可能看不到什么新的东西,只是简单聊一下 Objective-C 这个有趣的设计。

# 什么是 Category

如果要跟没写过 Objective-C 的朋友解释什么是 Category,我通常会说:Category 可以在既不子类化也不侵入一个类的源码的情况下,为类添加新的方法,从而达到扩展一个类或者分离一个类的目的

Category 在中文的语境下通常翻译成:类别或者类目。但是我觉得这个翻译并不能体现它的特性,当然你也不能像 Swift 的 Extension 一样把它翻译成扩展,因为 Objective-C 里面也有 Class Extension,和 Category 还不一样,这个我们等下会谈到。

例如,我们可以给 NSString 类增加一个 Category:

@implementation NSString (Extension)

- (NSString *)stringByBase64Encoding {
    NSData *encodedData = [self dataUsingEncoding:NSUTF8StringEncoding];
    return [encodedData base64EncodedStringWithOptions:0];
}

@end

引入这个 Category 之后,任何 NSString 的实例都可以获得 stringByBase64Encoding 这个方法,用起来极为方便。

# 为什么要用 Category

既然有些事情子类化可以实现,工具类也可以实现,那么我们为什么还需要 Category?

就上面的问题而言,完全可以子类化一个 MyString: NSString 来添加这个方法,也可以通过把方法添加到一个类似叫做 StringHelper 的工具类里面。

我的理解是:Category 可以减少特殊化,同时还能对类提供约束。如果实现在 MyString 上面的话,这些方法必须 MyString 的实例才可用(如果是实例方法的话),这太特殊了,很多时候我们不喜欢这种特殊,例如我们有时候不得不去继承某个类,那么这个类就失去了 MyString 里面这些方法。又或者是我们从某个 SDK 里面得到了一个类,想要添加方法的时候,如果通过子类化的方式,就会很别扭。如果是写在工具类里面,那么方法和类之间其实没有任何约束,这并不方便也不能更好的表达他们之间的关系。

所以简单一句话,Category 可以非侵入式的扩展或者分离一个类,把方法移到别的文件去。

# Method Swizzling & Hook

Swizzling 和 Category 是分不开的话题,这方面网上各种文章也有很多讨论,比如说这篇:Method Swizzling 简单说,在 Objective-C 里面,Method Swizzling 提供了一个方法替换的方式,可以在运行时将方法 A 掉包成方法 B,在方法 B 里面可以调用原方法,进而实现了 Hook。

在我们需要做一些全局的方法 Hook 的时候,或者是不得已要去 Hack 一个 SDK 默认实现的时候,常常会用到类似的方案。

一个比较好的实现是 JRSwizzle,可以通过简单的创建 Category 实现方法交换。另一个更有趣的项目是 Aspects,这是研究 Objective-C Runtime 必读的项目,短短 1000 行代码,可以不用创建任何新的文件,对 Objective-C 方法进行 Hook,非常值得一读。


# Associated Objects

新手容易犯的错误之一是给 Category 增加 Property,却发现不能这样干。当然有些时候我们的确是有这样的场景的,例如说要给 UIView 添加一个 Category 方法,上面实现一个 loading 效果。那么必然会添加一个 subview 上去,要把这个 view 找出来的话,不用 property 就会比较麻烦。解决的方案就是 associated objects,这个话题已经玩烂了,还是可以看 NSHipster 的这篇文章:Associated Objects

# 覆盖主类方法

新手容易犯的另一个错误是企图用 Category 来覆盖主类的某个方法,以达到 Override 的效果,或者是不小心将方法名和主类命名成了一样的。

这样做当然是不行的,这是一个典型的 Undefined Behavior,我之前在 QQ 分享 SDK 里面还见过这种 UB。解决的方案是 Class Cluster(类簇) 或者上面提到的 Method Swizzling

# 正确分离一个类

如果我们有一个类,功能繁多,容易想到的方式是通过 Category 来分离各种功能,在各个文件里面去实现各自的功能。

例如 ViewController.m 是主类,ViewController+UI.m 进行各种 UI 操作,ViewController+Data.m 进行各种数据操作,这很棒。

但是有个问题,Category 常常需要访问主类的 Property,如果 Property 在主类的 m 文件里面的话,Category 是不能够访问的到的,但是如果将 Property 放到了 h 文件,会将一些不必要的信息暴露给别的类,这是我们所不想要的。我们更希望有些内容是在主类以及各个 Category 的范围内是 Private 的。

遗憾的是这件事情并没有绝对完美的解决方案,但是 Objective-C 提供了 Class Extension 来做一个弥补。Class Extension 像是一个匿名的 Category,他长这样:

@interface ViewController()

@end

虽然长得很像但是他和 Category 有着完全不同的意义,我们常常在 m 文件里面建立一个 Class Extension 来添加各种 Property 和 Private 的方法,这个特性刚好可以用来解决上面那个方法暴露的问题。

我们需要做几件事情:

  • 新建一个叫做 ViewController+Private.h 的文件,这里面是一个 Class Extension
  • 将在各个 Category 和主类 m 文件里面才能访问的 Property 和 Method 放到一个叫做 ViewController+Private.h 的头文件里面去
  • 将真正需要暴露给别的类的 Property 和 Method 放到 ViewController.h 这个头里面去
  • 主类和 Category 文件之间访问的时候,引入 ViewController+Private.h 文件,其他类使用 ViewController 的时候,仅仅引入 ViewController.h 文件

当然,这并不能从语法上完全杜绝使用 ViewController 的人企图使用 +Private 里面的方法,但这是一个强有力的暗示,ViewController+Private.h 的存在意味着告诉调用者,这个文件你不要去引入他,虽然你可以这么做,但是这么做是肮脏的,是不被接受的。如果是做 Framework 的话,这个 h 文件就不要暴露出来了。

可以参考一下我这个项目里面的做法:cyanzhong/Retriever

# 最后

这一篇比较无聊,随着 Swift 的迅速崛起和 Objective-C 的日渐式微,以后我们会用着更好的 Swift 和更好的 Extension 机制,这篇文章的内容也就没有了存在的意义。Swift 的 Extension 甚至能用来实现 Protocol,这一点来说又比 Objective-C 有趣了不少,以后再聊聊这个。

- EOF -

发布于 2017-02-04

文章被以下专栏收录

    分享 Cocoa 开发中遇到的坑和有趣的事,付费社区:https://xiaozhuanlan.com/devnotes