击水湘江

Born To Fight!


  • Home

  • About

  • Tags

  • Archives

Swift中的Dictionary,Set,Range

Posted on 2017-07-28

Dictionary,Set,Range

Dictionay的Key

Swift中Dictionary的Key都应该是可哈希的,也就是遵守Hashable协议,又因为Hashable是Equatable的扩展,所以又必须遵守Equatable协议

在Swift中比较两个Type对象(引用相等用===)是否相等,需要遵守Equatable协议,并覆盖==方法,如:

   struct Person {
   var name: String
   var zipCode: Int
   var birthday: Date
}
    extension Person: Equatable {
    static func ==(lhs: Person, rhs: Person) -> Bool {
    return lhs.name == rhs.name
    && lhs.zipCode == rhs.zipCode
    && lhs.birthday == rhs.birthday
    }
}

也就是必须明确一下两点:

  1. 相等的实例必须有相同的哈希值。
  2. 具有相同哈希值的两个对象不一定相等。

由于每次往Dictionary中插入数据,都会判断哈希方法,以确定是否覆盖之前Key的Value。所以这个哈希方法应该具备以下两个特点:

一、应该的执行效率就会影响到对Dictionary的操作效率。
二、哈希方法应该尽可能得减少碰撞。

如上面的Person所示,如果要做为Key就还需要增加下面的代码:

extension Person: Hashable {
  var hashValue:Int {
  returen name.hasValue^zipCode.hashValue^birthday.hashValue
  }
}

注:

  1. 标注库中的基础类型如: String,integer,float,boole,以及没有关联值的Enum。
  2. 在Dictionary中,如果使用可变的引用类型来作为Key并且将其改变,这个改变同时又改变了哈希值,那么这个Key对应的Value就丢失了。但是如果值类型的实例作为了Key,因为它执行了一次Copy,所以就不用担心被改变了。这也是值类型的一个优势所在。

Set

Set是Swift标准库中唯一遵循SetAlgebra协议的类型(Foundation中的:IndexSet和CharacterSet也遵循该协议),SetAlgebra中提供了subtracting减法,intersection取交集,formUnion取并集,isDisjoint是否有交集等代数方法。IndexSet中存储都是正整数,它和Set相比存储效率更高,因为IndexSet是存储范围的,比如10000中的前500个数只需要存储俩个数值:起点和终点。

在过滤某个数组中的相同元素时候我们往往可以将其放到Set中,这样就可以将相同元素过滤,但是会遇到一个问题,得打的Set和原来的不是顺序不一样,这时,我们可以给Sequence添加Extension来解决。

extension Sequence where Iterator.Element: Hashable {
func unique() -> [Iterator.Element] {
    var seen:Set<Iterator.Element> = []
    return filter{
        if seen.contains($0){
          return false
        }else{
            seen.insert($0)
            return true
        }
    }
}
}
let result = [1,2,3,12,1,3,4,5,6,4,6].unique()

通过上面的方法就可以将元素按照原来的顺序排列下来。

Range

Swift中管更capable的集合叫countable,countable的边界包含integer和指针。但是不包含浮点型,因为Stride对 integer有限制。如果要遍历浮点型数据,那么需要使用stride(from:to:by),以及stride(from:through:by)来新建一个Sequence。
将封闭的Ragne转化成半封闭的Range是不可能的,因为在浮点数的情况下不能确定比最大值小的值是多少。 除非这个Sequence是Strideable的。如果一个函数接受一个Range座位返回值的话,不可以用...来创建。

Vapor的安装和部署

Posted on 2017-07-26

Vapor
在Swift后端框架中,Vapor是比较常用的,它发展迅速,语法简洁,社区活跃,现将其在Mac上的简单的使用流程做以介绍。

安装

一、安装最新版的Xcode

Xcode是免费的可以直接在App Store中直接下载。下载完之后需要打开Xcode来完成安装,这可能需要等一段时间。
安装Xcode

二、验证Swif是否安装

通过执行eval "$(curl -sL check.vapor.sh)"来第二次确定安装是否成功。

三、安装Vapor

确定Swift成功安装之后我们来安装Vapor toolbox,这其中包含了Vapor的所有依赖以及创建项目时好用的CLI。

安装Homebrew

Homebrew在安装OpenSSL,MySQL,Postgres,Redis,SQLite等依赖的时候极其有用,没有的时候执行如下命令安装:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

安装Homebrew Tap

Vapor的Homebrew Tap可以让你安装Vapor的所有包。执行如下命令安装

brew tap vapor/homebrew-tap
brew update

安装Vapor

执行如下命令安装

brew install vapor

安装完成之后会出现如下图案:

新建项目

新建

接下来我们用api模板(toolbox提供了web,api等各种模板)来创建一个Hello的项目。执行如下指令

vapor new Hello --template=api

然后我们执行tree指令就可以看到如下的目录结构:

Vapor 目录结构
如果出现Command not found不能执行请安装tree软件

brew install tree

我们打开如下的目录找到Routes.swift文件。在Build方法中我们看到:

get("plaintext") { req in

return "Hello, world!"
}

上述表示执行get方法,闭包返回请求结果。

编译

确保你所在的是项目的根目录执行如下指令来编译:

vapor build

执行完毕之后将会看到

Building Project [Done]

发布状态编译

在发布状态编译会提升其性能,执行如下指令:

vapor build --release

启动本地服务

执行如下指令来启动服务

vapor run serve

然后你会看到如下信息:

Server starting....

然后就可以在浏览器中执行localhost:8080/plaintext来看到刚才的Hello, World了。

生成Xcode项目

我们刚才做的启动等操作都是通过终端来的,我们也可以使用Xcode来完成,要使用Xcode来执行,我们需要首先创建一个*.xcodeproj文件,执行如下指令来创建:

vapor xcode

部署

完成上述操作之后你就可以在本地访问自己的服务了,但怎样才能部署到远程,生成自己的链接来访问呢?我们使用Heroku,来完成,Heroku的免费版完全可以满足我们日常练习的需求,并且其简单快捷的Git操作指令一定会让你爱不释手。

创建Heroku账户

请在Heroku官网创建自己的账号。

注意要记住自己的邮箱和密码,因为一会儿需要在终端进行登录。

安装Heroku CLI

Heroku CLI用来创建、管理Heroku上apps的命令行工具。执行如下指令来完成安装:

brew install heroku

安装完成之后执行如下指令来登录,输入邮箱和密码来登录:

heroku login
Enter your Heroku credentials.
Email: adam@example.com
Password (typing will be hidden):
Authentication successful.

创建程序

进入你要部署的App,比如上文的Hello项目,然后执行:

$cd Hello
$git init
$git add.
$git commit -m "add hello app"

然后在Heroku中创建一个app,以便其接收你的代码,执行heroku apps:creat [NAME]指令其中名字必须以字母开头,字呢个包含小写字母,数字和连字符,并且其在heroku的所有程序中必须是唯一的。如果出现:

Creating app... done, ⬢ young-island-91962
         https://young-island-91962.herokuapp.com/ | https://git.heroku.com/young-island-91962.git

这说明执行成功了。
执行git remote -v,就会出现远程git的URL了

heroku    https://git.heroku.com/young-island-91962.git (fetch)
heroku    https://git.heroku.com/young-island-91962.git (push)

然后执行

heroku create
Creating falling-wind-1624... done, stack is cedar-14
http://falling-wind-1624.herokuapp.com/ | https://git.heroku.com/falling-wind-1624.git
Git remote heroku added

注意坑

然后执行git push heroku master
这时会报错:

No default language could be detected for this app.
          remote: HINT: This occurs when Heroku cannot detect the buildpack to use for this application automatically.

这说明heroku官方没有swift的buildpack,所以我们要自己添加

heroku create --buildpack https://github.com/kylef/heroku-buildpack-swift.git
heroku buildpacks:set https://github.com/kylef/heroku-buildpack-swift.git

最后在执行push的时候还会报错:

error at=error code=H10 desc="App crashed" method=GET path="/plaintext",这时需要修改Procfile文件,Procfile需要放到项目的根目录里,内容如下:

web: Run --env=production --workdir=./ --config:servers.default.port=$PORT

这样就可以执行Git的push指令进行部署了:

git push heroku master

随后的操作就变得像平时提交项目一样简单。

git commit -m "change something" -a
git push heroku master

这样一个项目就部署成功了。

RunTime应用实例:MustOverride

Posted on 2017-06-05

常用做法

在IOS开发中,我们的基类往往会写一些空方法,然后让子类去实现,基类控制主要流程(这其实就是模板方法模式),这时我们往往这样写:

1
2
3
- (void)mustBeOverriddenMethod {
[NSException raise:@"Method did not be overridden" format:@"you must override this method in the subclass"];
}

这样该方法如果直接被父类调用就会报异常,并且提示一定要被子类所覆盖。但是该方法存在如下弊端:

  1. 该方法一定要被调用才可以报异常,如果子类没有调用该方法,也没有覆盖该方法,父类在某些特定的情况下才调用该方法,那么就会出错。
  2. 不可以在该方法内部做一个基本的实现,然后被子类继承并且调用[super mustBeOverriddenMethod]
  3. 如果项目中存在一个子类,但是暂时没有用到,并且其没有覆写这个方法,那么没有提示。以后其他人用这个类,很可能就会出错。

优雅的做法及疑问

以上这些问题都可以通过MustOverride框架来实现。
先来看下其用法,然后我们逐步分析其实现方式。
只要在父类需要被实现的方法内容添加一个宏:SUBCLASS_MUST_OVERRIDE即可:

1
2
3
- (void)someMethod {
SUBCLASS_MUST_OVERRIDE;
}

这样就可以了,并且更加神奇的是:

  1. 没有类调用该方法也可以报异常。
  2. 就算子类没有被用到也会报异常。
  3. 父类中可以做简单的实现,子类可以调用super来扩展该实现。

这时你可能产生如下疑问:

  1. 这个类没有用到为啥可以报异常?
  2. 它是怎样找到这个类的被标记了SUBCLASS_MUST_OVERRIDE的方法的?

对问题的剖析

一切都要从这个宏说起,进入宏的定义可以发现:

1
2
 #define SUBCLASS_MUST_OVERRIDE __attribute__((used, section("__DATA,MustOverride" \
))) static const char *__must_override_entry__ = __func__

是不是感觉有些长?我们可以将该宏拆分:

1
2
     #define SUBCLASS_MUST_OVERRIDE static const char *__must_override_entry__ = __func__
__attribute__((used, section("__DATA, MustOverride" )))

首先定义了一个静态常量指针__must_override_entry__,这个指针指向__func__,也就是该宏所在方法的方法名。然后利用__attribute__(编译器指令,可以在声明时做一些错误检查,或者一些优化),将其放入指定的section中(关于section的定义会在后续章节中加以说明),我们可以在loader.h中看到section是这样一个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
  struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};

关于used的用法我们要到ARM的指令说明中查询

ARM中关于used的说明

从上面可以看出,used的意思是告诉编译器该静态变量要在该对象文件中被保留(尽管该变量是没有被引用的)。被标注的静态变量将会按照声明的顺序,放到指定的一个section中。使用__attribute__((section("name")))可以指明该section.
那么放到section中的静态变量是怎样被使用的呢?
我们可以看到在load方法中,其调用了CheckOverrides函数,也就是在该类加载到Runtime中的时候就被调用,不论其是否被使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Dl_info info;
dladdr((const void *)&CheckOverrides, &info);

const MustOverrideValue mach_header = (MustOverrideValue)info.dli_fbase;
const MustOverrideSection *section = GetSectByNameFromHeader((void *)mach_header, "__DATA", "MustOverride");
if (section == NULL) return;

NSMutableArray *failures = [NSMutableArray array];
for (MustOverrideValue addr = section->offset; addr < section->offset + section->size; addr += sizeof(const char **))
{
NSString *entry = @(*(const char **)(mach_header + addr));
NSArray *parts = [[entry substringWithRange:NSMakeRange(2, entry.length - 3)] componentsSeparatedByString:@" "];
NSString *className = parts[0];
NSRange categoryRange = [className rangeOfString:@"("];
if (categoryRange.length)
{
className = [className substringToIndex:categoryRange.location];
}

BOOL isClassMethod = [entry characterAtIndex:0] == '+';
Class cls = NSClassFromString(className);
SEL selector = NSSelectorFromString(parts[1]);

for (Class subclass in SubclassesOfClass(cls))
{
if (!ClassOverridesMethod(isClassMethod ? object_getClass(subclass) : subclass, selector))
{
[failures addObject:[NSString stringWithFormat:@"%@ does not implement method %c%@ required by %@",
subclass, isClassMethod ? '+' : '-', parts[1], className]];
}
}
}

从中可以看到其从Dl_info中获取了section,
什么是Dl_info,dladdr?我们要从Linux指令集中去查找,
Linux中关于dladdr的说明

从其中的解释可以看出来,dladdr可以用来确定addr指明的地址是否存在于公用的对象中,这些对象是被调用程序所加载的。如果存在那么dladdr会返回公用对象及重叠addr的表示。该信息被封装到了Dl_info结构体中。取出Dl_info结构体中的dli_fbase,然后调用getsectbynamefromheader_64,就可以获取之前存储数据的section。然后遍历该section以找到所有被标识的方法。接下来利用RunTime找到所有的子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
      static NSArray *SubclassesOfClass(Class baseClass)
{
static Class *classes;
static unsigned int classCount;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
classes = objc_copyClassList(&classCount); // 获取项目中所有用到的类
});
NSMutableArray *subclasses = [NSMutableArray array];
for (unsigned int i = 0; i < classCount; i++)
{
Class cls = classes[i];
Class superclass = cls;
while (superclass)
{
if (superclass == baseClass)
{
[subclasses addObject:cls];
break;
}
superclass = class_getSuperclass(superclass);
}
}
return subclasses;
}

判断某个类是否覆盖了方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  static BOOL ClassOverridesMethod(Class cls, SEL selector)
{
unsigned int numberOfMethods;
Method *methods = class_copyMethodList(cls, &numberOfMethods);
for (unsigned int i = 0; i < numberOfMethods; i++)
{
if (method_getName(methods[i]) == selector)
{
free(methods);
return YES;
}
}
free(methods);
return NO;
}

如果没有覆盖则报异常。
小结:MustOverrid在编译期利用__attribute__((used,section("__DATA, MustOverride")))来将方法名放到section中,然后在文件加载到runtime的时候找到这个section,进而找到对应地方法,找到所有的子类,利用runtime判断其是否覆盖了父类的方法。

附:

关于load方法的几点说明:
在类或者分类被加载到Runtime的时候,会触发load方法;并且只会在第一次被加载的时候被调用,所以只会调用一次。
load方法的调用顺序:

  1. 父类先调用+load方法,然后子类再调用。
  2. 分类调用+load方法要晚于原类。

延伸阅读

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0474e/BABHIIEF.html
http://tech.meituan.com/DiveIntoCategory.html
http://man7.org/linux/man-pages/man3/dladdr.3.html
https://www.bignerdranch.com/blog/inside-the-bracket-part-5-runtime-api/
http://nshipster.com/__attribute__/

正则表达式最佳实践

Posted on 2017-05-26

Regular-Expressions
作为一名开发人员,无论是前端,后端,移动端,都可能会接触到正则表达式,最常见的场景就是注册登录了,我们需要对电话号码或者邮箱做校验,如果对用户名有特殊字符有限制的话还会对特殊字符做校验。我们通常的做法就是百度或者谷歌,最后复制粘贴完事,对于那一串奇奇怪怪的字符串感觉很头大。接下来的这篇文章会向你详细解释正则表达式的语法,分析正则表达式,并且附带上常用的表格,最后给出IOS中用到正则表达式的两个类NSRegularExpress,NSPredicate,NSString。

主要内容包括

一、简介
二、正则表达式的PlayGround
三、基本语法表及简介
四、正则表达式实例
五、正则表达式在IOS中
六、常用的正则表达式

简介

正则表达式就是一个用来检索或者替换合乎某种条件的一串字符集。

正则表达式的PlayGround

在学习正则表达式时,面对很多的长篇大论,可能比较枯燥无味,写代码跑项目又比较费时费力。regexpal网站(需要翻墙)刚好可以解决我们的问题,它可以随时测试我们写的正则是否有误,并且有语法检查和语法提示。在接下来的说明中,可以边看边操作了。
正则表达式的PlayGround

基本语法表及简介

正则表达式常用指令集

  • 纯文本形式,比如a就将匹配文本中的a,如果Mike就会匹配文本中的Mike,文本之间是与的关系。
  • \ 其中\是转意字符,表示该字符后面的字母有特殊含义,比如下面要说的\d,\b等,因为在很多语言中,比如0C,Swift中\已经是转意字符,所以需要\\b来表示\b的含义。
  • []匹配里面的任何一个字符,比如p[abcde],将匹配pa,pb,pc,pd,pe。当然可以换成p[a-e],其中-表示“至”的意思,[0-9]表示0到9间的任何一个数字。
  • {}如上表,表示的是匹配的次数,如{6},表示匹配六次,{5,}表示匹配5次以上。比较难以理解的是{2,4}?,这表示最少匹配两次,最多匹配四次,但是,如果四个字母同时出现了,就算两个匹配,如果三个字母出现了,就匹配两次。如:正则[A-Z]{2,3}?,检测MIKEF,就会产生两个匹配MI和KE,可以在上文的网站中练习。
  • .匹配任何一个字符,比如M.M,匹配MuM,MdM,M@M,等。
  • \w匹配很想单词的字符,包含字母,数字,下划线,但是不包含标点符号,及其他字符,比如:hello\w,匹配hello_,hello8,但是不匹配hello!。
  • \d匹配数字,其和[0-9]是同意的。例如\d\d?:\d\d就是可以匹配时间,比如12:30和9:20等。digital单词的首字母
  • \b表示文字的边界,比如空格和标点符号。如go\b将会匹配go home和go!但是不会匹配gone,在需要匹配整个单词的时候往往有用。boundary单词的首字母
  • \s表示空格以及新的一行。比如Hey\s将会匹配Hey man!中的Hey。
  • ^表示一行的开头,比如^Hello将会匹配Hello Everyone!但是不会匹配He said Hello。注意:在[]里面的^表示的非的意思,如:[^DE]表示的是:不是DE的任何字符。
  • $表示行的结尾,例如end$将会匹配it was the end但是不会匹配the end is comming。
  • *表示匹配其前面的字符0次或者很多次,如:go\*d,将会匹配good,goood,gooood,goooooood,gd等。
  • +表示匹配其前面的字符一次或者很多次,如:go+d将不会匹配gd。

注:关于强匹配的概念,如果想了解的可以!在正则ExpressionsInfo网站上学习,捕获的意思就是被捕获的信息可以利用$n的形式来获取并且用来做替换。由于其使用不是很多,就不在赘述。

正则表达式实例

经过上面对正则表达式基本语法的讲解及练习,我们来使用试着写几个正则表达式。

英文名字校验,规则如下:

  • 名字: 标准的英文字母,1到10个字母组成,首字母大写
  • Middle Name简写:标准英文字母,1个字母,大小写都可以
  • 姓:标准的英文字母,可能有'(只能出现一个),比如: O’Brien,长度在2到10个字母,首字母大写’

根据上面的表格我们很容易写出这样的

名字:^[A-Z][a-z]{1,9}$,其中^表示一行的开始[A-Z]表示第一个字母大写[a-z]表示中间的是小写字母,最后{1,9}表示1到9个字母,最后结尾是$
MiddleName: ^[a-x]|[A-Z]$,其中|表示或的意思
姓:^[A-Z]'?[a-z]{1,9}$,其中’?表示’可以出现一次也可以不出现

日期,规则如下:

日期应该在1/1/1900到31/12/2099年之间,并且日期的格式必须是dd/mm/yyyy或dd-mm-yyyy或者dd.mm.yyyy这三种格式:参考上面的速查表,我们可以写出如下的正则表达式来

1
^0[1-9]|([1-2]\d)|3[01][/-.]0[1-9]|[1][012][/-.](19|20)\d\d$

其中日期:0[1-9]|([1-2]\\d)|3[01],也就是穷举了所有的可能01,02,03…9,然后1或者2拼上\d,最后是3可能是30和31
月份:0[1-9]|[1][012],和前面的日期类似,并且更少了,大于10的之后10,11,12几种情况
年份:(19|20)\d\d前面是可能出现的年分19和20,后面就是任意0-9的组合
分隔符:[/-.]只可能出现这三种情况,所以用[]括起来即可

日期加强版,规则如下:

格式是xx/xx/xx或xx.xx.xx或xx-xx-xx,分别是月,日,年,如:10-05-12表示12年10月5日,其中月分可以是英文全拼也可能是缩写,比如January->Jan,February->Feb,日期可能第几天,比如1st,2nd之类的,月日年之间可以有不定的几个空格,如March 13th, 2001:

1
(\d{1,2}[-/.]\d{1,2}[-/.]\d{1,2})|(Jan(uary)?|Feb(ruary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|Aug(ust)?|Sep(tember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?)\s*\d{1,2}(st|nd|rd|th)?+[,]\s*\d{4}

我们可以先用()将其切开,在用|将其切开
先看全是数字的:(\d{1,2}[-/.]\d{1,2}[-/.]\d{1,2})表示两个数字,两个数字的组合,如:10-05-12
然后看字母类型的(Jan(uary)?|Feb(ruary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|Aug(ust)?|Sep(tember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?穷举了所有的月份信息,然后\s*表示任意多个空格,然后是日期\d{1,2}(st|nd|rd|th)?再加若干个空格:\s*最后是四位数字。

时间,规则如下:

时间可以一位或者两位,数字,然后可以有若干个空格,最后是am或者pm,经过以上几个例子,我们不难写出:\d{1,2}\s*[ab]m这样的正则表达式

正则表达式在iOS中的应用

NSRegularExpress

在IOS开发中,我们经常使用这个类来做有关文本校验筛选工作,对于对象的校验经常使用NSPredicate,其使用非常简单,主要包含创建,查找,替换几个方法:

1
2
3
4
5
6
7
8
9
NSError *error = NULL;
NSString *pattern = @"正则表达式";
NSString *string = @"需要校验的文本";
NSRange range = NSMakeRange(0, string.length);
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error]; // 创建RegularExpression对象
NSArray *matches = [regex matchesInString:string options:NSMatchingProgress range:range]; // 找到校验的结果matches中是NSTextCheckingResult对象,该对象中包含各种被查找对象的信息
// 下面两个方法还可以帮们替换掉找到的字符
- (NSString *)stringByReplacingMatchesInString:(NSString *)string options:(NSMatchingOptions)options range:(NSRange)range withTemplate:(NSString *)templ;
- (NSUInteger)replaceMatchesInString:(NSMutableString *)string options:(NSMatchingOptions)options range:(NSRange)range withTemplate:(NSString *)templ;

NSPredicate和正则表达式结合

1
2
3
NSString    *regularExpression = @"正则表达式";
NSPredicate *numberPre = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regularExpression];
return [numberPre evaluateWithObject:textString];

NSString的方法

1
2
-(NSRange)rangeOfString:(NSString *)aString options:(NSStringCompareOptions)mask;
NSRange range = [searchedText rangeOfString:@"正则表达式" options:NSRegularExpressionSearch];

常用的正则表达式

1.验证用户名和密码:”^[a-zA-Z]\w{5,15}$”
2.验证电话号码:(”^(\d{3,4}-)\d{7,8}$”)
eg:021-68686868 0511-6868686;
3.验证手机号码:”^1[3|4|5|7|8][0-9]\d{8}$”;
4.验证身份证号(15位或18位数字):”\d{14}[[0-9],0-9xX]”;
5.验证Email地址:(“^\w+([-+.]\w+)@\w+([-.]\w+)*.\w+([-.]\w+)$”);
6.只能输入由数字和26个英文字母组成的字符串:(“^[A-Za-z0-9]+$”) ;
7.整数或者小数:^[0-9]+([.]{0,1}[0-9]+){0,1}$
8.只能输入数字:”^[0-9]$”。
9.只能输入n位的数字:”^\d{n}$”。
10.只能输入至少n位的数字:”^\d{n,}$”。
11.只能输入m~n位的数字:”^\d{m,n}$”。
12.只能输入零和非零开头的数字:”^(0|[1-9][0-9]
)$”。
13.只能输入有两位小数的正实数:”^[0-9]+(.[0-9]{2})?$”。
14.只能输入有13位小数的正实数:”^[0-9]+(.[0-9]{1,3})?$”。
15.只能输入非零的正整数:”^+?[1-9][0-9]$”。
16.只能输入非零的负整数:”^-[1-9][]0-9″
$。
17.只能输入长度为3的字符:”^.{3}$”。
18.只能输入由26个英文字母组成的字符串:”^[A-Za-z]+$”。
19.只能输入由26个大写英文字母组成的字符串:”^[A-Z]+$”。
20.只能输入由26个小写英文字母组成的字符串:”^[a-z]+$”。
21.验证是否含有^%&’,;=?$\”等字符:”[^%&’,;=?$\x22]+”。
22.只能输入汉字:”^[\u4e00-\u9fa5]{0,}$”。
23.验证URL:”^http://([\w-]+.)+[\w-]+(/[\w-./?%&=]*)?$”。
24.验证一年的12个月:”^(0?[1-9]|1[0-2])$”正确格式为:”01″~”09″和”10″~”12″。
25.验证一个月的31天:”^((0?[1-9])|((1|2)[0-9])|30|31)$”正确格式为;”01″~”09″、”10″~”29″和“30”
“31”。
26.获取日期正则表达式:\d{4}[年|-|.]\d{\1-\12}[月|-|.]\d{\1-\31}日?
评注:可用来匹配大多数年月日信息。
27.匹配双字节字符(包括汉字在内):[^\x00-\xff]
评注:可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1)
28.匹配空白行的正则表达式:\n\s\r
评注:可以用来删除空白行
29.匹配HTML标记的正则表达式:<(\S
?)[^>]>.?</>|<.? />
评注:网上流传的版本太糟糕,上面这个也仅仅能匹配部分,对于复杂的嵌套标记依旧无能为力
30.匹配首尾空白字符的正则表达式:^\s
|\s$
评注:可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式
31.匹配网址URL的正则表达式:[a-zA-z]+://[^\s]

评注:网上流传的版本功能很有限,上面这个基本可以满足需求
32.匹配帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
评注:表单验证时很实用
33.匹配腾讯QQ号:[1-9][0-9]{4,}
评注:腾讯QQ号从10 000 开始
34.匹配中国邮政编码:[1-9]\d{5}(?!\d)
评注:中国邮政编码为6位数字
35.匹配ip地址:((2[0-4]\d|25[0-5]|[01]?\d\d?).){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)。

延伸阅读:

  1. https://www.raywenderlich.com/30288/nsregularexpression-tutorial-and-cheat-sheet
  2. http://www.regexpal.com/
  3. http://nshipster.com/nspredicate/
  4. http://nshipster.com/nssortdescriptor/

WebViewJavaScriptBridge源码剖析

Posted on 2017-05-26

WebViewJavaScriptBridge是IOS中JS和OC交互的常用框架,它利用block的形式处理回调(相关Demo已上传),支持以下两种调用:

基本用法

它的两种使用场景如下:

WebViewJavaScriptBridge使用场景

OC端的方法如下

Method Frome OC
Method 1 是注册一个OC的方法–testObjcCallback,handler是JS掉用的内容,responseCallback是将OC处理返回给JS的回调(对应的是上述第2种调用);
Method 2 是调用JS的方法的testJavascriptHandler方法,@{ @"foo":@"before ready" }是需要传递的参数,responseCallback是将JS处理结果返回给OC的回调(对应的是上述的第1种调用)

JS端的方法如下

Method Frome JS
Method 1 是JS注册一个方法供OC调用,responseCallback(responseData)是将处理结果返回OC。
Method 2 是在点击了一个按钮之后JS调用OC的方法,{'foo': 'bar'}是给OC的参数,response是OC处理后返回给JS的数据。
注:JS中是可以不写;号的,这和swift一样

JS调用OC,OC将处理结果回调给JS:要想被JS调用,我们首先要注册一个handler,和回调的 block,注册时候以键值对的形式存储这个block,handler,当JS调用OC时调用webView:shouldStartLoadWithRequest:navigationType:这个方法,根据JS传来的数据,找到之前保存的Block并且调用,同时新建一个需要把处理结果回调给JS的Blcok,OC处理完结果之后调用刚才创建的Block利用stringByEvaluatingJavaScriptFromString将处理结果返回给JS。
OC调用JS时与此类似。基于这个流程,我们来看WebViewJavaScriptBridge的实现过程。

原理

接下来我们来分析从页面加载到OC和JS互相调用的整个过程:

准备工作

当加载HTML文件的时候调用[webView loadHTMLString:appHtml baseURL:baseURL];,这时会调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }

NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
return NO;
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
return YES;
}
}

在这个方法中判断URL的类型,如果是WebViewJavascriptBridgeURL那么就会判断是BridgeLoadedURL,QueueMessageURL还是未知的URL,在首次调用时是返回YES的,然后的URL就是BridgeLoadedURL,我们在看它的判断条件[self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];Scheme是自己设置的https,那么BridgeLoaded(__bridge_loaded__)是什么呢?我们看ExampleApp.html文件,发现它的script标签中有这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge(function(bridge) {
var uniqueId = 1
function log(message, data) {
var log = document.getElementById('log')
var el = document.createElement('div')
el.className = 'logLine'
el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
if (log.children.length) { log.insertBefore(el, log.children[0]) }
else { log.appendChild(el) }
}

在这里我们发现了https://__bridge_loaded__这个iframe的src,并且在接下来调用setupWebViewJavascriptBridge时这个src会当做一个请求,这时会调用shouldStartLoadWithRequest这个方法。此时就满足了isBridgeLoadedURL这个请求。这时就会调用

1
[_base injectJavascriptFile]

注入一个JS文件,这个JS文件的主要内容是(篇幅问题,有删减):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};

var messagingIframe;
var sendMessageQueue = [];
var messageHandlers = {};
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
var responseCallbacks = {};
var uniqueId = 1;
var dispatchMessagesWithTimeoutSafety = true;

function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
function callHandler(handlerName, data, responseCallback) {
_doSend();
}
function disableJavscriptAlertBoxSafetyTimeout() {
dispatchMessagesWithTimeoutSafety = false;
}
function _doSend(message, responseCallback) {
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}

function _doDispatchMessageFromObjC() {

}
}
}

function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}

messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);

registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}

下面我们来分析下注入的JavaScript的内容。

  1. 给window对象添加一个属性WebViewJavascriptBridge(JS中可以直接给对象添加属性),这个对象包含以下内容:

1) registerHandler:注册调用方法
2)callHandler:调用OC时的方法
3)disableJavscriptAlertBoxSafetyTimeout:超时时弹框是否展示的标示
4)_fetchQueue:获取Queue对象的方法
5)_handleMessageFromObjC:处理OC调用的方法

  1. 定义了一系列的变量来存储数据

messagingIframe:iframe标签,当我们的WebView加载它的时候,会调用其中的src,src就是调用请求的URL。

1)sendMessageQueue:message数组
2)messageHandlers:handler对象 JS中{}表示对象
3)CUSTOM_PROTOCOL_SCHEME:scheme标示
4)QUEUE_HAS_MESSAGE:有Message标识
5)responseCallbacks:回调对象
6)uniqueId:唯一标示ID

进过系列一的剖析,我们明白了使用WebViewJavaScriptBridge前需要做的准备工作,那么接下来,我们一起探讨OC和JS相互调用的具体执行过程以及其中的要点。

JS调用OC,然后OC将处理结果返回JS

OC首先注册JS将调用的方法

OC调用registerHandler:,这时将其调用信息存储在messageHandlers字典中以handlerName为Key,给JS处理结果的Block为Value(_base.messageHandlers[handlerName] = [handler copy]);

在JS中调用被注册的方法

JS调用

1
2
3
 bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})

来调用上文OC注册的方法,这个brige就是上文注入JS代码时候创建的,我们再它内部做了什么。

1
2
3
4
5
6
7
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}

这里判断了参数类型,如果传入的参数只有两个,并且第二个是function类型,那么就将第二个参数变为callBack,data置空,将handlerName和data转化成一个对象的两个属性并传给_doSend()。

1
2
3
4
5
6
7
8
9
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

这里的responseCallback是JS先调用OC然后OC调用JS时才会有的,如果这种情况,那么需要用唯一的标识(callbackId),来将这个responseCallback存储在responseCallbacks中,并且给message添加callbackId 这个属性。这个数值会在下次OC调用JS的时候作为唯一的Key被用到。软后将message放入:sendMessageQueue队列中,然后拼接src。

在回掉方法中拦截相应的方法,然后调用block

经过方法步骤2,会调用下面的回调方法

1
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{}

在这个方法中调用

1
2
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];

首先获取JS中的messageQueue(步骤2中的sendMessageQueue),然后调用flushMessageQueue:方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];

/////////*********OC先调用了JS,JS再调用了OC*********///////////
NSString* responseId = message[@"responseId"];
if (responseId) {
//调用之前存储的Bolck
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];

/////////*********JS先调用OC,OC再调用JS*********///////////
/// 这里是JS先调用OC的时候存储的是 JS的回调函数
} else {

// JS先调用的OC,OC再调用JS
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
//JS调用OC时候的存储(后续OC调用JS返回计算结果)
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}

WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
//调用OC的Block,同时,如果OC调用responseCallback,则调用_queueMessage进行相应的处理
handler(message[@"data"], responseCallback);
}
}

这里先将返回的JSON字符串转换成对象,这里的字符串是调用

1
2
3
4
5
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}

获取的,这里将sendMessageQueue转为JSON,然后将其置空,这里为啥使用数组而不用对象来存储呢?因为可能JS还没有处理结束就有两次调用,要保证他们不丢失使用了数组。然后判断数组中的Message对象是否有responseId(JS调用OC第一次时存储的),这里没有responseId所以走else:如果有callbackId(在JS中作为回调用的),定义responseCallback,这个block就是OC将处理结果返回给JS时用到的block。如果没有callbackId说明,不需要回调JS,这个时候responseCallback为空。最后调用步骤1中存储在messageHandlers对象中的block,并且将刚才创建的responseCallback作为参数传入,以便OC将计算结果传递给JS。

OC将计算结果返回给JS

1
2
3
4
5
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {

NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"response form oc's call back");
}];

在handler的最后一步调用responseCallback()将处理结果回调给JS。这个responseCallback()就是我们在步骤3中创建的responseCallback。我们再来看这个block。看步骤3可以看到这个其内部调用

[self _queueMessage:msg];
[self _dispatchMessage:message];

在_dispatchMessage内部执行:

1
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];

接下来JS中的_handleMessageFromObjC就会接收到OC传过来处理结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;

if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
// OC先调用JS是用到
}
}

这个时候我们看到了步骤三中的responseId的作用了,这时候responseId就表明了是OC将处理结果传递给JS并不需要JS再调用OC了,这时只调用responseCallback(message.responseData);将数据传给JS。
这样我们就完成了JS调用OC,然后OC将结果回调给JS的全部过程。

OC调用JS,然后JS将处理结果返回给OC

JS注册相应的方法供回调

同OC注册方法时候一样,JS也是用一个messageHandlers对象来存储

1
2
3
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}

OC调用JS时存储调用信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];

if (data) {
message[@"data"] = data;
}

if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}

if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}

这里使用message字典来存储参数,方法名,使用responseCallbacks来存储JS处理完之后,需要回调的Block(这里为了确保多次调用不会覆盖之前的调用,使用了唯一的callbackId)。
同上文所述,最终会调用

1
2
3
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
}

JS调用_dispatchMessageFromObjC

这时message没有responseId,会走else,

1
2
3
4
5
6
7
8
9
10
11
12
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}

这里定义了需要给OC传递结果的responseCallback,取出之前注册的handler:messageHandlers[message.handlerName],然后调用这个handler,并将这个responseCallback作为参数传进去,handler(message.data, responseCallback);

JS将结果回传给OC

在步骤三中调用handler:

1
2
3
4
5
6
 function(data, responseCallback) {
log('ObjC called testJavascriptHandler with', data)
var responseData = { 'Javascript Says':'Right back atcha!' }
log('JS responding with', responseData)
responseCallback(responseData)
}

在这个handler的结尾调用步骤三种的responseCallback(传入的只有数据没有回调),根据步骤三可以看出来其会调用_doSend方法。该方法中由于没有传进去回调,所以不会给message对象添加callbackId,只调用:

1
2
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;

这是由于含有responseId(在步骤三中的_doSend调用时设置),所以只会取出之前存储的block,并且将结果回传给OC:

1
2
3
4
//调用之前存储的Bolck
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];

至此,OC和JS交互的所有逻辑已介绍完毕(WKWebView实现方式相同),总结下两种情景的回调,其实现方式及其相似,正如文章开头的总结。

SDWebImage学习笔记(一)

Posted on 2017-04-26

保证一段代码在主线程中运行,怎么做更好?

可以使用一个宏来替代,这样代码更加整洁,如

#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
    block();\
} else {\
    dispatch_sync(dispatch_get_main_queue(), block);\
}
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
    block();\
} else {\
    dispatch_async(dispatch_get_main_queue(), block);\
}

Block除了常见的回调,还有什么应用场景?

在具体的处理方式需要客户端传入时。
如:

1
2
typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);
@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;

该处就是利用Block将处理CacheKey的方法开放给了客户端。通过block的返回值获取。其实这里用代理也可以实现,但是代理相对来说代码量会更多,并且代码较为分散。

我们在加锁的时候一直用@synchronized (self)合理吗?

不合理,@synchronized (objc),只要这个objc是同一个对象,那么就会获得同一把锁。如果访问的是两种不同的资源,那么就需要使用两种不同的objc,比如:

1
2
3
4
5
6
   @synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}

这里分别是对runningOperations和failedURLs同步,那么就需要使用两种不同的objc,当然都用self不能算错,但是将不需要同步的代码同步了,就降低了系统的性能。另外使用self,还容易引起死锁,比如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
  //class A
@synchronized (self) {
[_sharedLock lock];
NSLog(@"code in class A");
[_sharedLock unlock];
}
//class B
[_sharedLock lock];
@synchronized (objectA) {
NSLog(@"code in class B");
}
[_sharedLock unlock];

如果要让数组中的每个对象都调用某个方法怎么做?

1
2
- (void)makeObjectsPerformSelector:(SEL)aSelector
- (void)makeObjectsPerformSelector:(SEL)aSelector withObject:(nullable id)argument

以上两个方法可以实现,而不需要分别遍历每个对象,然后分别调用performSelector:

内存缓存为啥要用NSCache?

NSCache和NSDictionary极其相似,他的方法如下:

1
2
3
4
5
6
7
8
- (nullable ObjectType)objectForKey:(KeyType)key;
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;//该方法不常用,因为精确地计算对象所占的字节是很费力的,并且计算也会影响缓存的效率。
- (void)removeObjectForKey:(KeyType)key;
- (void)removeAllObjects;
@property NSUInteger totalCostLimit; // limits are imprecise/not strict
@property NSUInteger countLimit; // limits are imprecise/not strict
@property BOOL evictsObjectsWithDiscardedContent;//内存吃紧时是否删除废弃的对象

从中可以看到,他和NSMutableDictioary非常相似,但是为啥在做内存缓存时要用它呢?原因在于:

  1. 当系统资源将要耗尽时,它可以自动删减缓存。如果采用字典,那么就要自己编写相关逻辑,在系统发出“低内存”通知时手工删减缓存。
  2. NSMutableDictionary是非线程安全的,而NSCache是线程安全的。
  3. NSMutableDictionary中的key必须实现NSCopying协议,NSCache中的key不必实现copy因为它是”保留”键的(强引用),而不是”拷贝”键的。
  4. 如果缓存设置超过了设置的最大值,则会清除旧的数据,保留最新缓存的数据。

FOUNDATION_STATIC_INLINE放在方法名前有何作用?

1
2
3
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
return image.size.height * image.size.width * image.scale * image.scale;
}

内联函数的代码会被直接嵌入在被调用的地方,调用几次就嵌入几次,没有使用call指令,这样就减少了在函数调用过程中保存现场(压栈)恢复现场(弹栈)的操作,可以加快执行速度。不过调用次数多的话,会使可执行文件变大,这样会降低速度。相比起宏来说,内核开发者一般更喜欢使用内联函数。因为内联函数没有长度限制,格式限制。编译器还可以检查函数调用方式,以防止其被误用。

inline(内联函数)在什么时候使用?

在SDWebIamgeCompat中使用了inline UIImage *SDScaledImageForKey(NSString *key, UIImage *image),这是个内联函数(函数代码被放入符号表中,在使用时进行替换,比调用一般的函数更加高效),那么我们在什么时候使用内联函数呢?经过查找相关资料,总结下inline的使用场合:
使用inline的场合:

  1. 想要使用inline替换#define时。
  2. 短函数。(如果函数的代码较长,使用内联将消耗过多栈内存)
  3. 函数调用很频繁。

不应使用inline的场合:

  1. 很大的函数。
  2. 和I/O相关的函数。
  3. 构造函数和析构函数。
  4. 在开发框架时候,使用inline可能会破坏框架的兼容性。

获取某个目录下文件的个数:

1
2
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
count = [[fileEnumerator allObjects] count];

如何让某个属性只在固定版本的时候才会有?

1
2
3
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
#endif

上面这段代码就可以让backgroundTaskId在iPhone的版本在4.0以后才会有。

Notification的方法调用所在的线程是根据Post时候所在线程决定的

1
2
3
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});

这样注册该通知的对象就可以在主线程中调用响应的方法了。

如何保持后台下载图片的线程一直存在?

使用RunLoop可以让线程常驻(具体解释在我的[实例化讲解RunLoop]中有说明(https://mikefighting.github.io/2016/04/25/understanding-run-loop/)),调用`CFRunLoopRun()`和`CFRunLoopStop(CFRunLoopGetCurrent())`分别用来开始和结束一个RunLoop

分类中需要填加属性怎么办?

如果分类中的属性只是分类内部使用,那么其实可以直接使用关联,而不必非要显式创建一个属性,这样也可以直接使用.语法,这时没有属性,所以.语法的无论是在=左边,还是在=右边最终都会调用这个方法,例如:

1
2
3
4
5
6
7
8
9
static char imageURLStorageKey;
- (NSMutableDictionary *)imageURLStorage {
NSMutableDictionary *storage = objc_getAssociatedObject(self, &imageURLStorageKey);
if (!storage){
storage = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &imageURLStorageKey, storage, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return storage;
}

如何让一个id类型的对象调用某个具体的方法?

让这个对象遵守某项协议就可以调用,如下

1
2
3
4
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}

同理,如果想让某个方法的返回值具有某个方法,也可以让这个返回值遵守某协议,如:

1
2
3
4
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress: (SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

设计接口时,要尽量考虑使用者的习惯,并对常见错误进行处理

在需要传入URL参数的地方,使用者很可能不小心传入了字符串,这个时候要么在方法中抛出异常,要么就在内部判断类型并替使用者做相应的转换,如:

1
2
3
4
5
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url {
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
}

某个类添加通知的方式

很多时候某个类要发出通知,我们经常放到宏里,但是如果这两个类是相关的,我们其实可以将通知放到对应的头文件中,然后在.m文件中将其赋值。

1
2
3
4
///.h
extern NSString *const SDWebImageDownloadStartNotification;
///.m
NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";

使用NSURLConnection时,怎样控制是否缓存请求到的数据?

1
2
3
4
5
6
7
8
9
10
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
responseFromCached = NO; // If this method is called, it means the response wasn't read from cache
if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
// Prevents caching of responses
return nil;
}
else {
return cachedResponse;
}
}

如果这里返回nil,那么将不会缓存这个response,如果返回cachedResponse表示可以缓存这个response.

IOS5.0之后,如果请求和响应满足以下条件,系统就会在如下目录中生成一个Cache.db这样一个数据库来存储缓响应的数据。
生成的缓存目录
在缓存期间如果访问相同的URL,那么就会直接从这个数据库中得到相应的数据;同时在系统的内存告紧时,会自动把内存缓存清空。
这个缓存协议被回调的条件是:

  • HTTP或者HTTPS请求(如果是自定义的协议,那么协议需要支持缓存)
  • 请求必须是成功的(状态码为200-299)
  • 响应必须是服务端传回来的,而不是本地缓存传回来的
  • 进行该请求的NSURLRequest对象的cachePolicy是允许缓存的
  • 服务的响应头含有支持缓存的字段
  • 响应的内容大小没有超过缓存的大小(例如,提供磁盘缓存时,响应内容不能超过磁盘缓存的5%)

注意:如果要自定义NSURLCache,那么在自定义NSURLCache进行数据缓存时,一定要在- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions中进行初始化,

Cocoapods多版本共存并自由切换

Posted on 2017-04-25

Cocoapods是iOS中的第三方框架管理工具,一台电脑为什么要安装多个版本的Cocoapods呢?在公司里可能存在不同的IOS开发团队分别对不同的业务线进行开发,各个团队之间所用的Cocoapod版本不同,这时你被外派到另外一个团队做开发。

Rubyems:简称gems是一个用于对rails组建近些年个打包的ruby打包系统,它提供了一个分发ruby程序喝库的标准格式,还提供了一个管理程序包的工具。Rubyems的功能类似于linux下的apt-get,是个包管理器,可以从远程下载所需的包。
gem:你可以这样理解,gem是一系列文件和包的总称,是一些rails项目依赖的软件或者环境,或者是依赖的关系库,当你的项目中缺少的时候,你可以用gem install 来进行安装,这种安装是通过RubyGems这个包管理工具来安装的,当然你也可以通过bundleer来安装。
RVM:Ruby Version Manager,ruby版本管理工具,利用它可以很方便的安装多个版本的Ruby。

实现的原理

通过RVM来安装多个版本的ruby,再根据不同版本的ruby来安装相应版本的cocoapods,最后使用rvm use命令切换不同的ruby环境来使用不同版本的cocoapods.

常用的几个指令

*ruby -v查看rugy的版本()
*rvm -v (查看rvm的版本)
*gem sources -l(查看gem shources)
*rvm list(查看已安装的所有版本:ruby)
*rvm use rubyVersion(使用某个版本的ruby),例如:rvm use ruby-2.3.3
*rvm install rubyVersion(安装某个版本的ruby),例如:rvm install 2.3.3
*rvm use rubyVersion --default(将某个版本的ruby设置为默认版本),例如rvm use 1.9.3 --default
*rvm remove rubyVersion(删除某个版本的ruby),例如:rvm remove 1.9.3
*rvm list known查看所有可用的ruby版本
*sudo gem install cocoapods -v <Version> -n /usr/local/bin安装cocoapod
*gem list查看当前gem下的所有安装包

实现步骤

步骤一、 执行rvm -v,如果发现没有rvm则执行curl -L get.rvm.io | bash -s stable && source ~/.rvm/scripts/rvm安装rvm.
步骤二、 执行rvm list known查看所有可用的ruby版本,然后执行rvm install someVersion来执相应版本的ruby; 或者从ruby官网上下载不同版本Ruby时,一定要下载osx操作系统的,否则在执行rvm mount ~/Downloads/ruby-2.3.3.tar.bz2时,将会出现Libraries missing for ruby-2.3.3: xcrun. Refer to your system manual for installing libraries,下载完之后到响应的目录下执行rvm mount ruby-2.2.3.tar.bz2就可以安装对应的ruby。
步骤三、重复执行步骤二,安装不同版本的ruby,

各种错误及处理方式

cannot execute binary file这种错误

执行完:rvm use ruby-2.3.3和sudo gem install cocoapods之后出现:/Users/a58/.rvm/rubies/ruby-2.3.3/bin/ruby: /Users/a58/.rvm/rubies/ruby-2.3.3/bin/ruby: cannot execute binary file这种错误。

Gemset’’does not exist

执行完rvm use ruby-2.2.3,出现:

1
Gemset''does not exist,'rvm ruby-2.2.3 do rvm gemset create ' first, or append '--create'.

这种错误是由于没有设置default,在执行rvm list的时候会出现如下`

1
# Default ruby not set. Try 'rvm alias create default <ruby>'.

这样的提示。使用rvm --create ruby-2.1.9 之后这种提示消失。逐个将其他版本的ruby也使用rvm --create rubyVersion这个指令,然后就可以切换至不同版本的ruby了。

ERROR:Could not find a valid gem

在执行sudo gem install cocoapods来安装cocoapods 的时候,

1
2
ERROR:Could not find a valid gem 'cocoapods' (>= 0), here is why:
Unable to download data from https://ruby.taobao.org/ - SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed (https://ruby.taobao.org/specs.4.8.gz)

这是因为淘宝镜像最近出问题了,使用gem sources -a http://rubygems-china.oss.aliyuncs.com再安装一个镜像,然后可以执行sudo gem install cocoapods了,但是在执行pod -v,pod search AFNetworking,pod setup时却发现:

1
/Users/a58/.rvm/rubies/ruby-2.2.3/lib/ruby/2.2.0/rubygems/dependency.rb:315:in 'to_specs': Could not find 'cocoapods' (>= 0) among 6 total gem(s) (Gem::LoadError)`。

xcrun: error: active developer path…

在执行rvm install 2.1.0时报错:

1
`xcrun: error: active developer path ("/Applications/Xcode.app/Contents/Developer") does not exist, use 'xcode-select --switch path/to/Xcode.app' to specify the Xcode that you wish to use for command line developer tools (or see 'man xcode-select')

这是因为rvm寻找的路径是/Applications/Xcode.app/Contents/Developer,而我的Xcode被我修改成了Xcode8.0,找不到路径了,所以把Xcode的名字改过来就好了。

Error running…

需要更新Homebrew

1
2
Error running 'requirements_osx_brew_update_system ruby-2.1.0',
showing last 15 lines of /Users/a58/.rvm/log/1487732911_ruby-2.1.0/update_system.log`

这个时候需要更新Homebrew,执行brew update来更新Homebrew,这时却发现Error: /usr/local must be writable!,然后点击Command + Shift + G,然后输入/usr这个时候就看到usr目录,找到下面的local文件夹,右击”Get Info”,将最下面的权限中的everyone改为可读写的,这时就可以执行brew update指令了。执行完之后再执行rvm install ruby 2.2.2,就可看到如下图所示,就说明ruby安装成功了: 执行sudo gem install cocoapods这个指令就可以成安装cocoapods了,接着执行pod --version,就可以查看当前的pod版本号了:

1
2
/Users/a58/.rvm/gems/ruby-2.2.2@global/gems/cocoapods-1.2.0/lib/cocoapods/executable.rb:89: warning: Insecure world writable dir /usr/local in PATH, mode 040777
1.2.0

在执行完brew update之后再执行有关pod的指令还是会报错

1
/Users/a58/.rvm/rubies/ruby-2.2.3/lib/ruby/2.2.0/rubygems/dependency.rb:315:in 'to_specs': Could not find 'cocoapods' (>= 0) among 6 total gem(s) (Gem::LoadError)

这样的错误。然后把现有的cocoapod卸载,执行:

1
sudo gem uninstall cocoapods

卸载完之后执行

1
sudo gem install cocoapods -v 1.2.0 -n /usr/local/bin

这时依然会出现这个错误

在使用2.0.0版本的ruby安装pod的时候出现如下错误

1
2
3
4
5
6
ERROR:SSL verification error at depth 1: unable to get local issuer certificate (20)
ERROR:You must add /O=Digital Signature Trust Co./CN=DST Root CA X3 to your local trusted store
ERROR:SSL verification error at depth 2: self signed certificate in certificate chain (19)
ERROR:Root certificate is not trusted (/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA)
ERROR:While executing gem ... (Errno::EPERM)
Operation not permitted - /usr/bin/pod

Operation not permitted -

在osx是10.11.6的时候,gem update –system会出错

1
ERROR:While executing gem ... (Errno::EPERM) Operation not permitted - /usr/bin/update_rubygems

这时候需要到rubyGem的官网现在最新的zip文件,解压进入到rubygems-2.6.10文件中,然后执行ruby setup.rb就可以安装gem了。

missing bin/ruby

删除某个版本的ruby的时候,出现:

1
ruby-2.2.3 [ missing bin/ruby ]

这时前往/Users/用户名/.rvm/rubies/ruby-2.2.3,然后删除对应的ruby-2.2.3即可。

rvm instlall 2.2.3 报错

1
`Empty path passed to certificates update, functions stack: requirements_osx_update_openssl_cert_run rvm_requiremnts_fail_or_run_action __rvm_osx_ssl_certs_ensure_for_ruby __rvm_osx_ssl_certs_ensure_for_ruby_except_jruby external_import_setup external_import main`,

这时执行rvm reinstall 2.2.3 --disable-binary这个时候又出现错误:

1
2
dyld: lazy symbol binding failed: Symbol not found: _clock_gettime
dyld: Symbol not found: _clock_gettime

其原因在于没有安装Xcode的CommandLineTools工具,执行下面的代码:xcode-select --install即可。

ERROR:While executing gem…(TypeError)

安装pod时候出现:

1
2
`ERROR:While executing gem ... (TypeError)
no implicit conversion of nil into String

执行:gem update –system

利用Core Graphics实现刮奖效果

Posted on 2017-04-10

刮奖是商家类项目中经常使用的组件,其实现方式也有多种,下面介绍一种使用Core Graphics实现的一种方式。

Core Graphics中有一种根据遮罩图片(Masking Images)和原图片最终合成位图的方法,下面看一个官方文档给出的效果来一个直观的展示。
原图
遮罩图
合成图
从中可以看到黑色的部分将原图显示了出来,白色的部分把原图遮住了,灰色的部分和原图经过一定的算法进行了合成。我们可以通过不断的改变遮罩层中某部分的颜色,最终产生刮奖的效果。具体步骤如下:
实现步骤
从中可以看出,刚开始产生的Marsk是黑色的,这时合成之后蒙层图片原样展示,手指一动的时候往mask上绘制了白色的线条,这样,合成之后蒙层上被划过的地方被白色所取代,这样就出现了刮奖的效果,在每次绘制完成之后只需要调用setNeedsDisplay方法,然后在drawRect方法中不断展现最终合成的图片即可。
具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceGray();
float scale = [UIScreen mainScreen].scale;
//1. 获取刮奖层
UIGraphicsBeginImageContextWithOptions(hideView.bounds.size, NO, 0);
[hideView.layer renderInContext:UIGraphicsGetCurrentContext()];
hideView.layer.contentsScale = scale;
hideImage = UIGraphicsGetImageFromCurrentImageContext().CGImage;
UIGraphicsEndImageContext();

size_t imageWidth = CGImageGetWidth(hideImage);
size_t imageHeight = CGImageGetHeight(hideImage);
CFMutableDataRef pixels = CFDataCreateMutable(NULL, imageWidth * imageHeight);
//2. 获取context手指滑动时不断在这个context上画上白线。
contextMask = CGBitmapContextCreate(CFDataGetMutableBytePtr(pixels), imageWidth, imageHeight , 8, imageWidth, colorspace, kCGImageAlphaNone);
CGContextFillRect(contextMask, self.frame);

// 设置滑动时候产生的线条颜色是白色
CGContextSetStrokeColorWithColor(contextMask, [UIColor whiteColor].CGColor);
CGContextSetLineWidth(contextMask, _sizeBrush);
CGContextSetLineCap(contextMask, kCGLineCapRound);

CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData(pixels);
CGImageRef mask = CGImageMaskCreate(imageWidth, imageHeight, 8, 8, imageWidth, dataProvider, nil, NO);

//2. 根据iamge mask产生最终的图片
scratchImage = CGImageCreateWithMask(hideImage, mask);
CGImageRelease(mask);
CGColorSpaceRelease(colorspace);

手指滑动时候调用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
UITouch *touch = [[event touchesForView:self] anyObject];
currentTouchLocation = [touch locationInView:self];
previousTouchLocation = [touch previousLocationInView:self];
[self scratchTheViewFrom:previousTouchLocation to:currentTouchLocation];
}

// 绘制图像
- (void)scratchTheViewFrom:(CGPoint)startPoint to:(CGPoint)endPoint {

BOOL needRender = [self needRenderWithCurrentLocation:endPoint previousLocation:previousTouchLocation];
if (!needRender) return;

float scale = [UIScreen mainScreen].scale;
CGContextMoveToPoint(contextMask, startPoint.x * scale, (self.frame.size.height - startPoint.y) * scale);
CGContextAddLineToPoint(contextMask, endPoint.x * scale, (self.frame.size.height - endPoint.y) * scale);
CGContextStrokePath(contextMask);
// 调用drawRect 方法
[self setNeedsDisplay];
self.isDrawn = YES;
}
- (void)drawRect:(CGRect)rect {
UIImage *imageToDraw = [UIImage imageWithCGImage:scratchImage];
[imageToDraw drawInRect:CGRectMake(0.0, 0.0, self.frame.size.width, self.frame.size.height)];
}

IOS中AOP框架Aspects源码分析

Posted on 2017-04-07

iOS中AOP框架Aspects源码分析

AOP是Aspect Oriented Programming的缩写,意思就是面向切面编程。具体的解释可以到维基百科上或者其它地方查看。在IOS中使用Swizzle技术可以实现面向切面编程,我在RunTime应用实例–关于埋点的思考博文中也提到了Aspects框架,下面就来对该框架做以分析。

一般的Swizzle是怎么实现的?

在RunTime应用实例–关于埋点的思考,也讲到了MethodSwizzle的技术,下面来看最常见的实现方案。比如我们要Hook住UIButton的sendAction:to:forEvent:,这时候我们一般这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originSEL = @selector(sendAction:to:forEvent:);
SEL swizzleSEL = @selector(swizzleSendAction:to:forEvent:);
Class processedClass = [self class];
Method originMethod = class_getInstanceMethod(processedClass, originSEL);
Method swizzleMethod = class_getInstanceMethod(processedClass, swizzleSEL);
method_exchangeImplementations(originMethod, swizzleMethod);
});
}
- (void)swizzleSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
// 执行相应的逻辑
// 调用原来的系统方法
[self swizzleSendAction:action to:target forEvent:event];
}

具体的解释可以参见RunTime应用实例–关于埋点的思考这篇文章。

这样做有什么弊端?

  • 我们要给每一个要Hook的方法额外新加一个方法,方法的参数个数是一样的。
  • 如果每个被Hook的方法内部的实现逻辑都一样,那么就需要在每个新添加的方法中调用这段实现逻辑。
  • 我们新加的代码是在被Hook方法之前还是之后调用的呢?
  • 我们能放将这个新加的方法转化为一个Block呢?这样代码会更紧凑,逻辑更清晰。
  • 如果这个方法转化为Block,那么如何将这个Block替换掉原来的方法实现Swizzle呢?怎样在合适的时候调用这个block呢?

带着这些问题我们来看Aspects是如何实现的。

主要的实现思路

  1. 使用和原方法相同参数不同方法名的方法,替换被hook的方法,这样系统在找不到这个方法的时候就会走到forwardInvocation:。
  2. 使用__ASPECTS_ARE_BEING_CALLED__替换掉系统的forwardInvocation:。
  3. 给类增加AspectsForwardInvocationSelectorName方法,它的实现是原来的forwardInvocation:的IMP。
  4. 当要hook的方法被调用时,系统会调用forwardInvocation:方法。由于这个方法也被替换掉了,所以会调用__ASPECTS_ARE_BEING_CALLED__。
  5. __ASPECTS_ARE_BEING_CALLED__内部,先调用被hook方法之前的block,再调用替换被hook方法的block,以及没有替换的实现,最后调用被hook方法之之后的block。
  6. 如果hook出错,则再调用原来的AspectsForwardInvocationSelectorName的方法。

Aspects.m包含的内部类及分类

  • AspectInfo:存储被hook方法的信息。
  • AspectIdentifier:记录每一次Aspect的信息。
  • AspectsContainer:某个类或者某个对象所有被hook方法的集合。
  • AspectTracker:对所有hook方法的操作(增加或者减少)。
  • NSInvocation (Aspects):获取NSInvocation的参数。
  • NSObject (Aspects):框架的主要分类,定义公共接口。

各类之间的关系如图:
Aspect框架各类之间的结构

执行流程

公共方法主要调用:aspect_add方法,该方法内部主要调用三个方法

  1. AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);获取该方法对应的AspectsContainer。
  • 将要hook的方法转化为aliasSelector。
  • 取得关联的AspectsContainer,如果没有,怎设置关联的AspectsContainer;
  1. identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];,将调用的参数封装成AspectIdentifier。
  • 通过:aspect_blockMethodSignature取的block对应的NSMethodSignature,
  • 创建AspectIdentifier并且返回
  1. aspect_prepareClassAndHookSelector(self, selector, error);,准备工作及hook方法。
  • aspect_hookClass,内部调用aspect_swizzleForwardInvocation,将系统的forwardInvocation: 替换为__ASPECTS_ARE_BEING_CALLED__。
  • 在改方法内部分别调用被hook方法之前,替换被hook方法,被hook方法之后的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 // Before hooks.  
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);
// Instead hooks.
BOOL respondsToAlias = YES;
if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
}else {
Class klass = object_getClass(invocation.target);
do {
if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
[invocation invoke];
break;
}
}while (!respondsToAlias && (klass = class_getSuperclass(klass)));
}
// After hooks.
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);

从中可以看出Aspect重要使用了NSInvocation来避免了不同参数个数的限制,通过将block转换为NSMethodSignature对象,可以实现对原有方法的替换或者Swizzle,然后调用其invoke方法,可以在适当的时机触发这个block,通过标识将block分为,原方法之前,之后,替换原方法等做法。而不必像一般实现方法那样,改变每个新加方法中调用原来方法的位置来实现。

其它Tips

  1. 如果某个类只可能被其它一个类用到,那么可以将它们写到一个.m文件中,这是高内聚的一种表现。
  2. 类,包括分类的出现,其实是为了更好的组织代码,让代码的功能更清晰易懂。
  3. 将一段代码加锁的方式:可以将代码块作为参数,然后对其执行前后加锁,这样做的好处是:如果以后要换锁的类型,那么只要在这个方法中改变就可以了:例如:
1
2
3
4
5
6
static void aspect_performLocked(dispatch_block_t block) {
static OSSpinLock aspect_lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&aspect_lock);
block();
OSSpinLockUnlock(&aspect_lock);
}
  1. 可以通过自定义结构体的方式将block转换成NSMethodSignature对象,然后在适当的时候调用。
1
2
- (void)invoke;
- (void)invokeWithTarget:(id)target;

这两个方法来触发这个block。具体做法请见:AspectBlockRef这个结构体和aspect_blockMethodSignature这个方法。这里有一个疑问:根据苹果官方提供的block定义,其中是没有signature这个字段的:

1
2
3
4
5
6
7
/* Revised new layout. */
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};

但是Aspect中的block转化为响应的结构体:AspectBlockRef layout = (__bridge void *)block;之后就会自动有signature字段,这点不是很理解还望大神指教。

  1. 给定一个对象,和这个对象的SEL,如果参数非常多,使用performSelector:withObject:这种方式不太合适,这时可以借鉴YYKit中的NSObject+YYAdd里面的方法,如果这时有NSInvocation对象,那么可以直接调用objc_msgSend(self, SEL, invocation)这种方式来实现。

RunTime应用实例--关于埋点的思考

Posted on 2016-06-07

埋点是现在很多App中都需要用到的,这个问题可能每个人都能处理,但是怎样来减少埋点所带来的侵入性,怎样用更加简洁的方式来处理埋点问题,怎样减少误埋,如果上线了发现少埋了怎么办?下面是本文讨论的重点(本文Demo已上传GitHub,可以下载讨论):

  • 什么是埋点?埋点的作用是什么?
  • 常规的处理方式是怎样的?
  • 我们可以怎样优化?
  • 怎样使用RunTime对其进行优化?
  • 在实践中遇到了什么问题以及解决方案?
  • 最理想的埋点是什么样的?
  • 其中可能存在的问题是什么?

接下来将对其一一做以说明:

什么是埋点?埋点的作用是什么?

其实埋点也叫日志上报,其实就是根据需求上报一系列关于用户行为的数据,比如:用户点击了哪个按钮,用户浏览了哪个网站,用户在某个页面停留了多久等数据。这些数据对于运营来说很有用,他们可以用来分析某个功能开发的是不是合理,是不是因为某个地方的不合理而到导致了转化率的下降,从而对我们的App进行相应的改进,我们来看下某个第三方平台提供的埋点实例。
埋点统计字段定义
上图中说明了,某个时间对应的事件ID,以及针对这个事件需要关联的字段。下面是后台系统对某个埋点所做的数据统计:
后台系统对埋点的数据分析

这样我们就可以详细的分析出用户对于App的反馈,从而及时的修改我们的产品。

常规的埋点的做法是怎样的?

其实很简单,我们就在相应的事件里面加入相关的代码,给服务器上报数据不就得了。如下所示:

1
2
3
4
5
6
7
8
9
// 这个一个按钮的响应事件
- (void)someButtonAction:(UIButton *)someButton{
// 该按钮需要处理的业务
[self upDateSomthing]
// 开始埋点
// eid:事件id,sa:用户id, cI:当前时间
NSDictionary *upLoadDic = @{@"eid":@"311",@"sa":@"706976487532177",@"cI":@"2016-6-4 12:11:34"};
[ZHUpLoadManager upLoadWithDic:upLoadDic];
}

这样一个埋点问题就解决了,单同时却隐藏着很多问题:1.这样每点击一个一下按钮就请求一次网络会不会出现性能问题?2.如果这样频繁的数据上报会不会消耗更多的用户流量?3.这样的代码能经受住需求的变更吗?比如字段变了,或者你把cI看错了,应该是cl。4.这样的代码会不会造成难以测试?5.这样的频繁上报会不会增加服务器端的压力?6.代码整洁吗?……(程序员的一个好习惯是:这个代码能否经受住需求的变更。)

我们可以怎样优化?

  1. 首先我们可以用一个类,来专门处理这些需要上报的埋点的字段,将这些字段作为常量,例如:
1
2
3
4
5
6
7
8
// LogManager.h
extern NSString * const kLogEventKey; //事件id
extern NSString * const kLogUserIdKey; //用户id
extern NSString * const kLogOperationInterval; //操作时间
// LogManager.m
NSString * const kLogEventKey = @"co"; //事件id
NSString * const kLogUserIdKey = @"sa"; //用户id
NSString * const kLogOperationInterval = @"cq"; //操作时间
  1. 对于用户id,当前时间,用户手机型号,手机品牌,等等与用户所在页面无关的内容,可以用统一的一个类进行处理,将其作为这个类的一个属性,使用getter方法将其相应的数值返回即可(对于恒定不变的可以使用懒加载)。
  2. 这样的数据传输策略是有问题的,每次点击都上报,可能一个面需要上报的地方很多,这就会造成很大的性能问题,我们可以先将需要上传的数据缓存起来,然后缓存够50条数据上报一次,或者每隔5分钟上报一次;
  3. 为了节省流量我们可以,1)将数据压缩之后再上报,可以参考我的另一篇文章;2)和服务端商量,用尽可能短的字段,如:cityName = @"北京";变为cn = @"北京";3)尽量不要上传的频率过高,如第三点。
  4. 如何解决代码的整洁,易于测试的问题?请看下面。

怎样使用RunTime来进行优化?

我么能不能利用RunTime来给每一个Button的响应事件中添加一段代码,利用这段代码来进行埋点上报呢?或者进一步来说我们能不能给所有继承自UIControl的对象都添加这样一段代码呢?这样我们不是可以捕获所有的用户事件了吗?(其实答案是否定的,看第五条);这时我们可以利用Mehod Swizzle,或者叫方法注入,或者叫hook住了某个方法,听着挺玄乎,其实就是RunTime的一个API,这个API能够交换两个方法的实现。通过这个API,我们可以这样实现方法注入。如下图所示:
方法注入的实现过程
那么我们点击按钮系统会不会给每个按钮都执行一个统一的方法?然后我们往这个方法中嵌入响应的代码片段就可以了。答案是肯定的。我们可以往

1
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;

这个方法里面嵌入相应的代码片段。我们可以这样:1.将互换方法实现的的这个方法放到一个工具类中,因为我们可能不止一处要用到这种方法。2.我们给UIControl添加一个Category,然后在里面调用这个工具类然后实现所插入的代码片段。这里我们既然可以得到target还有action,那么很多情况下我们就可以唯一确定这个埋点了,那么我们怎样从这么多的埋点中选出这个这个埋点呢?我们其实可以用字典和数组结合的方式将这些方法的target和方法的参数一一存起来,然后在嵌入的方法内部获取其对应的方法,以及其相应的,这个事先配置好的字典和数组的结合放在哪里比较合适呢?plist。下面就以最简单的形式展示这种思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 工具类
@interface ZHSwizzleTool : NSObject
+ (void)zhSwizzleWithClass:(Class)processedClass originalSelector:(SEL)originSelector swizzleSelector:(SEL)swizzlSelector;
@end

@implementation ZHSwizzleTool
+(void)zhSwizzleWithClass:(Class)processedClass originalSelector:(SEL)originSelector swizzleSelector:(SEL)swizzlSelector{

Method originMethod = class_getInstanceMethod(processedClass, originSelector);
Method swizzleMethod = class_getInstanceMethod(processedClass, swizzlSelector);
BOOL didAddMethod = class_addMethod(processedClass, originSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
class_replaceMethod(processedClass, swizzlSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}else{
method_exchangeImplementations(originMethod, swizzleMethod);
}
}
@end

// 分类
@implementation UIControl (ZHSwizzle)
+(void)load{

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

SEL originSEL = @selector(sendAction:to:forEvent:);
SEL swizzleSEL = @selector(sendSwizzleAction:to:forEvent:);

[ZHSwizzleTool zhSwizzleWithClass:[self class]originalSelector:originSEL swizzleSelector:swizzleSEL];

});
}
- (void)sendSwizzleAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{

// 注意这里调用的是原来的系统方法
[self sendSwizzleAction:action to:target forEvent:event];

NSString *selectorName = NSStringFromSelector(action);

// 这个plist中存储的数据格式是这样的:@{@"someViewController":@"selector0":@[para0,para1,para2],@"selector1":@[para0,para1]]};

NSString *pathString = [[NSBundle mainBundle]pathForResource:@"ZHLogInfo" ofType:@"plist"];
NSDictionary *plistDic = [NSDictionary dictionaryWithContentsOfFile:pathString];

//1. 获取Target的名字
NSDictionary *controllerDic = plistDic[NSStringFromClass([target class])];

//2. 获取这个方法对应的参数列表
NSArray *parameterArray = controllerDic[selectorName];
//3. 实例化数据中心
ZHLogDataCenter *logCenter = [[ZHLogDataCenter alloc]init];
NSMutableDictionary *logInfoDic = [NSMutableDictionary dictionary];

for (NSString *parameter in parameterArray) {

NSString *getSelector = [NSString stringWithFormat:@"%@",parameter];
SEL getSeletor = NSSelectorFromString(getSelector);
//4. 从数据中心中获取相应的数据
id value = [logCenter performSelector:getSeletor withObject:nil];
//5.获取成功则将其存入需要上传的字典
if (value)
[logInfoDic setObject:value forKey:parameter];

}
//6.将这个字典存入埋点管理类,其会将其存入缓存并等待上传
[ZHLogCenter zhLogWithInforDictionary:logInfoDic];

}
@end

下面是这个代码中用到的Plist中的配置:
埋点相关字段的plist配置

在实践中遇到了什么问题以及解决方案?

  1. 并不是所有的事件都是有继承自UIControl的控件来发出的,比如:手势,点击Cell。
  2. 并不是所有的按钮点击了之后就立马需要埋点上传?可能在按钮的响应方法中经过了层层的if(){ } else{ }最后才需要埋点。
  3. 和事件所在类无关的埋点数据可以同意从ZHLogDataCenter这个类中中取,那么如果这个数据是和所在类有关呢?
  4. 对于代理方法该怎样处理?
  5. 如果很多个按钮对应着一个事件该怎样处理?
  6. 项目中事件的处理方法不尽相同,方法的参数个数不一样,并且方法的返回值也不一样,如何对他们进行统一的处理?

下面我们来一一解决这些问题。

问题1:对于不是来自UIControl的子类发出的事件,我们一样是可以进行hooK,只不过方法有所不同。我们在UIControl的分类中写了一段嵌入的代码,确实hook住了系统UIButton的点击事件,是因为UIButton自身会调用UIControl的这个方法。但是对于点击事件,这个是我们自己写的一个方法,它的父类UIViewController中是没有的,所以在执行我们自己点击事件的方法时UIViewController分类中要嵌入的方法是不会被调用的,这时候怎么办,我们可以动态的给我们自己要hook的ViewController动态的添加一个方法,然后就可以hook了(这一点不太好理解)。具体的添加方法,可以参考本文的实例代码。

问题2:对于是否上传和具体的业务逻辑相关的情况,我们可以用方法所在类的一个属性值进行标记,这个属性写在.m文件中即可(KVC可以获取.m文件中的属性值。),我们先执行要hook那个类的方法,然后根据plist中配置的相关标记进行相应的处理(这里的属性值其实也是不必要的,我么可以根据类名和方法名字符串的哈希生成唯一的key,然后利用runtime自动关联到这个类的mf_condition属性上,这个属性是一个字典其key就是刚才生成的,value就是运行完这个方法之后得到的值,然后这个值再跟plist中的配置做以比较)。

问题3:对于和事件所在类有紧密关联的埋点数据,比如某个页面对应的产品ID,比如某个页面点击了cell,之后这个cell对应的model的ID。这个时候我们可以参考方法2,添加一个属性,用一个属性值来存储这些这些需要上传的具体数据。

问题4:代理方法和手势的处理也是一样的,既然一个类实现了某个代理方法,那么其[someInstance respondsToSelector:someSelector]所返回的BOOL值应该是YES的,然后其它的就和手势的处理是一样的了。

问题5:对于很多按钮对应一个响应事件的情况,我们可以利用RunTime动态的给按钮添加一个属性,比如:buttonIdentifier,这样我们就可以在plist中进行相应的配置,以进行相应的埋点处理。

问题6:这个问题其实就是hook住所有的方法,然后给他们添加同一个代码段的问题,这时候我们可以使用Aspects这个第三方框架:

1
2
3
4
5
6
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add((id)self, selector, options, block, error);
}

调用这个接口,因为在UIViewController的分类中调用这个接口的对象不一样,并且我们根据plist中的配置hook的selector不一样,然而最后执行的block却是一样的,这就很好的解决了问题。

最理想的埋点是什么样的?

最理想的埋点是动态的,就是PM给我们说需要哪些埋点,然后服务器给我们发一个类似与上文中提到的plist一样的文件,或者一个json,我们存到本地,如果这些埋点没有更新,我们就从本地中读取相应的文件,做相应的埋点,如果有更新,我们重新从服务器获取最新的需要埋的点,然后进行相应埋点。这样就解决了少埋,或者埋点不恰当,需要添加埋点的问题。

其中可能存在的问题是什么?

当然这里面也有其难以处理的问题,比如我们使用了一个第三方控件,这个第三方控件的事件回调不是用delegate实现的,而是用block实现的,并且这个埋点和具体的业务逻辑有关系,那么这种方法就难以处理了。 如果很多事件的逻辑处理放到了block中进行,那么也将造难以处理。

1…456
击水湘江

击水湘江

努力让明天的自己爱上今天的自己!

56 posts
10 tags
© 2019 击水湘江
Powered by Hexo
|
Theme — NexT.Muse v6.0.3