0%

Python

Linux和Mac环境自带了Python 2.x版本,但是如果要更新到3.x的版本,可以在Python的官方网站下载安装

注释

  1. 单行注释 - 以#和空格开头的部分
  2. 多行注释 - 三个引号开头,三个引号结尾

语言元素

变量和类型

  • 整型:Python中可以处理任意大小的整数
    • (Python 2.x中有int和long两种类型的整数,但这种区分对Python来说意义不大,因此在Python 3.x中整数只有int这一种了),而且支持二进制(如0b100,换算成十进制是4)、八进制(如0o100,换算成十进制是64)、十进制(100)和十六进制(0x100,换算成十进制是256)的表示法。
  • 浮点型:浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,浮点数除了数学写法(如123.456)之外还支持科学计数法(如1.23456e2)。
  • 字符串型:字符串是以单引号或双引号括起来的任意文本,比如'hello'"hello",字符串还有原始字符串表示法、字节字符串表示法、Unicode字符串表示法,而且可以书写成多行的形式(用三个单引号或三个双引号开头,三个单引号或三个双引号结尾)。
  • 布尔型:布尔值只有TrueFalse两种值,要么是True,要么是False,在Python中,可以直接用TrueFalse表示布尔值(请注意大小写),也可以通过布尔运算计算出来(例如3 < 5会产生布尔值True,而2 == 1会产生布尔值False)。
  • 复数型:形如3+5j,跟数学上的复数表示一样,唯一不同的是虚部的i换成了j

在对变量类型进行转换时可以使用Python的内置函数(准确的说下面列出的并不是真正意义上的函数,而是后面我们要讲到的创建对象的构造方法)。

  • int():将一个数值或字符串转换成整数,可以指定进制。
  • float():将一个字符串转换成浮点数。
  • str():将指定的对象转换成字符串形式,可以指定编码。
  • chr():将整数转换成该编码对应的字符串(一个字符)。
  • ord():将字符串(一个字符)转换成对应的编码(整数)。

运算符

说明: 在实际开发中,如果搞不清楚运算符的优先级,可以使用括号来确保运算的执行顺序。

分支结构

1
2
3
4
5
6
7
8
x = float(input('x = '))
if x > 1:
y = 3 * x - 5
elif x >= -1:
y = x + 2
else:
y = 5 * x + 3
print('f(%.2f) = %.2f' % (x, y))
1
2
3
4
5
6
7
8
9
x = float(input('x = '))
if x > 1:
y = 3 * x - 5
else:
if x >= -1:
y = x + 2
else:
y = 5 * x + 3
print('f(%.2f) = %.2f' % (x, y))

说明: 大家可以自己感受一下这两种写法到底是哪一种更好。在之前我们提到的Python之禅中有这么一句话“Flat is better than nested.”,之所以提倡代码“扁平化”是因为嵌套结构的嵌套层次多了之后会严重的影响代码的可读性,所以能使用扁平化的结构时就不要使用嵌套。

循环结构

for-in循环

如果明确的知道循环执行的次数或者要对一个容器进行迭代(后面会讲到),那么我们推荐使用for-in循环。

  • range(101)可以产生一个0到100的整数序列。
  • range(1, 100)可以产生一个1到99的整数序列。
  • range(1, 100, 2)可以产生一个1到99的奇数序列,其中的2是步长,即数值序列的增量。

while循环

如果要构造不知道具体循环次数的循环结构,我们推荐使用while循环。while循环通过一个能够产生或转换出bool值的表达式来控制循环,表达式的值为True循环继续,表达式的值为False循环结束。下面我们通过一个“猜数字”的小游戏(计算机出一个1~100之间的随机数,人输入自己猜的数字,计算机给出对应的提示信息,直到人猜出计算机出的数字)来看看如何使用while循环。

说明: break关键字来提前终止循环,需要注意的是break只能终止它所在的那个循环,这一点在使用嵌套的循环结构需要引起注意。除了break之外,还有另一个关键字是continue,它可以用来放弃本次循环后续的代码直接让循环进入下一轮。

函数和模块的使用

编程大师Martin Fowler先生曾经说过:“代码有很多种坏味道,重复是最坏的一种!”,要写出高质量的代码首先要解决的就是重复代码的问题。

定义函数

在Python中可以使用def关键字来定义函数,和变量一样每个函数也有一个响亮的名字,而且命名规则跟变量的命名规则是一致的。在函数名后面的圆括号中可以放置传递给函数的参数,这一点和数学上的函数非常相似,程序中函数的参数就相当于是数学上说的函数的自变量,而函数执行完成后我们可以通过return关键字来返回一个值,这相当于数学上说的函数的因变量。

函数的参数

函数是绝大多数编程语言中都支持的一个代码的“构建块”,但是Python中的函数与其他语言中的函数还是有很多不太相同的地方,其中一个显著的区别就是Python对函数参数的处理。在Python中,函数的参数可以有默认值,也支持使用可变参数,所以Python并不需要像其他语言一样支持函数的重载,因为我们在定义一个函数的时候可以让它有多种不同的使用方式.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 在参数名前面的*表示args是一个可变参数
# 即在调用add函数时可以传入0个或多个参数
def add(*args):
total = 0
for val in args:
total += val
return total


print(add())
print(add(1))
print(add(1, 2))
print(add(1, 2, 3))
print(add(1, 3, 5, 7, 9))

用模块管理函数

需要说明的是,如果我们导入的模块除了定义函数之外还中有可以执行代码,那么Python解释器在导入这个模块时就会执行这些代码,事实上我们可能并不希望如此,因此如果我们在模块中编写了执行代码,最好是将这些执行代码放入如下所示的条件中,这样的话除非直接运行该模块,if条件下的这些代码是不会执行的,因为只有直接执行的模块的名字才是“__main__”。

1
2
3
4
5
6
7
8
9
10
11
12
13
def foo():
pass

def bar():
pass

# __name__是Python中一个隐含的变量它代表了模块的名字
# 只有被Python解释器直接执行的模块的名字才是__main__
if __name__ == '__main__':
print('call foo()')
foo()
print('call bar()')
bar()

字符串和常用数据结构

  • sorted函数返回列表排序后的拷贝不会修改传入的列表
  • 函数的设计就应该像sorted函数一样尽可能不产生副作用
  • 通过yield关键字将一个普通函数改造成生成器函数。

使用元组

  1. 元组中的元素是无法修改的
  2. 元组在创建时间和占用的空间上面都优于列表。

面向对象编程基础

类是抽象的概念,而对象是具体的东西。在面向对象编程的世界中,一切皆为对象,对象都有属性和行为,每个对象都是独一无二的,而且对象一定属于某个类(型)

在Python中可以使用class关键字定义类,然后在类中通过之前学习过的函数来定义方法,这样就可以将对象的动态特征描述出来,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student(object):

# __init__是一个特殊方法用于在创建对象时进行初始化操作
# 通过这个方法我们可以为学生对象绑定name和age两个属性
def __init__(self, name, age):
self.name = name
self.age = age

def study(self, course_name):
print('%s正在学习%s.' % (self.name, course_name))

# PEP 8要求标识符的名字用全小写多个单词用下划线连接
# 但是部分程序员和公司更倾向于使用驼峰命名法(驼峰标识)
def watch_movie(self):
if self.age < 18:
print('%s只能观看《熊出没》.' % self.name)
else:
print('%s正在观看岛国爱情大电影.' % self.name)

面向对象进阶

@property装饰器

使用@property包装器来包装getter和setter方法,使得对属性的访问既安全又方便,

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
class Person(object):

def __init__(self, name, age):
self._name = name
self._age = age

# 访问器 - getter方法
@property
def name(self):
return self._name

# 访问器 - getter方法
@property
def age(self):
return self._age

# 修改器 - setter方法
@age.setter
def age(self, age):
self._age = age

def play(self):
if self._age <= 16:
print('%s正在玩飞行棋.' % self._name)
else:
print('%s正在玩斗地主.' % self._name)


def main():
person = Person('王大锤', 12)
person.play()
person.age = 22
person.play()
# person.name = '白元芳' # AttributeError: can't set attribute


if __name__ == '__main__':
main()

__slots__魔法

通常,动态语言允许我们在程序运行时给对象绑定新的属性或方法,当然也可以对已经绑定的属性和方法进行解绑定。但是如果我们需要限定自定义类型的对象只能绑定某些属性,可以通过在类中定义__slots__变量来进行限定。需要注意的是__slots__的限定只对当前类的对象生效,对子类并不起任何作用。

  • 限定Person对象只能绑定_name, _age和_gender属性

    • __slots__ = (‘_name’, ‘_age’, ‘_gender’)

静态方法和类方法

1
2
3
@staticmethod
def is_valid(a, b, c):
return a + b > c and b + c > a and a + c > b
1
2
3
4
@classmethod
def now(cls):
ctime = localtime(time())
return cls(ctime.tm_hour, ctime.tm_min, ctime.tm_sec)

类之间的关系

简单的说,类和类之间的关系有三种:is-a、has-a和use-a关系。

继承和多态

iOS的一些基础梳理,方便求职者能有一些参考。 还是那句话,书山有路勤为径,学海无涯苦作舟!

高级

  • 解决研发过程中的关键问题技术难题
  • 调优设备流量、性能、电量
  • 较强的软件设计能力
  • 对iOS内部原理有深刻理解

资深

  • 精通高性能编程以及性能调优
  • 灵活运用数据结构、算法解决复杂程序设计问题
  • 提供性能优化、日志搜索、统计分析等方案
  • 架构、模块设计

专家

  • 你还来看我这干啥,不要检查我的作业~

简历

  • 排版清晰
  • 重要和突出的表达
  • 线上bug数变化、量化指标 质量上的指标
  • 开发成本节约了多少 量化指标
  • 基本信息、工作经历、项目经验、擅长技能
  • 项目经验(主导、参与),背景、方案、效果
阅读全文 »

前言

面试题

  • Category的实现原理,以及Category为什么只能加方法不能加属性。
  • Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
  • load、initialize的区别,以及它们在category重写的时候的调用的次序。

分类中的对象方法依然是存储在类对象中的,同对象方法在同一个地方,那么调用步骤也同调用对象方法一样。如果是类方法的话,也同样是存储在元类对象中

category和extension?

  • extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。(详见2)

  • 但是category则完全不一样,它是在运行期决议的。 就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

话不多说,源码来看

我们知道,所有的OC类和对象,在runtime层都是用struct表示的,category也不例外,在runtime层,category用结构体category_t(在objc-runtime-new.h中可以找到此定义),它包含了:

  • 1)、类的名字(name)
  • 2)、类(cls)
  • 3)、category中所有给类添加的实例方法的列表(instanceMethods)
  • 4)、category中所有添加的类方法的列表(classMethods)
  • 5)、category实现的所有协议的列表(protocols)
  • 6)、category中添加的所有属性(instanceProperties)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; // 对象方法
struct method_list_t *classMethods; // 类方法
struct protocol_list_t *protocols; // 协议
struct property_list_t *instanceProperties; // 属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}

property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

从源码基本可以看出我们平时使用categroy的方式,对象方法,类方法,协议,和属性都可以找到对应的存储方式。并且我们发现分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成get set方法的声明,需要我们自己去实现。

通过源码我们发现,分类的方法,协议,属性等好像确实是存放在categroy结构体里面的,那么他又是如何存储在类对象中的呢?
我们来看一下底层的内部方法探寻其中的原理。
首先我们通过命令行将MyClass+MyAddition.m.m文件转化为c++文件,查看其中的编译过程。

1
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MyClass+MyAddition.m

好吧,我们得到了一个3M大小,10w多行的.cpp文件(这绝对是Apple值得吐槽的一点),我们忽略掉所有和我们无关的东西,在文件的最后,我们找到了如下代码片段:

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
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"printName", "v16@0:8", (void *)_I_MyClass_MyAddition_printName}}
};

static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"name","T@\"NSString\",C,N"}}
};

extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_MyClass;

static struct _category_t _OBJC_$_CATEGORY_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"MyClass",
0, // &OBJC_CLASS_$_MyClass,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition,
0,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MyClass_$_MyAddition,
};
static void OBJC_CATEGORY_SETUP_$_MyClass_$_MyAddition(void ) {
_OBJC_$_CATEGORY_MyClass_$_MyAddition.cls = &OBJC_CLASS_$_MyClass;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
(void *)&OBJC_CATEGORY_SETUP_$_MyClass_$_MyAddition,
};
static struct _class_t *L_OBJC_LABEL_CLASS_$ [1] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {
&OBJC_CLASS_$_MyClass,
};
static struct _class_t *_OBJC_LABEL_NONLAZY_CLASS_$[] = {
&OBJC_CLASS_$_MyClass,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_MyClass_$_MyAddition,
};

我们可以看到,

  • 1)、首先编译器生成了实例方法列表OBJC$_CATEGORY_INSTANCE_METHODSMyClass$_MyAddition和属性列表OBJC$_PROP_LISTMyClass$_MyAddition,两者的命名都遵循了公共前缀+类名+category名字的命名方式,而且实例方法列表里面填充的正是我们在MyAddition这个category里面写的方法printName,而属性列表里面填充的也正是我们在MyAddition里添加的name属性。还有一个需要注意到的事实就是category的名字用来给各种列表以及后面的category结构体本身命名,而且有static来修饰,所以在同一个编译单元里我们的category名不能重复,否则会出现编译错误。

  • 2)、其次,编译器生成了category本身OBJC$_CATEGORYMyClass$_MyAddition,并用前面生成的列表来初始化category本身。

  • 3)、最后,编译器在DATA段下的objc_catlist section里保存了一个大小为1的category_t的数组L_OBJC_LABELCATEGORY$(当然,如果有多个category,会生成对应长度的数组^_^),用于运行期category的加载。

到这里,编译器的工作就接近尾声了,对于category在运行期怎么加载?

Runtime的时候加载

我们知道,Objective-C的运行是依赖OC的runtime的,而OC的runtime和其他系统库一样,是OS X和iOS通过dyld动态加载的。

对于OC运行时,入口方法如下(在objc-os.mm文件中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;

// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
lock_init();
exception_init();

// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}

ategory被附加到类上面是在map_images的时候发生的,在new-ABI的标准下,_objc_init里面的调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法,而在_read_images方法的结尾,有以下的代码片段:

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
// Discover categories. 
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
class_t *cls = remapClass(cat->cls);

if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = NULL;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}

// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
BOOL classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (isRealized(cls)) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
getName(cls), cat->name,
classExists ? "on existing class" : "");
}
}

if (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->isa, hi);
if (isRealized(cls->isa)) {
remethodizeClass(cls->isa);
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
getName(cls), cat->name);
}
}
}
}

从上述代码中我们可以知道这段代码是用来查找有没有分类的。通过_getObjc2CategoryList函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。可以看到最终都调用了remethodizeClass(cls);函数。我们来到remethodizeClass(cls);函数内部查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;

runtimeLock.assertLocked();

isMeta = cls->isMetaClass();

// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}

attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}

通过上述代码我们发现attachCategories函数接收了类对象cls和分类数组cats,如我们一开始写的代码所示,一个类可以有多个分类。之前我们说到分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。
我们来到attachCategories函数内部。

attachCategoryMethods做的工作相对比较简单,它只是把所有category的实例方法列表拼成了一个大的实例方法列表,然后转交给了attachMethodLists方法,继续往下看

需要注意的有两点:

  • 1)、category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA

  • 2)、category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。

问: Category的实现原理,以及Category为什么只能加方法不能加属性?
答:分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。

load 和 initialize

load方法会在程序启动就会调用,当装载类信息的时候就会调用。 调用顺序看一下源代码。

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
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads(); //看这里看这里!!!!! 先调用类的load
}

// 2. Call category +loads ONCE
more_categories = call_category_loads(); //看这里看这里!!!!! 分类的load

// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}

load方法依次调用 类 分类 ,函数式调用的哦

initialize是通过消息发送机制调用的,消息发送机制通过isa指针找到对应的方法与实现,因此先找到分类方法中的实现,会优先调用分类方法中的实现。

总结

问:Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
答:Category中有load方法,load方法在程序启动装载类信息(_objc_init)的时候就会调用。load方法可以继承。调用子类的load方法之前,会先调用父类的load方法
问:load、initialize的区别,以及它们在category重写的时候的调用的次序。
答:区别在于调用方式和调用时刻
调用方式:load是根据函数地址直接调用,initialize是通过objc_msgSend调用
调用时刻:load是runtime加载类、分类的时候调用(只会调用1次),initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
调用顺序:先调用类的load方法,先编译那个类,就先调用load。在调用load之前会先调用父类的load方法。分类中load方法不会覆盖本类的load方法,先编译的分类优先调用load方法。initialize先初始化父类,之后再初始化子类。如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次),如果分类实现了+initialize,就覆盖类本身的+initialize调用。

本文使用的源码是libdispatch-1008.220.2版本。

once.h

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
//第一部分 dispatch_once 的定义,其实是一个_dispatch_once
void
dispatch_once(dispatch_once_t *predicate,
DISPATCH_NOESCAPE dispatch_block_t block);

void
_dispatch_once(dispatch_once_t *predicate,
DISPATCH_NOESCAPE dispatch_block_t block)
{
if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
dispatch_once(predicate, block);
} else {
dispatch_compiler_barrier();
}
DISPATCH_COMPILER_CAN_ASSUME(*predicate == ~0l);
}
#undef dispatch_once
#define dispatch_once _dispatch_once


//第二部分 dispatch_once_f 的定义,其实是一个 ——dispatch_once_f
void
dispatch_once_f(dispatch_once_t *predicate, void *_Nullable context,
dispatch_function_t function);
void
_dispatch_once_f(dispatch_once_t *predicate, void *_Nullable context,
dispatch_function_t function)
{
if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
dispatch_once_f(predicate, context, function);
} else {
dispatch_compiler_barrier();
}
DISPATCH_COMPILER_CAN_ASSUME(*predicate == ~0l);
}
#undef dispatch_once_f
#define dispatch_once_f _dispatch_once_f

由上可以简单的知道,dispatch_once 调用了 dispatch_once_f方法,
为了解释DISPATCH_EXPECT,我把宏定义贴下面

1
2
3
4
5
6
7
#if __GNUC__
#define DISPATCH_EXPECT(x, v) __builtin_expect((x), (v))
#define dispatch_compiler_barrier() __asm__ __volatile__("" ::: "memory")
#else
#define DISPATCH_EXPECT(x, v) (x)
#define dispatch_compiler_barrier() do { } while (0)
#endif

头文件中用到一个DISPATCH_EXPECT宏,这个宏展开之后其实是一个GCC内置函数__builtin_expect(exp, c),在GCC手册上是这样描述这个函数的:由于大部分程序员在分支预测方面做得很糟糕,所以 GCC 提供了这个内建函数来帮助程序员处理分支预测,优化程序。其第一个参数 exp 为一个整型表达式,这个内建函数的返回值也是这个 exp ,而 c 为一个编译期常量。这个函数的语义是:你期望 exp 表达式的值等于常量 c ,从而 GCC 为你优化程序,将符合这个条件的分支放在合适的地方

我们来看实现

once.c

class

class_data_bits_t bits 这是一个64位结构体,里面只有一个 uintptr_t bits,
在x86_64架构上,并且class_rw_t [2, 47]只占到了47位,如下

63 47 2 1 0
extra class_rw_t data requires raw_isa has default_rr is_swift
1
2
3
4
#define FAST_IS_SWIFT           (1UL<<0)
#define FAST_HAS_DEFAULT_RR (1UL<<1)
#define FAST_REQUIRES_RAW_ISA (1UL<<2)
#define FAST_DATA_MASK 0x00007ffffffffff8UL

在编译期间类的结构中的 class_data_bits_t *data 指向的是一个 class_ro_t * 指针:

  • 类在内存中的位置是在编译期间决定的,在之后修改代码,也不会改变内存中的位置。
  • 类的方法、属性以及协议在编译期间存放到了“错误”的位置,直到 realizeClass 执行之后,才放到了 class_rw_t 指向的只读区域 class_ro_t,这样我们即可以在运行时为 class_rw_t 添加方法,也不会影响类的只读结构。
  • 在 class_ro_t 中的属性在运行期间就不能改变了,再添加方法时,会修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethods,对于方法的添加会在之后的文章中分析。

在 Objective-C 运行时 初始化的过程中会对其中的类进行第一次初始化也就是执行 realizeClass 方法,为类分配可读写结构体 class_rw_t 的空间,并返回正确的类结构体。

而 _class_initialize 方法会调用类的 initialize 方法,

1
2
3
4
5
6
7
8
if (!cls->isRealized()) {
rwlock_writer_t lock(runtimeLock);
realizeClass(cls);
}

if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
}

参考 对象是如何初始化的

@selector()

使用 @selector() 生成的选择子不会因为类的不同而改变,其内存地址在编译期间就已经确定了。也就是说向不同的类发送相同的消息时,其生成的选择子是完全相同的。

  • Objective-C 为我们维护了一个巨大的选择子表
  • 在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中
  • 在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中

objc_msgsend()

Objective-C 中 objc_msgSend 的实现并没有开源,它只存在于 message.h 这个头文件中。

当编译器遇到一个方法调用时,它会将方法的调用翻译成以下函数中的一个 objc_msgSend、objc_msgSend_stret、objc_msgSendSuper 和 objc_msgSendSuper_stret。 发送给对象的父类的消息会使用 objc_msgSendSuper 有数据结构作为返回值的方法会使用 objc_msgSendSuper_stret 或 objc_msgSend_stret 其它的消息都是使用 objc_msgSend 发送的

objc_msgSend -> _class_loopupMethodAndLocadCache3 -> lookUpImpOrForward

对于 objc/objc-runtime-new.mm 中 lookupImpOrForward 的总结如下:

  • 无锁的缓存查找
  • 如果类没有实现(isRealized)或者初始化(isInitialized),实现或者初始化类
  • 加锁
  • 缓存以及当前类中方法的查找
  • 尝试查找父类的缓存以及方法列表
  • 没有找到实现,尝试方法解析器
  • 进行消息转发
  • 解锁、返回实现

不过因为 _class_lookupMethodAndLoadCache3 传入的 cache = NO,所以这里会直接跳过 if 中代码的执行,在 objc_msgSend 中已经使用汇编代码查找过了。

如果缓存没有命中 -> 找到之后加入缓存 -> 第二次调用的时候,就不会走lookupImpOrForward,而是汇编查找了缓存,

为了提高消息传递的效率,ObjC 对 objc_msgSend 以及 cache_getImp 使用了汇编语言来编写。

看着看着来到了这里

在运行时创建一个类

1
2
3
Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
objc_registerClassPair(newClass);

添加的方法使用名为ReportFunction其实现的函数,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void ReportFunction(id self, SEL _cmd)
{
NSLog(@"This object is %p.", self);
NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);

Class currentClass = [self class];
for (int i = 1; i < 5; i++)
{
NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
currentClass = object_getClass(currentClass);
}

NSLog(@"NSObject's class is %p", [NSObject class]);
NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
}

从表面上看,这一切都非常简单。在运行时创建类只需三个简单的步骤:

  • 为“类对”分配存储(使用objc_allocateClassPair)。
  • 根据需要将方法和ivars添加到类中(我添加了一个方法class_addMethod)。
  • 注册该类以便可以使用(使用objc_registerClassPair)。

然而,当前的问题是:什么是“class pair”?该函数objc_allocateClassPair只返回一个值:类。那对的另一半在哪里?

我敢肯定你已经猜到了这一对的另一半是元类(这是这篇文章的标题)但是为了解释这是什么以及你为什么需要它,我将给出一些关于对象的背景知识和Objective-C中的类。

成为对象de数据结构需要什么?

在Objective-C中,对象的类由其isa指针确定。该isa指针指向对象的类。

1
2
3
typedef struct objc_object {
Class isa;
} *id;

这就是说:任何以指向Class结构的指针开头的结构都可以视为objc_object。

Objective-C中对象最重要的特性是你可以向它们发送消息:

1
2
[@"stringValue"
writeToFile:@"/file.txt" atomically:YES encoding:NSUTF8StringEncoding error:NULL];

什么是元级?

现在,正如您可能已经知道的那样,Class 在Objective-C中也是一个对象。这意味着您可以发送消息给Class。

1
NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding];

在这种情况下,defaultStringEncoding被发送到NSString class。

这是有效的,因为Class 在Objective-C中的每个都是一个对象本身。这意味着Class结构必须以isa指针开头,以便它与objc_object上面显示的结构二进制兼容,并且结构中的下一个字段必须是指向superclass(或nil基类)的指针。

正如我上面所展示的那样Class,根据您运行的运行时版本,有几种不同的方式可以定义,但是,它们都以isa字段后跟字段开头superclass。

1
2
3
4
5
6
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
/* followed by runtime specific details... */
};

但是,为了让我们调用类上的方法,类的isa指针本身必须指向一个类结构,而这个类结构必须包含我们可以在类上调用的方法列表。

这就引出了元类的定义:元类是类对象的类。

简单地说:

  • 当您向对象发送消息时,将在对象类的方法列表中查找该消息。
  • 当您向类发送消息时,将在类的元类的方法列表中查找该消息。

元类非常重要,因为它存储类的类方法。每个类必须有一个惟一的元类,因为每个类都有一个潜在惟一的类方法列表。

元类的类是什么?

与之前的类一样,元类也是一个对象。这意味着您也可以在其上调用方法。当然,这意味着它还必须有一个类。

所有元类都使用基类的元类(继承层次结构中顶层类的元类)作为。这意味着对于从NSObject派生的所有类(大多数类),元类都将NSObject元类作为其

遵循所有元类都使用基类的元类作为它们的类的规则,任何基元类都将是它自己的类(它们的isa指针指向它们自己)。这意味着NSObject元类上的isa指针指向它自己(它是它自己的一个实例)。

类和元类的继承

就像类用它的超类指针指向超类一样,元类使用它自己的超类指针指向类的超类的元类。

更奇怪的是,基类元类将其超类设置为基类本身

这种继承层次结构的结果是,层次结构中的所有实例、类和元类都继承自层次结构的基类。

对于NSObject层次结构中的所有实例、类和元类,这意味着所有NSObject实例方法都是有效的。对于类和元类,所有NSObject类方法也是有效的。

所有这些在文本中都很混乱。Greg Parker将实例、类、元类和它们的超类以及它们是如何组合在一起的组成了一个很好的图表。

这一点的实验证实

为了确认这一切,让我们看看我在本文开头给出的ReportFunction的输出。这个函数的目的是跟踪isa指针并记录它所发现的内容。

要运行ReportFunction,我们需要创建动态创建的类的实例,并在其上调用report方法。

1
2
3
id instanceOfNewClass = [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil];
[instanceOfNewClass performSelector:@selector(report)];
[instanceOfNewClass release];

由于报表方法没有声明,所以我使用performSelector:调用它,因此编译器不会给出警告。

ReportFunction现在将遍历isa指针,并告诉我们使用哪些对象作为类、元类和元类的类。

获取对象的类:ReportFunction使用object_getClass跟踪isa指针,因为isa指针是该类的受保护成员(您不能直接访问其他对象的isa指针)。ReportFunction不使用类方法来实现这一点,因为在类对象上调用类方法不会返回元类,而是再次返回类(因此[NSString类]将返回NSString类而不是NSString元类)。

This is the output (minus NSLog prefixes) when the program runs:

1
2
3
4
5
6
7
8
This object is 0x10010c810.
Class is RuntimeErrorSubclass, and super is NSError.
Following the isa pointer 1 times gives 0x10010c600
Following the isa pointer 2 times gives 0x10010c630
Following the isa pointer 3 times gives 0x7fff71038480
Following the isa pointer 4 times gives 0x7fff71038480
NSObject's class is 0x7fff71038490
NSObject's meta class is 0x7fff71038480

反复查看isa值所达到的地址:

  • 对象是地址0x10010c810。
  • 类的地址是0x10010c600。
  • 元类是地址0x10010c630。
  • 元类的类(即NSObject元类)是地址0x7fff71038480。
  • NSObject元类的类是它自己。

地址的值并不十分重要,除非它显示了从类到元类到NSObject元类的过程。

总结

元类是类对象的类。每个类都有自己独特的元类(因为每个类都可以有自己独特的方法列表)。这意味着所有类对象本身并不都属于同一个类。

元类将始终确保类对象具有层次结构中基类的所有实例和类方法,以及中间的所有类方法。对于派生自NSObject的类,这意味着所有NSObject实例和协议方法都为所有类(和元类)对象定义。

所有元类本身都使用基类的元类(NSObject层次结构类的NSObject元类)作为类,包括运行时中惟一自定义的基类元类。

先说类的本质

我们都知道类的本质其实就是一个isa指针,我们直接看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct objc_object {
private:
isa_t isa;
//从这开始往下不用看了,都是方法
public:

// ISA() assumes this is NOT a tagged pointer object
Class ISA();

// getIsa() allows this to be a tagged pointer object
Class getIsa();

// initIsa() should be used to init the isa of new objects only.
// If this object already has an isa, use changeIsa() for correctness.
// initInstanceIsa(): objects with no custom RR/AWZ
// initClassIsa(): class objects
// initProtocolIsa(): protocol objects
// initIsa(): other objects
void initIsa(Class cls /*nonpointer=false*/);
//、、、下面的省略

isa_t 的isa又是什么?

1
2
3
4
5
6
7
8
9
10
11
12
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};

原来就是一个union共用体,包含cls, bits, ISA_BITFIELD, 我们展开ISA_BITFIELD看一下
在arm64结构下,

1
2
3
4
5
6
7
8
9
10
11
# if __arm64__
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19

加上注释吧:

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
struct {
// 0代表普通的指针,存储着Class,Meta-Class对象的内存地址。
// 1代表优化后的使用位域存储更多的信息。
uintptr_t nonpointer : 1;

// 是否有设置过关联对象,如果没有,释放时会更快
uintptr_t has_assoc : 1;

// 是否有C++析构函数,如果没有,释放时会更快
uintptr_t has_cxx_dtor : 1;

// 存储着Class、Meta-Class对象的内存地址信息
uintptr_t shiftcls : 33;

// 用于在调试时分辨对象是否未完成初始化
uintptr_t magic : 6;

// 是否有被弱引用指向过。
uintptr_t weakly_referenced : 1;

// 对象是否正在释放
uintptr_t deallocating : 1;

// 引用计数器是否过大无法存储在isa中
// 如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
uintptr_t has_sidetable_rc : 1;

// 里面存储的值是引用计数器减1
uintptr_t extra_rc : 19;
};

cls的内存分布可以参考我的另外一片文章, 类的本质

看过我分类的本质也会发现,class_rw_t中的methods是二维数组的结构,并且可读可写,因此可以动态的添加方法,并且更加便于分类方法的添加。参考 category 源码可以发现,attachList函数内通过memmove 和 memcpy两个操作将分类的方法列表合并在本类的方法列表中。那么此时就将分类的方法和本类的方法统一整合到一起了。
其实一开始类的方法,属性,成员变量属性协议等等都是存放在class_ro_t中的,当程序运行的时候,需要将分类中的列表跟类初始的列表合并在一起的时,就会将class_ro_t中的列表和分类中的列表合并起来存放在class_rw_t中,也就是说class_rw_t中有部分列表是从class_ro_t里面拿出来的。

类的初始信息本来其实是存储在class_ro_t中的,并且ro本来是指向cls->data()的,也就是说bits.data()得到的是ro,但是在运行过程中创建了class_rw_t,并将cls->data指向rw,同时将初始信息ro赋值给rw中的ro。最后在通过setData(rw)设置data。那么此时bits.data()得到的就是rw,之后再去检查是否有分类,同时将分类的方法,属性,协议列表整合存储在class_rw_t的方法,属性及协议列表中。

iOS相关博客

  • YY
  • 孙源sunny
  • 唐巧
  • 戴铭
  • 王巍
  • SatanWoo
  • GCD看这一篇就够了
  • objc4
  • 动态库和静态库
  • App点击到启动
  • CoreText是如何绘制文本
  • 绘制像素到屏幕上

参考文档备忘录

  • Apple官方文档
  • 计算机知识点
  • 计算机调度算法
  • Swift 翻译组
  • Swift source code
  • 苹果源码库

关于iOS基础技术部分

杂项

面试快餐

  • 部分索引
  • 内存管理
  • 性能优化
  • 离屏渲染等原理
  • SideTable相关(主要用来引用计数和weak表)
    定义在objc-private文件下。 由static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap; 全局静态变量
    类型为StripedMap,iOS有8张SideTable表,mac有64张, 重载了[]方法,可以通过对象指针分配其中一张表Sidetable,
    • https://www.jianshu.com/p/13c4fb1cedea
    • gihub面试大全
  • 关联对象。 解决weak可以包装一层,属性弱引用。
  • HTTP3.0
    TCP四元组(源IP、源端口、目标IP、目的端口),网络发生变化的时候,重连。QUIC采用Connection ID不关心四元组
  • AutoReleasePool 与线程一一对应,page结构体中有thread成员。 TLS(Thread local Stroge)hotPage。 嵌套用哨兵对象
  • 为什么会有元类的设计
  • isEqual 和 hash

首先是YY大神的理解runloop
下面是收集的RunLoop相关知识点,一些应用场景点击这里查看

官方文档翻译

一、RunLoop概念

RunLoop是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。

1、没有消息处理时,休眠已避免资源占用,由用户态切换到内核态(CPU-内核态和用户态)
2、有消息需要处理时,立刻被唤醒,由内核态切换到用户态

RunLoop源码

  1. NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
  2. CFRunLoopRef 的代码是开源的,你可以在这里 http://opensource.apple.com/tarballs/CF/ 下载到整个 CoreFoundation 的源码来查看。
  3. Swift 开源后,苹果又维护了一个跨平台的 CoreFoundation 版本:https://github.com/apple/swift-corelibs-foundation/,这个版本的源码可能和现有 iOS 系统中的实现略不一样,但更容易编译,而且已经适配了 Linux/Windows。

为什么main函数不会退出?

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

UIApplicationMain内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码(不是简单的for循环或while循环)

1
2
3
4
5
6
7
8
9
10
//无限循环代码模式(伪代码)
int main(int argc, char * argv[]) {
BOOL running = YES;
do {
// 执行各种任务,处理各种事件
// ......
} while (running);

return 0;
}

UIApplicationMain函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。

二、RunLoop的数据结构

NSRunLoop(Foundation)CFRunLoop(CoreFoundation)的封装,提供了面向对象的API
RunLoop 相关的主要涉及五个类:

CFRunLoop:RunLoop对象
CFRunLoopMode:运行模式
CFRunLoopSource:输入源/事件源
CFRunLoopTimer:定时源
CFRunLoopObserver:观察者

1、CFRunLoop

pthread(线程对象,说明RunLoop和线程是一一对应的)、currentMode(当前所处的运行模式)、modes(多个运行模式的集合)、commonModes(模式名称字符串集合)、commonModelItems(Observer,Timer,Source集合)构成

2、CFRunLoopMode

由name、source0、source1、observers、timers构成

3、CFRunLoopSource

分为source0和source1两种

  • source0:
    即非基于port的,也就是用户触发的事件。需要手动唤醒线程,将当前线程从内核态切换到用户态
  • source1:
    基于port的,包含一个 mach_port 和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件。
    具备唤醒线程的能力

4、CFRunLoopTimer

基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。 因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。

5、CFRunLoopObserver

监听以下时间点:CFRunLoopActivity

  • kCFRunLoopEntry
    RunLoop准备启动

  • kCFRunLoopBeforeTimers
    RunLoop将要处理一些Timer相关事件

  • kCFRunLoopBeforeSources
    RunLoop将要处理一些Source事件

  • kCFRunLoopBeforeWaiting
    RunLoop将要进行休眠状态,即将由用户态切换到内核态

  • kCFRunLoopAfterWaiting
    RunLoop被唤醒,即从内核态切换到用户态后

  • kCFRunLoopExit
    RunLoop退出

  • kCFRunLoopAllActivities
    监听所有状态

6、各数据结构之间的联系

线程和RunLoop一一对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的

三、RunLoop的Mode

关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。

当RunLoop运行在Mode1上时,是无法接受处理Mode2或Mode3上的Source、Timer、Observer事件的

总共是有五种CFRunLoopMode:

  • kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行
  • UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
  • UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
  • kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案

四、RunLoop的实现机制

对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。

RunLoop通过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。在用户态调用 mach_msg_trap()时会切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作。
即基于port的source1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待处理和手动唤醒RunLoop

Mach消息发送机制
大致逻辑为:
1、通知观察者 RunLoop 即将启动。
2、通知观察者即将要处理Timer事件。
3、通知观察者即将要处理source0事件。
4、处理source0事件。
5、如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。
6、通知观察者线程即将进入休眠状态。
7、将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。

  • 一个基于 port 的Source1 的事件(图里应该是source0)。
  • 一个 Timer 到时间了。
  • RunLoop 自身的超时时间到了。
  • 被其他调用者手动唤醒。

8、通知观察者线程将被唤醒。
9、处理唤醒时收到的事件。

  • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
  • 如果输入源启动,传递相应的消息。
  • 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2

10、通知观察者RunLoop结束。

五、RunLoop与NSTimer

一个比较常见的问题:滑动tableView时,定时器还会生效吗?
默认情况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就无法接受处理Timer的事件。
怎么去解决这个问题呢?把Timer添加到UITrackingRunLoopMode上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。
所以我们需要把Timer同时添加到UITrackingRunLoopModekCFRunLoopDefaultMode上。
那么如何把timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes

1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

Timer就被添加到多个mode上,这样即使RunLoop由kCFRunLoopDefaultMode切换到UITrackingRunLoopMode下,也不会影响接收Timer事件

CFRunLoopTimerRef与NSTimer是 toll-free bridged的

  1. 对于重复的NSTimer,其多次触发的时刻不是一开始算好的,而是timer触发后计算的。但是计算时参考的是上次应当触发的时间_fireTSR,因此计算出的下次触发的时刻不会有误差。
  2. 设置了tolerance的NSTimer,对于iOS和MacOS系统,实质上会采用GCD timer的形式注册到内核中,GCD timer触发后,再由RunLoop处理其回调逻辑。对于没有设置tolerance的timer,则是用mk_timer的形式注册。
  3. RunLoopMode中timer的排序是按照_fireTSR,也就是应当触发的时间排序的。而且,出于对于保证timer严格有序的考虑,保证时间考前的tolerance较大的timer不会影响后面的timer,系统在给GCD timer 传dummy字段时候会保证_fireTSR+dummy小于后面timer的最晚触发时间。
  4. RunLoop层在timer触发后进行回调的时候,不会对tolerance进行验证。也就是说,因为RunLoop忙导致的timer触发时刻超出了tolerance的情况下,timer并不会取消,而不执行回调。
  5. 对于RunLoop忙时很长(或者timeInteval很短)的情况,会导致本该在这段时间内触发的几次回调中,只触发一次,也就是说,这种情况下还是会损失回调的次数。
  6. 对于RunLoop比较忙的情况,timer的回调时刻有可能不准,且不会受到tolerance的任何限制。tolerance的作用不是决定timer是否触发的标准,而是一个传递给系统的数值,帮助系统合理的规划GCD Timer的mach-port触发时机。设置了tolerance,一定会损失一定的时间精确度,但是可以显著的降低耗电。
    CFAbsoluteTimeGetCurrent()获取准确的时间

请看下节 Runloop 运行

Runloop 运行

一、autoreleasePool 在何时被释放?

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observerorder2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

二、PerformSelector 的实现原理?

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

三、PerformSelector:afterDelay:这个方法在子线程中是否起作用?为什么?怎么解决?

不起作用,子线程默认没有 Runloop,也就没有 Timer

解决的办法是可以使用 GCD 来实现:Dispatch_after

四、为什么 NSTimer 有时候不好使?

因为创建的 NSTimer 默认是被加入到了 defaultMode,所以当 RunloopMode 变化时,当前的 NSTimer 就不会工作了。

五、解释一下 GCDRunloop 中的使用?

GCD由 子线程 返回到 主线程,只有在这种情况下才会触发 RunLoop。会触发 RunLoop 的 Source 1 事件。

六、解释一下 NSTimer

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个TimerTimer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动 TableView 时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop

七、等待补充中。。。

  • Timer
  • autoreleasePool创建
  • 线程保活
  • 检测卡顿
  • scrollview的更新 TrackingRunloopMode下不做操作,添加到 defaultmode下