写点什么

如何在 Objective-C 中实现协议扩展

  • 2019-12-09
  • 本文字数:5683 字

    阅读完需:约 19 分钟

如何在 Objective-C 中实现协议扩展


Swift 中的协议扩展为 iOS 开发带来了非常多的可能性,它为我们提供了一种类似多重继承的功能,帮助我们减少一切可能导致重复代码的地方。

关于 Protocol Extension

在 Swift 中比较出名的 Then 就是使用了协议扩展为所有的 AnyObject 添加方法,而且不需要调用 runtime 相关的 API,其实现简直是我见过最简单的开源框架之一:


Swift


public protocol Then {}
extension Then where Self: AnyObject { public func then(@noescape block: Self -> Void) -> Self { block(self) return self }}
extension NSObject: Then {}
复制代码


只有这么几行代码,就能为所有的 NSObject 添加下面的功能:


Swift


let titleLabel = UILabel().then {  $0.textColor = .blackColor()  $0.textAlignment = .Center}
复制代码


这里没有调用任何的 runtime 相关 API,也没有在 NSObject 中进行任何的方法声明,甚至 protocol Then {} 协议本身都只有一个大括号,整个 Then 框架就是基于协议扩展来实现的。


在 Objective-C 中同样有协议,但是这些协议只是相当于接口,遵循某个协议的类只表明实现了这些接口,每个类都需要对这些接口有单独的实现,这就很可能会导致重复代码的产生。


而协议扩展可以调用协议中声明的方法,以及 where Self: AnyObject 中的 AnyObject 的类/实例方法,这就大大提高了可操作性,便于开发者写出一些意想不到的扩展。


如果读者对 Protocol Extension 兴趣或者不了解协议扩展,可以阅读最后的 Reference 了解相关内容。

ProtocolKit

其实协议扩展的强大之处就在于它能为遵循协议的类添加一些方法的实现,而不只是一些接口,而今天为各位读者介绍的 ProtocolKit 就实现了这一功能,为遵循协议的类添加方法。

ProtocolKit 的使用

我们先来看一下如何使用 ProtocolKit,首先定义一个协议:


Objective-C


@protocol TestProtocol
@required
- (void)fizz;
@optional
- (void)buzz;
@end
复制代码


在协议中定义了两个方法,必须实现的方法 fizz 以及可选实现 buzz,然后使用 ProtocolKit 提供的接口 defs 来定义协议中方法的实现了:


Objective-C


@defs(TestProtocol)
- (void)buzz { NSLog(@"Buzz");}
@end
复制代码


这样所有遵循 TestProtocol 协议的对象都可以调用 buzz 方法,哪怕它们没有实现:



上面的 XXObject 虽然没有实现 buzz 方法,但是该方法仍然成功执行了。

ProtocolKit 的实现

ProtocolKit 的主要原理仍然是 runtime 以及宏的;通过宏的使用来隐藏类的声明以及实现的代码,然后在 main 函数运行之前,将类中的方法实现加载到内存,使用 runtime 将实现注入到目标类中。


如果你对上面的原理有所疑惑也不是太大的问题,这里只是给你一个 ProtocolKit 原理的简单描述,让你了解它是如何工作的。


ProtocolKit 中有两条重要的执行路线:


  • _pk_extension_load 将协议扩展中的方法实现加载到了内存

  • _pk_extension_inject_entry 负责将扩展协议注入到实现协议的类

加载实现

首先要解决的问题是如何将方法实现加载到内存中,这里可以先了解一下上面使用到的 defs 接口,它其实只是一个调用了其它宏的超级宏这名字是我编的


Objective-C


#define defs _pk_extension
#define _pk_extension($protocol) _pk_extension_imp($protocol, _pk_get_container_class($protocol))
#define _pk_extension_imp($protocol, $container_class) \ protocol $protocol; \ @interface $container_class : NSObject <$protocol> @end \ @implementation $container_class \ + (void)load { \ _pk_extension_load(@protocol($protocol), $container_class.class); \ } \
#define _pk_get_container_class($protocol) _pk_get_container_class_imp($protocol, __COUNTER__)#define _pk_get_container_class_imp($protocol, $counter) _pk_get_container_class_imp_concat(__PKContainer_, $protocol, $counter)#define _pk_get_container_class_imp_concat($a, $b, $c) $a ## $b ## _ ## $c
复制代码


使用 defs 作为接口的是因为它是一个保留的 keyword,Xcode 会将它渲染成与 @property 等其他关键字相同的颜色。


上面的这一坨宏并不需要一个一个来分析,只需要看一下最后展开会变成什么:


Objective-C


@protocol TestProtocol;
@interface __PKContainer_TestProtocol_0 : NSObject <TestProtocol>
@end
@implementation __PKContainer_TestProtocol_0
+ (void)load { _pk_extension_load(@protocol(TestProtocol), __PKContainer_TestProtocol_0.class);}
复制代码


根据上面宏的展开结果,这里可以介绍上面的一坨宏的作用:


  • defs 这货没什么好说的,只是 _pk_extension 的别名,为了提供一个更加合适的名字作为接口

  • _pk_extension_pk_extension_imp 中传入 $protocol_pk_get_container_class($protocol) 参数

  • _pk_get_container_class 的执行生成一个类名,上面生成的类名就是 __PKContainer_TestProtocol_0,这个类名是 __PKContainer_$protocol__COUNTER__ 拼接而成的(__COUNTER__ 只是一个计数器,可以理解为每次调用时加一)

  • _pk_extension_imp 会以传入的类名生成一个遵循当前 $protocol 协议的类,然后在 + load 方法中执行 _pk_extension_load 加载扩展协议


通过宏的运用成功隐藏了 __PKContainer_TestProtocol_0 类的声明以及实现,还有 _pk_extension_load 函数的调用:


Objective-C


void _pk_extension_load(Protocol *protocol, Class containerClass) {
pthread_mutex_lock(&protocolsLoadingLock);
if (extendedProtcolCount >= extendedProtcolCapacity) { size_t newCapacity = 0; if (extendedProtcolCapacity == 0) { newCapacity = 1; } else { newCapacity = extendedProtcolCapacity << 1; } allExtendedProtocols = realloc(allExtendedProtocols, sizeof(*allExtendedProtocols) * newCapacity); extendedProtcolCapacity = newCapacity; }
...
pthread_mutex_unlock(&protocolsLoadingLock);}
复制代码


ProtocolKit 使用了 protocolsLoadingLock 来保证静态变量 allExtendedProtocols 以及 extendedProtcolCount extendedProtcolCapacity 不会因为线程竞争导致问题:


  • allExtendedProtocols 用于保存所有的 PKExtendedProtocol 结构体

  • 后面的两个变量确保数组不会越界,并在数组满的时候,将内存占用地址翻倍


方法的后半部分会在静态变量中寻找或创建传入的 protocol 对应的 PKExtendedProtocol 结构体:


Objective-C


size_t resultIndex = SIZE_T_MAX;for (size_t index = 0; index < extendedProtcolCount; ++index) {  if (allExtendedProtocols[index].protocol == protocol) {    resultIndex = index;    break;  }}
if (resultIndex == SIZE_T_MAX) { allExtendedProtocols[extendedProtcolCount] = (PKExtendedProtocol){ .protocol = protocol, .instanceMethods = NULL, .instanceMethodCount = 0, .classMethods = NULL, .classMethodCount = 0, }; resultIndex = extendedProtcolCount; extendedProtcolCount++;}
_pk_extension_merge(&(allExtendedProtocols[resultIndex]), containerClass);
复制代码


这里调用的 _pk_extension_merge 方法非常重要,不过在介绍 _pk_extension_merge 之前,首先要了解一个用于保存协议扩展信息的私有结构体 PKExtendedProtocol


Objective-C


typedef struct {  Protocol *__unsafe_unretained protocol;  Method *instanceMethods;  unsigned instanceMethodCount;  Method *classMethods;  unsigned classMethodCount;} PKExtendedProtocol;
复制代码


PKExtendedProtocol 结构体中保存了协议的指针、实例方法、类方法、实例方法数以及类方法数用于框架记录协议扩展的状态。


回到 _pk_extension_merge 方法,它会将新的扩展方法追加到 PKExtendedProtocol 结构体的数组 instanceMethods 以及 classMethods 中:


Objective-C


void _pk_extension_merge(PKExtendedProtocol *extendedProtocol, Class containerClass) {  // Instance methods  unsigned appendingInstanceMethodCount = 0;  Method *appendingInstanceMethods = class_copyMethodList(containerClass, &appendingInstanceMethodCount);  Method *mergedInstanceMethods = _pk_extension_create_merged(extendedProtocol->instanceMethods,                                extendedProtocol->instanceMethodCount,                                appendingInstanceMethods,                                appendingInstanceMethodCount);  free(extendedProtocol->instanceMethods);  extendedProtocol->instanceMethods = mergedInstanceMethods;  extendedProtocol->instanceMethodCount += appendingInstanceMethodCount;
// Class methods ...}
复制代码


因为类方法的追加与实例方法几乎完全相同,所以上述代码省略了向结构体中的类方法追加方法的实现代码。


实现中使用 class_copyMethodListcontainerClass 拉出方法列表以及方法数量;通过 _pk_extension_create_merged 返回一个合并之后的方法列表,最后在更新结构体中的 instanceMethods 以及 instanceMethodCount 成员变量。


_pk_extension_create_merged 只是重新 malloc 一块内存地址,然后使用 memcpy 将所有的方法都复制到了这块内存地址中,最后返回首地址:


Objective-C


Method *_pk_extension_create_merged(Method *existMethods, unsigned existMethodCount, Method *appendingMethods, unsigned appendingMethodCount) {
if (existMethodCount == 0) { return appendingMethods; } unsigned mergedMethodCount = existMethodCount + appendingMethodCount; Method *mergedMethods = malloc(mergedMethodCount * sizeof(Method)); memcpy(mergedMethods, existMethods, existMethodCount * sizeof(Method)); memcpy(mergedMethods + existMethodCount, appendingMethods, appendingMethodCount * sizeof(Method)); return mergedMethods;}
复制代码


这一节的代码从使用宏生成的类中抽取方法实现,然后以结构体的形式加载到内存中,等待之后的方法注入。

注入方法实现

注入方法的时间点在 main 函数执行之前议实现的注入并不是在 + load 方法 + initialize 方法调用时进行的,而是使用的编译器指令(compiler directive) __attribute__((constructor)) 实现的:


Objective-C


__attribute__((constructor)) static void _pk_extension_inject_entry(void);
复制代码


使用上述编译器指令的函数会在 shared library 加载的时候执行,也就是 main 函数之前,可以看 StackOverflow 上的这个问题 How exactly does attribute((constructor)) work?


Objective-C


__attribute__((constructor)) static void _pk_extension_inject_entry(void) {  #1:加锁  unsigned classCount = 0;  Class *allClasses = objc_copyClassList(&classCount);
@autoreleasepool { for (unsigned protocolIndex = 0; protocolIndex < extendedProtcolCount; ++protocolIndex) { PKExtendedProtocol extendedProtcol = allExtendedProtocols[protocolIndex]; for (unsigned classIndex = 0; classIndex < classCount; ++classIndex) { Class class = allClasses[classIndex]; if (!class_conformsToProtocol(class, extendedProtcol.protocol)) { continue; } _pk_extension_inject_class(class, extendedProtcol); } } } #2:解锁并释放 allClasses、allExtendedProtocols}
复制代码


_pk_extension_inject_entry 会在 main 执行之前遍历内存中的所有 Class(整个遍历过程都是在一个自动释放池中进行的),如果某个类遵循了allExtendedProtocols 中的协议,调用 _pk_extension_inject_class 向类中注射(inject)方法实现:


Objective-C


static void _pk_extension_inject_class(Class targetClass, PKExtendedProtocol extendedProtocol) {
for (unsigned methodIndex = 0; methodIndex < extendedProtocol.instanceMethodCount; ++methodIndex) { Method method = extendedProtocol.instanceMethods[methodIndex]; SEL selector = method_getName(method);
if (class_getInstanceMethod(targetClass, selector)) { continue; }
IMP imp = method_getImplementation(method); const char *types = method_getTypeEncoding(method); class_addMethod(targetClass, selector, imp, types); }
#1: 注射类方法}
复制代码


如果类中没有实现该实例方法就会通过 runtime 中的 class_addMethod 注射该实例方法;而类方法的注射有些不同,因为类方法都是保存在元类中的,而一些类方法由于其特殊地位最好不要改变其原有实现,比如 + load+ initialize 这两个类方法就比较特殊,如果想要了解这两个方法的相关信息,可以在 Reference 中查看相关的信息。


Objective-C


Class targetMetaClass = object_getClass(targetClass);for (unsigned methodIndex = 0; methodIndex < extendedProtocol.classMethodCount; ++methodIndex) {  Method method = extendedProtocol.classMethods[methodIndex];  SEL selector = method_getName(method);
if (selector == @selector(load) || selector == @selector(initialize)) { continue; } if (class_getInstanceMethod(targetMetaClass, selector)) { continue; }
IMP imp = method_getImplementation(method); const char *types = method_getTypeEncoding(method); class_addMethod(targetMetaClass, selector, imp, types);}
复制代码


实现上的不同仅仅在获取元类、以及跳过 + load+ initialize 方法上。

总结

ProtocolKit 通过宏和 runtime 实现了类似协议扩展的功能,其实现代码总共也只有 200 多行,还是非常简洁的;在另一个叫做 libextobjc 的框架中也实现了类似的功能,有兴趣的读者可以查看 EXTConcreteProtocol.h · libextobjc 这个文件。


本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/protocol-extension


2019-12-09 15:54804

评论

发布
暂无评论
发现更多内容

Activity的生命周期,这可能是目前最全的

android 程序员 移动开发

Anaconda详细安装及使用教程,阿里P8大牛亲自教你

android 程序员 移动开发

大势已来!!区块链的真正价值是什么

CECBC

2021京东 Android 岗 Java 面试真题解析,android平台架构的四个层次

android 程序员 移动开发

2021牛转乾坤:新花样玩转Android组件化架构实践,15K-50K的详细Android学习指南

android 程序员 移动开发

知识中台与区块链助力多源可信数据价值释放

CECBC

2021新鲜面经,蚂蚁内部转岗Android面试分享,深夜思考

android 程序员 移动开发

面试阿里P6,过关斩将直通2面,结果3面找了个架构师来吊打我?

Java spring 程序员 架构

Activity的任务栈Task以及启动模式与Intent的Flag详解(经典博文,值得收藏

android 程序员 移动开发

Andoird中LiveEventBus的使用——用LiveEventBus替代RxBus

android 程序员 移动开发

想要实现元宇宙,需要哪些技术支撑?

行云创新

技术 云原生 vr 云宇宙 虚拟

2020阿里巴巴,字节跳动,京东,android驱动开发环境搭建

android 程序员 移动开发

2021Android面经,历时一个半月,斩获3个大厂offer,阿里P8大佬整理

android 程序员 移动开发

1.1 黑客与社会工程学

sec01张云龙

11月日更

巧用 Redis 数据结构实现亿级数据聚合统计

码哥字节

redis 数据统计 NoSQL 数据库 11月日更

Android - 在线浏览源码,电话短信相关,文本变化监听器

android 程序员 移动开发

2020面试题合集之吊打面试官系列(一),kotlin数组性能

android 程序员 移动开发

分布式服务下,消息中间件改造

kafka 架构 RocketMQ RabbitMQ 中间件

ajax分析 学习(1),android0基础

android 程序员 移动开发

Androdid Droid Fu介绍,flutter底部弹窗

android 程序员 移动开发

2021 Android 大厂面试(五)插件化,androidframework开发书籍

android 程序员 移动开发

GitLab和Rainbond整合实现一体化开发环境

北京好雨科技有限公司

DevOps gitlab #GitLab gitlab hook rainbond

ajax分析 学习,kotlin构造器

android 程序员 移动开发

2020这一年的Android面经汇总(百度、腾讯、滴滴,查漏补缺

android 程序员 移动开发

A010-menu资源,看完老板哭着让我留下来

android 程序员 移动开发

Andorid&Kotlin编译速度原理剖析(上),lambda表达式的作用与好处

android 程序员 移动开发

Andriod 网络框架 OkHttp 源码解析,总结一下

android 程序员 移动开发

Androdid Droid Fu介绍(1),万字Android技术类校招面试题汇总

android 程序员 移动开发

2021年尾 Android 面试之必问高级知识点(包含答案),kotlin语法大全

android 程序员 移动开发

2021疫情下Android技术人的宅家学习进阶指南!花了大价钱大厂内部买来的学习资料,爱看不看

android 程序员 移动开发

Activity-的-36-大难点,你会几个?,android游戏开发实践指南

android 程序员 移动开发

如何在 Objective-C 中实现协议扩展_语言 & 开发_Draveness_InfoQ精选文章