runtime实践之Method Swizzling

news/2024/7/5 2:54:05

利用 Objective-C 的 Runtime 特性,我们可以给语言做扩展,帮助解决项目开发中的一些设计和技术问题。这一篇,我们来探索一些利用 Objective-C Runtime 的黑色技巧。这些技巧中最具争议的或许就是 Method Swizzling 。

介绍一个技巧,最好的方式就是提出具体的需求,然后用它跟其他的解决方法做比较。

所以,先来看看我们的需求:对 App 的用户行为进行追踪和分析。简单说,就是当用户看到某个 View 或者点击某个 Button 的时候,就把这个事件记下来。

手动添加

最直接粗暴的方式就是在每个 viewDidAppear 里添加记录事件的代码。

@implementation MyViewController ()- (void)viewDidAppear:(BOOL)animated
{[super viewDidAppear:animated];// Custom code // Logging
    [Logging logWithEventName:@“my view did appear”];
}- (void)myButtonClicked:(id)sender
{// Custom code // Logging
    [Logging logWithEventName:@“my button clicked”];
}

这种方式的缺点也很明显:它破坏了代码的干净整洁。因为 Logging 的代码本身并不属于 ViewController 里的主要逻辑。随着项目扩大、代码量增加,你的 ViewController里会到处散布着 Logging 的代码。这时,要找到一段事件记录的代码会变得困难,也很容易忘记添加事件记录的代码。

你可能会想到用继承或类别,在重写的方法里添加事件记录的代码。代码可以是长的这个样子:

@implementation UIViewController ()- (void)myViewDidAppear:(BOOL)animated
{[super viewDidAppear:animated];// Custom code // Logging[Logging logWithEventName:NSStringFromClass([self class])];
}- (void)myButtonClicked:(id)sender
{// Custom code // LoggingNSString *name = [NSString stringWithFormat:@“my button in %@ is clicked”, NSStringFromClass([self class])];[Logging logWithEventName:name];
}

Logging 的代码都很相似,通过继承或类别重写相关方法是可以把它从主要逻辑中剥离出来。但同时也带来新的问题:

  1. 你需要继承 UIViewControllerUITableViewControllerUICollectionViewController 所有这些 ViewController ,或者给他们添加类别; 
  2. 每个 ViewController 里的 ButtonClick 方法命名不可能都一样; 
  3. 你不能控制别人如何去实例化你的子类; 
  4. 对于类别,你没办法调用到原来的方法实现。大多时候,我们重写一个方法只是为了添加一些代码,而不是完全取代它。 
  5. 如果有两个类别都实现了相同的方法,运行时没法保证哪一个类别的方法会给调用。

 

Method Swizzling

Method Swizzling 利用 Runtime 特性把一个方法的实现与另一个方法的实现进行替换。

上一篇文章 有讲到每个类里都有一个 Dispatch Table ,将方法的名字(SEL)跟方法的实现(IMP,指向 C 函数的指针)一一对应。Swizzle 一个方法其实就是在程序运行时在 Dispatch Table 里做点改动,让这个方法的名字(SEL)对应到另个 IMP 。

首先定义一个类别,添加将要 Swizzled 的方法:

@implementation UIViewController (Logging)- (void)swizzled_viewDidAppear:(BOOL)animated
{// call original implementation
    [self swizzled_viewDidAppear:animated];// Logging[Logging logWithEventName:NSStringFromClass([self class])];
}

代码看起来可能有点奇怪,像递归不是么。当然不会是递归,因为在 runtime 的时候,函数实现已经被交换了。调用 viewDidAppear: 会调用你实现的 swizzled_viewDidAppear:,而在 swizzled_viewDidAppear: 里调用 swizzled_viewDidAppear: 实际上调用的是原来的 viewDidAppear: 。

接下来实现 swizzle 的方法 :

@implementation UIViewController (Logging)void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)  
{// the method might not exist in the class, but in its superclassMethod originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);// class_addMethod will fail if original method already existsBOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));// the method doesn’t exist and we just added oneif (didAddMethod) {class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));} else {method_exchangeImplementations(originalMethod, swizzledMethod);}
}

这里唯一可能需要解释的是 class_addMethod 。要先尝试添加原 selector 是为了做一层保护,因为如果这个类没有实现 originalSelector ,但其父类实现了,那 class_getInstanceMethod 会返回父类的方法。这样 method_exchangeImplementations替换的是父类的那个方法,这当然不是你想要的。所以我们先尝试添加 orginalSelector ,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。

最后,我们只需要确保在程序启动的时候调用 swizzleMethod 方法。比如,我们可以在之前 UIViewController 的 Logging 类别里添加 +load: 方法,然后在 +load: 里把 viewDidAppear 给替换掉:

@implementation UIViewController (Logging)+ (void)load
{swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}

一般情况下,类别里的方法会重写掉主类里相同命名的方法。如果有两个类别实现了相同命名的方法,只有一个方法会被调用。但 +load: 是个特例,当一个类被读到内存的时候, runtime 会给这个类及它的每一个类别都发送一个 +load: 消息。

其实,这里还可以更简化点:直接用新的 IMP 取代原 IMP ,而不是替换。只需要有全局的函数指针指向原 IMP 就可以。

void (gOriginalViewDidAppear)(id, SEL, BOOL);void newViewDidAppear(UIViewController *self, SEL _cmd, BOOL animated)  
{// call original implementation
    gOriginalViewDidAppear(self, _cmd, animated);// Logging[Logging logWithEventName:NSStringFromClass([self class])];
}+ (void)load
{Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));gOriginalViewDidAppear = (void *)method_getImplementation(originalMethod);if(!class_addMethod(self, @selector(viewDidAppear:), (IMP) newViewDidAppear, method_getTypeEncoding(originalMethod))) {method_setImplementation(originalMethod, (IMP) newViewDidAppear);}
}

通过 Method Swizzling ,我们成功把逻辑代码跟处理事件记录的代码解耦。当然除了 Logging ,还有很多类似的事务,如 Authentication 和 Caching。这些事务琐碎,跟主要业务逻辑无关,在很多地方都有,又很难抽象出来单独的模块。这种程序设计问题,业界也给了他们一个名字 - Cross Cutting Concerns。

而像上面例子用 Method Swizzling 动态给指定的方法添加代码,以解决 Cross Cutting Concerns 的编程方式叫:Aspect Oriented Programming

转载于:https://www.cnblogs.com/yulang314/p/5011246.html


http://lihuaxi.xjx100.cn/news/244074.html

相关文章

适用于Mac上的SQL Server

适用于Mac上的SQL Server? 众所周知,很多人都在电脑上安装了SQL Server软件,普通用户直接去官网下载安装即可,Mac用户则该如何在Mac电脑上安装SQL Server呢?想要一款适用于Mac上的SQL Server?点击进入&…

SpringBoot+Vue实现学生管理系统

1.技术架构 前后端分离项目,后台:springBootmysqlmybatis 前端:vue框架 2.安装环境 IDEA/eclipse均可 jdk1.7或1.8 maven项目 mysql数据库版本为8.0.17 node 3.功能说明 用户角色:学生、家长或教师、管理员 学生&#…

Python开发环境配置

好久没有写博客了,自从6月份毕业后,进入一家做书法、字画文化宣传的互联网公司(www.manyiaby.com),这段时间一直在进行前端开发,对于后端的使用很少了,整天都是什么html、css、javascript、jque…

hdu 1247

Problem DescriptionA hat’s word is a word in the dictionary that is the concatenation of exactly two other words in the dictionary.You are to find all the hat’s words in a dictionary.InputStandard input consists of a number of lowercase words, one per li…

从虚拟化前端Bug学习分析Kernel Dump

前言 也许大家都知道,分析 Kernel Dump 有个常用的工具叫 Crash,在我刚开始学习分析 Kernel Dump 的时候,总是花大量的时间折腾这个工具的用法,却总是记不住这个工具的功能。后来有一次在参加某次内部分享的时候,有位大…

SpringBoot默认包扫描问题

SpringBootApplication注解默认扫描路径是: 自动扫描主程序所在包及其下面的所有子包里面的组件 在maven多模块项目中,如果想让扫描到,需要在子模块下面创建相同的包 如: 如果包名不同就需要使用ComponentScan注解来扫描 但是…

字母金字塔

s 65 h a 10 while a:s chr(s)h h sprint(h)s ord(s)s s 1a a - 1 a 26 s 65 h A u * (a 1) while a:print(u,h)s s 1s chr(s)h s h ss ord(s)u u[1:]a a - 1 转载于:https://www.cnblogs.com/wumac/p/5704035.html

IOS推送详解

为什么80%的码农都做不了架构师?>>> IOS推送详解 一.关于推送通知 推送通知,也被叫做远程通知,是在iOS 3.0以后被引入的功能。是当程序没有启动或不在前台运行时,告诉用户有新消息的一种途径,是从外部服务…