事半功倍——正则表达式由浅入深 及 iOS实战

零. 前言

在前面写的三篇文章:iOS分类 同名方法自动检测脚本、iOS瘦身——移除无用资源的LSUnusedResources源码分析与优化和iOS通知/KVO移除 自动检测脚本
中,核心功能都用到了正则表达式,可以说,正则是模糊匹配的最佳选项了,所以我尝试从入门介绍到应用场景,整理一下正则表达式在iOS开发场景中的作用。

一. 正则介绍

本章引用于正则菜鸟教程,会对该教程有个入门级的概括,加上一些自己的举例和见解。

正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。


例如:

runoo+b,可以匹配 runoob、runooob、runoooooob 等,+ 号代表前面的字符必须至少出现一次(1次或多次)。

runoo*b,可以匹配 runob、runoob、runoooooob 等,* 号代表字符可以不出现,也可以出现一次或者多次(0次、或1次、或多次)。

colou?r 可以匹配 color 或者 colour,? 问号代表前面的字符最多只可以出现一次(0次、或1次)。


通过使用正则表达式,可以:

(1) 提取特定符合规则的文本。

例如,可以测试输入字符串,以查看字符串内是否出现电话号码模式或信用卡号码模式。通常运用于日常开发需求中。

(2) 寻找特定规则的子字符串。

可以查找文档内或输入域内特定的文本,通常运用于提升debug或者阅读源码效率。

(3) 批量替换文本。

可以使用正则表达式来识别文档中的特定文本,完全删除该文本或者用其他文本替换它,通常运用于提升开发工作效率。

除了上面提到的例子之外,还有很多规则,熟练掌握他们,就可以运用于日常开发、开发需求、开发一些内部小工具中,有助于提升工作效率。下面来介绍常用的正则表达式规则。

二. 正则语法

1. 正则常用语法

每个工具都有自己的说明书,语法就是正则的说明书,但这个说明书非常简单,通常不用一天时间就能入门基础的语法,足以运用于80%的工作案例中。

正则表达式是由普通字符(例如字符 a 到 z、数字0到9、一些符号如_";等等)以及特殊字符(称为"元字符")组成的文字模式。模式描述在搜索文本时要匹配的一个或多个字符串。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。

普通字符就是你所能在键盘上打出的所有字符,而特殊字符则有一定的含义,如上面的例子中:+就是匹配一个或多个,*就是匹配零个或多个,等等。

下面会列举一些常用特殊字符和意义:

首先是一些非打印字符,也就是一些键盘上打不出来,但又有具体意义的字符,如:换行符\n、空白字符\s等,红框标出来的就是比较常用的非打印字符。

而特殊字符则会有一个抽象的意义,他并不是某个具体的字符,而是一种基于正则规则的抽象字符。

下面会用匹配的例子去加深对特殊字符的印象:

// *的举例(匹配零次或多次)

正则:zo*
字符:zoe zoo zo z ao
结果:zo zoo zo z 不匹配,因为*代表前面的字符(o)可出现也可不出现,但z必须出现

// +的举例(匹配一次或多次)
正则:zo+
字符:zoe zoo zo z ao
结果:zo zoo zo 不匹配 不匹配,因为+代表前面的字符(+)至少出现一次,但z必须出现

// .的举例(匹配换行符外的任意字符)
正则:zo.
字符:zoe zoo zo z ao
结果:zoe zoo 不匹配 不匹配 不匹配,因为.匹配的是一个字符,只要zo开头且后面接着字符就可以匹配
扩展:.*代表着中间匹配任意字符,如z.*o可以匹配zeeeeeeeeeeeo,但z.o只能匹配zeo;类似的,.+代表中间必须有至少一个任意字符。

// []的举例(相当于集合,在这集合内均可匹配)
正则:[a-z]o
字符:zoe zoo zo z ao
结果:zo zo zo 不匹配 ao,相当于o前面接任意一个字符都可以匹配

// ?的举例(匹配零次或一次)
正则:zo?
字符:zoe zoo zo z ao
结果:zo zo zo z 不匹配,相当于匹配z或者zo

// +和?结合的举例(?表示非贪婪,即尽可能少地匹配)
正则:zo+?
字符:zo zoo zooooooo
匹配:都是zo,因为需要匹配尽可能少的o
扩展:如果不加?,则代表贪婪,即尽可能多的匹配,如zo+可以匹配zooooooooooo

// ^和$结合使用的举例(用于匹配开始字符和结束字符)
正则:^z.o$
字符:zeo azeo zo
结果:zeo 不匹配 不匹配,因为这个字符串一定要以z开头,以o结尾,且中间要有一个字符

// ^和[]结合使用的举例(表示除这个集合外的任意字符)
正则:[^a-z]o
字符:ko 9o _o
结果:不匹配 9o _o

// {}使用举例(表示某个字符出现匹配某个区间次)
正则:zo{1,2}
字符:zo zoo zooo z
结果:zo zoo zoo 不匹配
扩展:{,2}表示{0,2},{1,}表示{1,正无穷}

// |使用举例(表示或)
正则:(zoo|aoo)m
字符:zoom aoom zoo zm
结果:zoom aoom 不匹配 不匹配


// \使用举例(表示转义字符)
正则:zo\.
字符:zo. zoo
结果:zo. 不匹配,因为\.是匹配.这个字符,没有其他意义

// ()使用举例(捕获分组,匹配到括号的字符,以后可以后续使用)
正则:(.*?)\ssays?:\s(.*)
字符:Tom and Jerry says: Yes
结果:如果遍历匹配结果,则分别为Tom and Jerry,Yes

// (?:regex)使用举例(非捕获分组,匹配到括号的字符,但不可以后续使用)
正则:[a-z]+(?:\d)
字符:abc123xyz
结果:abc1,括号内可以匹配字符


/** 环视匹配 **/
// 环视匹配,只匹配位置,不匹配字符,相当于一种条件限制,有时候用到()但是不希望匹配这些字符,就可以使用环视匹配。

// (?=regex)使用举例(顺序肯定环视,从左向右看,后面需要满足括号内的正则,即满足regex前面的位置)
正则:[a-z]+(?=\d)
字符:abc123xyz
结果:abc,即只匹配数字前的字母字符

// (?!regex)使用举例(顺序否定环视,从左向右看,后面不能出现括号内的正则)
正则:[a-z]+(?!\d)
字符:abc123xyz
结果:ab和xyz,因为c后面跟了数字,所以不能匹配

// (?<=regex)使用举例(逆向肯定环视,从右向左看,前面需要满足括号内的正则)
正则:(?<=\d)[a-z]+
字符:abc123xyz
结果:xyz,即只匹配数字后的字母字符

// (?

除此之外,还有一些比较常用的元字符,框出了比较常用的,可以参考一下。

掌握了上面的语法之后,我们就可以匹配一些比较常用的场景了:

^[a-zA-Z0-9_]+$      // 所有包含一个以上的字母、数字或下划线的字符串 
^[1-9][0-9]*$        // 所有的正整数 
^\-?[1-9][0-9]*$          // 所有的整数 
^[-]?[0-9]+(\.[0-9]+)?$ // 所有的浮点数

使用正则只需记住两点就可以掌握了:

  1. 提取特征(共同点)

  2. 排除例外

在日常开发需求中,会遇到很多诸如匹配URL、验证电话号码是否有效等等的需求,这些都是可以用正则解决的,仅需用这两步,兵来将挡水来土掩即可。

https?://[^\s]*        // 匹配HTTP(s) URL,特征:都由http(s)://开头,例外:URL中间不含空格
^[1][^012][0-9]{9}$    // 匹配国内手机号,特征:肯定是11位数字,第一位肯定是1,第二位肯定不是012
[1-9][0-9]{4,}        // 匹配QQ号,特征:5位以上,第一位肯定不是0

2. 拆分并读懂正则

正则虽然很方便,但是他的可读性对于新手来说并不是那么友好,那么就需要一个比较方便的读懂正则的方法了。

第二节和第三节内容截取自JS 正则迷你书,这本书很详细介绍了正则,如果感兴趣可以去看看。

首先看一下正则的优先级

在拆分正则时,应遵循优先级从高到低去拆解。

分析一下正则ab?(c|de*)+|fg,先看括号,显然(c|de*)是一个整体,再看竖杠,竖杠左右各为一个整体,所以原正则拆分为ab?(...)+、fg

分析括号里面的内容:拆分为c、de*,代表出现c或者出现d的时候e为0或多,且这个整体至少出现一次,至此就可以得到可视化逻辑:

再举一个例子,简单的身份证识别:^(\d{15}|\d{17}[\dxX])$

由优先级来看,可以看成^(...)$,括号里面看最低优先级的竖杠,可以分为\d{15}\d{17}[\dxX],分别代表15位数字和17位数字后面跟着x或X,可视化逻辑如下:

最后举一个较为复杂的,IPV4地址:

^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$

看上去很吓人,但我们还是遵循优先级来看,可以发现有两个并列的括号,^(aaa){3}(bbb)$,即以aaa连续出现3次为开头,以bbb出现为结尾的字符串。

观察前后两个括号内容,发现前面的括号只是多了一个\.,其他内容一样,所以可以看成(xxx\.){3}(xxx),即xxx.xxx.xxx.xxx,其中xxx的正则相同。

再看xxx到底是啥,将其拆分为0{0,2}\d0?\d{2}1\d{2}2[0-4]\d25[0-5],其中他们是“或关系”,分开分析如下:


0{0,2}\d,匹配一位数,包括 "0" 补齐的。比如,"9"、"09"、"009";

0?\d{2},匹配两位数,包括 "0" 补齐的,也包括一位数;

1\d{2},匹配 "100" 到 "199";

2[0-4]\d,匹配 "200" 到 "249";

25[0-5],匹配 "250" 到 "255"。


不难得出,正则的意思是xxx.xxx.xxx.xxx,其中xxx为:0-255,前面可补位可无补位,可视化形式如下:

三. 编写准确高效的正则

1. 编写更准确的正则

使用正则之前,先问自己:是否能使用正则、是否有必要使用正则、是否有必要构建一个复杂的正则。

如果原生语言的API能顺利解决问题,那便没有必要使用正则;如果可以拆分为多个可读性更强的正则,那便没必要构建太复杂的正则。

但如果觉得使用正则会使得效率提升很多,那编写一个更准确的正则无疑是一个正确的选择。

书中举了一个例子,需要匹配一个固定电话号码:

055188888888
0551-88888888
(0551)88888888

根据前面的步骤,我们需要提取特征,但不能太着急地一次性将三种特征归纳一起,不然很容易将字符是否出现的情况归纳为^\(?0\d{2,3}\)?-?[1-9]\d{6,7}$,虽然也能匹配上面的字符串,但是会有一些异常情况进入匹配,如(0551-88888888。所以我们要采用分步提取再化简。

这三种形式的号码特征都很明显,分别为:

^0\d{2,3}[1-9]\d{6,7}$
^0\d{2,3}-[1-9]\d{6,7}$
^\(0\d{2,3}\)[1-9]\d{6,7}$

显然地,这三者为“或”关系,所以得正则:

^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$

再提取公共部分:

^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/

进一步化简:

^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$

可视化形式如下:

2. 编写高效的正则

本章节摘自JS 正则迷你书、改进正则表达式的性能

2.1 正则的回溯

这篇文章总结道,回溯的两个要点:

1.如果需要在“进行尝试”和“跳过尝试”之间选择:对于匹配优先量词(贪婪量词),引擎会优先选择“进行尝试”,而对于忽略优先量词(懒惰量词),会选择“跳过尝试”。

2.LIFO(后进先出)原则。

所谓回溯,就是在匹配的位置,正则引擎为了满足其他匹配而不得不进行的字符释放,顺序是从右往左回退,所以叫“回溯”,我叫它“回吐”,把前面“吃到的”字符“吐”一部分出来。

假设正则为ab{1,3}c,目标字符串为"abbbc",则不会回溯

如目标字符串为"abbc",就会有回溯

假设正则为ab{1,3}bbc,目标字符串为"abbbc",则回溯就会有很多:

再看看".*"是如何匹配"abc"de的:

可以看到,由于正则是贪婪的,所以.*会尽可能地往前走,看到不行就回溯,直到可以匹配为止。

而对于非贪婪,也会存在回溯,如^(\d{1,3}?)(\d{1,3})$匹配12345

比如".*?"匹配123"abc"456

2.2 减少回溯的方法

再举个例子,正则表达式为".*",字符串为The name "McDonald's" is said "makudonarudo" in Japanese,可以看到很顺利地匹配成功了。(阴影部分是POSIX NFA,但现在我们讨论的是传统型NFA,所以到D就匹配成功了)

但如果匹配失败呢?就会多了很多次回溯的尝试。

现在我们其实是想匹配引号里面的内容,也就是说引号里面可以没有引号,那么我们就可以换个正则:"[^"]*"!,可以看到回溯明显比上面的少(注意,这两个正则意义不一样,使用时务必需要分清楚)

2.3 调整多选结构顺序

现在我们需要匹配引号里面的字符(包括转义的引号),如字符串为"2 \"x3\" likeness",我们需要提取2 \"x3\" likeness,那么有以下的方案:

可以看到,[^"\\]放到\\.前面可以有效减少回溯次数。

但值得注意的是,这种改动并不能加快报告失败的速度,因为在报告匹配失败之前,所有可能的匹配都已经被尝试。

2.4 避免量词嵌套

这篇文章提到,(.+)*这种量词结合结构造成的回溯是指数级别的,因为+会对字符串做任意长度的切割,而*则会在切割的基础上进行多次迭代,一旦不匹配字符串,对于长度为n的字符串,尝试次数为2^{n+2}+2^{n+1}-k(具体数字可能用不同的编译方式、不同的字符串会不一样,但只需知道是指数级别即可)。

举例:用(a+)*b去匹配aaaaaaaaaa(10个a),很明显会匹配失败,那么尝试的次数为:2^12+2^11-3=6141

加多一个a,次数为2^13+2^12-3=12285

20个a,次数为749997(这个不适用于公式了,我也不知道为啥,但真的是指数级别)

总之,不要用量词嵌套,(.*)*和(.+)*都不要用,下面的效果是一样的

(a*)* <==> (a+)* <==> (a*)+ <==> a*

(a+)+ <==> a+
2.5 一些提高正则效率的操作

除了上面三条之外,以下操作也有助于正则效率的提升:

(1) 用具体化字符组替代通配符

".*"可以替换为"[^"]*"

(2) 调整多选结构顺序

尽可能把命中几率高的放在多选结构前面

(3) 不要用量词嵌套

(a*)* <==> (a+)* <==> (a*)+ <==> a*

(a+)+ <==> a+

(4) 使用非捕获性分组

有时候难免会用到(),如果不想捕获括号里面的内容,请用(?:)替代()

(5) 独立出确定字符

a+可以替换成aa*,这样可以加快判断是否匹配失败,进而加快移位的速度。

(6) 提取分支公共部分

this|that可以修改为th(?:is|at),这样th就只需要检查一遍。

(7) 减少分支数量,缩小分支范围

red|read修改成rea?d

(8) 消除不必要的字符组

[.]修改成\.

(9) 用字符组代替多选结构

[uvwxyz]代替u|v|w|x|y|z,因为前者只进行匹配操作,而后者可能需要在目标字符串的每个位置进行6次回溯。

四. 正则在Xcode的使用

1. 使用正则查找特定字符

先想象一下,现在我们面对的是一个几千行的文件,因为debug需要,我们需要找到一个property:normal的被赋值过程,按平常的思维,你肯定会想到 command+F->搜索normal,顶多再勾选一下Case SensitiveMatches word,然后疯狂按回车去找。

当然上面的方法不是说没问题,但如果这个文件很多带有normal的方法和normal的临时变量,再加上我们现在需要找的是被赋值的过程(setter),找到的可能很多都是调用该normal变量的方法,可能你找一趟之后,前面的又忘了。

你可能又会想到解决方法,在搜索界面搜索normal =,这个看上去还行,但是鉴于有些人的代码风格可能不太一样,你不知道变量和等号之中隔了几个空格,比如有些人有强迫症:

self.normal                            = 1
self.longlongParam                     = 2;
self.longlonglonglonglonglonglongParam = 3;

又或者有些不符合代码规范的赋值方法:

self.normal=1;
self.longlongParam    = 2;

这时候你的查找方法就可能会漏掉一些情况了,而正则表达式,无疑给模糊搜索提供了一个有力的帮助,再想一想我们要找的property的特征:self.normal或者_normal,后面接上若干个空格和一个等号。

因此可以得到正则表达式(self\.|\_)normal\s*=,再勾选一下Regular Expression就可以了,或者如果你想在左侧搜索栏里面一一列举出来,你也可以右键该文件所在的文件夹,选Find in Selected Groups...,就可以更直观地找到该property的所有赋值过程,还可以一一点进去看。

2. 使用正则替换字符

案例一:给系统方法加一个参数

有时候,我们在模糊匹配之后,还需要替换或者复用这些字符,这时候怎么办呢?用()匹配到子字符串,再用$符号表示匹配到的字符串就可以了。

这篇文章举了一个需要在工程中全局使用正则替换的例子:在iOS6之前的系统方法是presentModalViewController:animated:,而在iOS6之后,该方法已过期,系统方法变为presentModalViewController:animated:completion:,如果想批量加一个参数completion:nil怎么办呢?

原文提到用这个正则表达式:presentModalViewController:(.*) animated:(.*)\]找到所有的方法,然后用presentViewController:$1 animated:$2 completion:nil]

但有个问题就是,如果这个方法是带有换行符号的话会匹配不到:

// 可以匹配
[self presentModalViewController:vc animated:YES];

// 匹配不到
[self presentModalViewController:vc
                        animated:YES];

所以考虑到换行的情况,我们用presentModalViewController:(\w*)(\s*)animated:(.*)\]匹配方法,然后用presentModalViewController:$1$2animated:$3$2completion:nil\]去替换,这样就能在保留原来格式的基础上,增加一个方法参数了。

// 替换结果1
[self presentModalViewController:vc animated:YES completion:nil];

// 替换结果2
[self presentModalViewController:vc
                        animated:YES
                        completion:nil];
案例二:批量为#define文件新增方法

再来一个例子,这篇文章则提到了接口字符串的替换:

一开始的define长这样:

#define host        @"https://xxx"

#define login   @"/login"
#define register    @"/register"

后面觉得自己每次都要手动拼接就很烦,索性自己在define阶段就把他拼接好完事了,类似实现这样的效果:

#define host        @"https://xxx"

#define login   [host stringByAppendingString:@"/login"]
#define register    [host stringByAppendingString:@"/register"]

如果手动搞,最烦的地方就是,你首先要用鼠标点击到@前面,粘贴一下,然后又点击到最右边,按一下],当然参数少的时候也不是很花时间,但是如果是一个上百行的文件的话,那就很酸爽了。。

现在我们假定需要整理的是符合格式为#define xxx @"/xxx"的定义,并为他自动加上拼接代码,需要考虑到的情况有:

// url
#define host        @"https://xxx"
// define和xxx之间有若干个空格
#define            login   @"/login"
// xxx和@"/xxx"之间有若干个空格
#define register                 @"/register"
// 中间可能还有"\"和换行符
#define \
forget    \
@"/forget"

我们要用到的正则是#define([\\\s]*)(.*)([\\\s]*)@"(/.*)",替换规则为#define$1$2$3[host stringByAppendingString:@"$4"],这样我们就可以替换到了:

// url
#define host        @"https://xxx"
// define和xxx之间有若干个空格
#define            login   [host stringByAppendingString:@"/login"]
// xxx和@"/xxx"之间有若干个空格
#define register                 [host stringByAppendingString:@"/register"]
// 中间可能还有"\"和换行符
#define \
forget    \
[host stringByAppendingString:@"/forget"]

在替换的时候,尽量要想到多空格、换行的情况,并且尽可能保证原有格式不被破坏,替换之后回头检查一下每一个修改,这样有利于维护较大文件的可读性(因为你替换的时候有可能一次性替换了上百条)

五. iOS内部工具实战

正则除了在开发层面帮助我们提升效率之外,还可以给我们创造优化工程的可能性,下面我将讲解两个核心功能用到正则的内部工具。

1. 使用正则表达式检测工程内无用的图片资源

这个工具的核心思想是:对于一个资源的名字,如果这个名字在特定类型的文件中被特定格式引用,如:.h、.m文件就是@"xxx",.xib文件就是@"image name="xxx"等等,那么这个图片资源被视为有用的,否则,就会被视为无用资源,可以被移除。

由此可以得到正则表达式@"(.*?)"image name="(.+?)"去匹配这些文件。

对于带数字的图片,我们就不能简单地这样判断了,因为工程中引用一般都是这样的:[NSString stringWithFormat:@"xxx_%ld", index],而工程名通常是这样的:xxx_1xxx_2...

这时候如果还按照全匹配的思路,那么这些带数字的文件就会被误删掉了,所以,我们通常会取数字的前缀xxx_,然后根据工程中是否含有xxx_%或者xxx_[0-9]去判断。

有些奇怪的图片把序号放前面,1_xxx,所以我们还需要判断是取前缀or后缀,对于前缀要用正则去搞:%(.*?)xxx,这样就可以覆盖到带数字的图片了。

效果也是比较可观的(我错过了两个版本才去截图,又多了一M多的大小= =,本来应该删除了6M多的)

具体实现和实践可以看我之前写的iOS瘦身——移除无用资源的LSUnusedResources源码分析与优化

2. 使用正则表达式检测分类同名方法

我们知道,由于iOS语言的特性,object setup阶段,会把category方法插入到类的方法列表中,如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法,可能会导致原有功能不可用。

所以我写了个脚本,用于检测有没有分类覆盖原有方法,并在编译时自动运行,就可以避免意外情况的发生了。

这个脚本的关键之处就在于:如何准确地提取所有类的方法,并把它们联系起来。因此获得思路如下:

第一步:需要先用@implementation(.*?)@end提取到文件的内容和类名。

第二步:匹配到所有的类方法或者实例方法,以+或者-开头,且以{结束,中间可能有若干个括号、空格、换行的若干情况的一段文本:

(\+|\-)\s*\([^;<>=\+\-]*?\)\s*([^;<>=\+\-]*?)\s*\{

上面的正则即可匹配到-(xxx)func:(xxx)param func:(xxx)param ... {}类似的格式,且把一些可能引起混淆的运算字符踢了出去,以避免加减号干扰到正则了。(注意:+ load方法要排除在外)。

第三步:匹配出所有带参数的前缀,用下面的正则表达式,如果不匹配,则说明这个方法没有参数,如果匹配了,则取每一个匹配的参数进行拼接即可。

(\w*?)\s*:\s*\(.*?\)

对于库文件则用nm -j指令提取符号表,再处理相应的方法就可以了,具体实现和实践可以看我的iOS分类 同名方法自动检测脚本。

效果是找到了工程中10处被覆盖的分类方法,16个迁移时未被及时移除的工程文件,以及6个已经失效的无用文件。在以后的开发过程中,这种不符合规范的写法就会被扼杀在开发阶段了。

3. 使用正则表达式检测监听未移除情况

在iOS开发过程中,如果增加了KVO和通知,需要在销毁时候及时移除掉,不然的话就有可能会崩溃。为了避免同事们开发过程中忘记remove,需要写个脚本,在编译时自动监测是否有移除,将崩溃扼杀于开发阶段中。

3.1 通知的监听移除情况

通知的通用创建为:[[NSNotificationCenter defaultCenter] addObserver:(weak)self(.param) selector:...],由于工程中加了个宏NOTIFICATION_CENTER,因此需要检测以上两种格式,得正则:

(\[NSNotificationCenter\s*defaultCenter\]|NOTIFICATION_CENTER)\s*?addObserver:\s*?.*?[sS]elf(.*?)\s+?selector:

通知的通用移除为:[[NSNotificationCenter defaultCenter] removeObserver:(weak)self(.param) ....],得正则:

observer = '.*?[sS]elf' + param # 这里的param是指上面添加方法的参数

(\[NSNotificationCenter\s*?defaultCenter\]|NOTIFICATION_CENTER)\s*?removeObserver:\s*? + observer + r'(\s*name:.*?)?(\s*object:.*?)?\s*?\]'

如果创建能匹配,移除不能匹配,则视为没有被移除,从而报错。

3.2 KVO的监听移除情况

KVO的通用创建为:[xxx addObserver:(weak)self(.param) forKeyPath:xxx options:.....],则可以通过以下正则检测:

addObserver:\s*.*?[sS]elf(.*?)\s+?forKeyPath:\s*(.+?)\s+?options:

KVO的通用移除为:[xxx removeObserver:(weak)self(.param) forKeyPath:....],则可以通过以下正则检测,其中param和keypath为上述正则的匹配结果:

observer = '.*?[sS]elf' + param

'removeObserver:\s*' + observer + '\s+?forKeyPath:\s*' + keypath

如果创建能匹配,移除不能匹配,则视为没有被移除,从而报错。测试样例可以看iOS通知/KVO移除 自动检测脚本

六. 总结

这篇文章主要介绍了正则的入门级语法和一些实践,在日常开发过程中,如果涉及到模糊匹配的,应第一时间想到用正则,他是一个一行顶几十行的神器,能够大幅度提升工作效率!

你可能感兴趣的:(事半功倍——正则表达式由浅入深 及 iOS实战)