做点有意思的事情

每天进步一点点


  • 首页

  • 分类

  • 标签

  • 归档

iOS项目使用cocos2d-objc集成Spine

发表于 2021-08-10 | 分类于 iOS |

Spine-runtimes

根据Spine版本在https://github.com/EsotericSoftware/spine-runtimes/releases下载对应的运行库版本。

这里使用的版本为3.4.02。

cmake

  1. 终端输入brew install cmake安装cmake;
  2. cd进入spine-runtimes/spine-cocos2d-objc目录;
  3. 输入mkdir build && cd build && cmake ../..下载cocos2d环境;

下载失败重试时,需要删除build目录。

运行示例项目

打开spine-runtimes/spine-cocos2d-objc/spine-cocos2d-objc.xcodeproj项目。

  1. 项目中cocos2d文件夹显示为红色:这是由于Inspectors面板中Full Path不正确,将其修正为/YourPath/spine-runtimes-3.4.02/spine-cocos2d-objc/cocos2d即可;
  2. CCBlendModeCache类中将objectForKey:方法的参数类型由id<NSCopying>修改为NSDictionary *;
  3. 调整示例项目支持的最低iOS系统版本
  4. 运行项目即可

现有项目集成

  1. 将示例程序中的cocos2d文件夹复制到现有项目工程目录;
  2. 将cocos2d.xcodeproj直接拖拽到项目中;
  3. 选中现有项目Target,选择Build Phases选项卡,点击Dependencies下的+号,选择cocos2d-ios;
  4. 选中现有项目Target,选择General选项卡下的Frameworks,libraries,and Embedded Content,点击+号,选择libcocos2d.a;
  5. 将spine-runtimes-3.4.02/spine-c文件夹导入现有工程;
  6. 将spine-runtimes-3.4.02/spine-cocos2d-objc/src/spine文件夹导入现有工程;
  7. 参考示例程序中将部分文件设置为-fno-objc-arc;
  8. 参考示例程序设置Header Search Paths;

Flutter项目初探

发表于 2021-07-27 | 分类于 Flutter |

创建Flutter应用

可以通过命令行输入下面的命令创建一个flutter项目:

1
flutter create first_flutter_app

注意,项目名不支持特殊符号和大写,只能使用下划线进行分隔。

创建成功后通过相应的开发工具打开项目即可。

示例程序分析

示例程序中,主要Dart代码在lib/main.dart文件中,这个文件是lutter启动的入口文件,包含main()函数。

导入包

下面代码的作用是导入Material UI组件库。Material是一种标准的移动端和web端的视觉设计语言, Flutter默认提供了一套丰富的Material风格的UI组件。

1
import 'package:flutter/material.dart';

应用入口

Flutter 应用中main函数为应用程序的入口。main函数中调用了runApp 方法,它的功能是启动Flutter应用。runApp接受一个Widget参数。

1
2
3
void main() {
runApp(MyApp());
}

在Flutter中,大部分都是widget,包括对齐(alignment)、填充(padding)和布局(layout)等功能,都是以widget的形式提供。

应用结构

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

MyApp类代表当前的Flutter应用,它继承了StatelessWidget类,这也就意味着应用本身也是一个widget。

在Flutter开发中,我们可以继承自StatelessWidget或者StatefulWidget来创建自己的Widget类。

  • StatelessWidget:没有状态改变的Widget,通常仅用来展示;
  • StatefulWidget:需要保存状态,并且可能出现状态改变的Widget;

widget的主要工作是提供一个build()方法来描述如何构建UI界面(通常是通过组合、拼装其它基础widget)。

build()方法会在以下情况下执行:

  • 当StatelessWidget第一次被插入到Widget树中时(也就是第一次被创建时;
  • 当父Widget发生改变时,子Widget会被重新构建;
  • 如果我们的Widget依赖InheritedWidget的一些数据,InheritedWidget数据发生改变时;

首页

1
2
3
4
5
6
7
8
9
10
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
...
}

MyHomePage是应用的首页,它继承自StatefulWidget类,表示它是一个有状态的组件(Stateful widget)。

一个Stateful widget至少由两个类组成:

  • 一个StatefulWidget类;
  • 一个 State类; StatefulWidget类本身是不变的,但是State类中持有的状态在widget生命周期中可能会发生变化;

State类

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
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {

_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

当按钮点击时,会调用_incrementCounter方法,先自增_counte计数,然后调用setState方法。

setState方法的作用是通知Flutter框架,有状态发生了改变,Flutter框架收到通知后,会执行build方法来根据新的状态重新构建界面, Flutter 对此方法做了优化。

Scaffold 是 Material库中提供的页面脚手架,它提供了默认的导航栏、标题和包含主屏幕widget树的body属性,body是页面的内容部分。

核心逻辑

整个示例程序的核心逻辑是,当右下角的floatingActionButton按钮被点击之后,会调用_incrementCounter方法。在_incrementCounter方法中,首先会自增_counter计数器(状态),然后setState会通知Flutter框架状态发生变化,接着,Flutter框架会调用build方法以新的状态重新构建UI,最终显示在设备屏幕上。

Dart基础

发表于 2021-07-20 | 分类于 Flutter |

Hello World

Dart的入口也是main函数:

1
2
3
main(List<String> args) {
print("Hello World");
}

变量声明

显式变量声明的方式如下:

1
2
3
//变量类型 变量名称 = 赋值;
String name = "dart fans";
int age = 20;

另外,也可以省略变量类型,由Dart进行类型推断(类似swift)。

1
var/dynamic/const/final 变量名称 = 赋值;

Dart本身是一个强类型语言,任何变量都是有确定类型的,声明变量后,只可以修改变量值,不能修改变量类型。

var

使用示例:

1
2
3
4
var language = "Dart";

//runtimeType用于获取变量当前的类型
print(language.runtimeType); //String

dynamic和Object

dynamic与var一样都是关键词,声明的变量可以赋值任意对象,功能类似于Objective-C中的id类型和Swift中的Any。

1
2
3
4
5
dynamic level = 28;
print(level.runtimeType); //int

level = "top";
print(level.runtimeType); //String

Object 是Dart所有对象的根基类,也就是说所有类型都是Object的子类(包括Function和Null),所以任何类型的数据都可以赋值给Object声明的对象。

dynamic与var一样都是关键词,声明的变量可以赋值任意对象。 而dynamic与Object相同之处在于,他们声明的变量可以在后期改变赋值类型。

dynamic与Object不同的是,dynamic声明的对象编译器会提供所有可能的组合, 而Object声明的对象只能使用Object的属性与方法, 否则编译器会报错。

final和const

final和const用于定义常量,定义之后的值都不可修改。使用final或者const修饰的变量,变量类型可以省略。

两者区别在于:

  • const为编译时常量,在编译期间就必须确认赋值;
  • final支持运行时赋值,可以在第一次使用时进行初始化;

数据类型

数字类型

Dart中的int和double可表示的范围并不是固定的,它取决于运行Dart的平台。

字符串和数字之间的转化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1.字符串转数字
var one = int.parse('111');
var two = double.parse('12.22');
print('${one} ${one.runtimeType}'); // 111 int
print('${two} ${two.runtimeType}'); // 12.22 double

// 2.数字转字符串
var num1 = 123;
var num2 = 123.456;
var num1Str = num1.toString();
var num2Str = num2.toString();
var num2StrD = num2.toStringAsFixed(2); // 保留两位小数
print('${num1Str} ${num1Str.runtimeType}'); // 123 String
print('${num2Str} ${num2Str.runtimeType}'); // 123.456 String
print('${num2StrD} ${num2StrD.runtimeType}'); // 123.46 String

布尔类型

Dart提供了一个bool的类型, 取值为true和false。要注意的是,Dart中不能判断非0即真, 或者非空即真。

1
2
3
4
5
var message = 'Hello Dart';
// 错误的写法
if (message) {
print(message)
}

字符串类型

Dart字符串是UTF-16编码单元的序列。可以使用单引号或双引号创建一个字符串, 可以使用三个单引号或者双引号表示多行字符串:

1
2
3
4
5
6
7
var s1 = 'Hello World';
var s2 = "Hello Dart";
var s3 = '''
哈哈哈
呵呵呵
嘿嘿嘿
''';

集合类型

对于集合类型,Dart则内置了最常用的三种:List / Set / Map。

List使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1.使用类型推导定义
var letters = ['a', 'b', 'c', 'd'];
print('$letters ${letters.runtimeType}'); //[a, b, c, d] List<String>
print(letters.length); //4

// 2.明确指定类型
List<int> numbers = [1, 2, 3, 4];
print('$numbers ${numbers.runtimeType}'); //[1, 2, 3, 4] List<int>
print(numbers.length); //4

// 3.常用操作
numbers.add(5); //[1, 2, 3, 4, 5]
numbers.remove(1); //[2, 3, 4, 5]
numbers.removeAt(1); //2, 4, 5]
numbers.contains(2); //true

Set使用:

1
2
3
4
5
6
7
8
// 1.使用类型推导定义
var lettersSet = {'a', 'b', 'c', 'd'};
print('$lettersSet ${lettersSet.runtimeType}'); //{a, b, c, d} _CompactLinkedHashSet<String>


// 2.明确指定类型
Set<int> numbersSet = {1, 2, 3, 4};
print('$numbersSet ${numbersSet.runtimeType}'); //{1, 2, 3, 4} _CompactLinkedHashSet<int>

Map使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1.使用类型推导定义
var infoMap1 = {'name': 'why', 'age': 18};
print('$infoMap1 ${infoMap1.runtimeType}');

// 2.明确指定类型
Map<String, Object> infoMap2 = {'height': 1.88, 'address': '北京市'};
print('$infoMap2 ${infoMap2.runtimeType}');

// 3.Map的操作
// 根据key获取value
print(infoMap1['name']); // why
// 获取所有的keys
print('${infoMap1.keys} ${infoMap1.keys.runtimeType}'); // (name, age) _CompactIterable<String>
// 获取所有的values
print('${infoMap1.values} ${infoMap1.values.runtimeType}'); // (why, 18) _CompactIterable<Object>
// 判断是否包含某个key或者value
print('${infoMap1.containsKey('age')} ${infoMap1.containsValue(18)}'); // true true
// 根据key删除元素
infoMap1.remove('age');
print('${infoMap1}'); // {name: why}

函数

Dart是一种真正的面向对象的语言,所以即使是函数也是对象,并且有一个类型Function。这意味着函数可以赋值给变量或作为参数传递给其他函数,这是函数式编程的典型特征。

函数的定义:

1
2
3
4
返回值 函数的名称(参数列表) {
函数体
return 返回值
}

Dart函数声明如果没有显式声明返回值类型时会默认当做dynamic处理,并不会进行类型推断。

对于只包含一个表达式的函数,可以使用简写语法:

1
sum(num1, num2) => num1 + num2;

Dart中没有函数重载。

函数的可选参数

函数的可选参数分为可选的位置参数和可选的命名参数。但是两者不能同时使用。

可选的位置参数

包装一组函数参数,用[]标记为可选的位置参数,并放在参数列表的最后面:

1
2
3
4
5
6
7
8
9
// 定义
printInfo2(String name, [int age, double height]) {
print('name=$name age=$age height=$height');
}

// 调用
printInfo2('why'); // name=why age=null height=null
printInfo2('why', 18); // name=why age=18 height=null
printInfo2('why', 18, 1.88); // name=why age=18 height=1.88

可选的命名参数

定义函数时,使用{param1, param2, …},放在参数列表的最后面,用于指定命名参数。可选命名参数在Flutter中使用非常多。例如:

1
2
3
4
5
6
7
8
9
10
// 定义
printInfo1(String name, {int age, double height}) {
print('name=$name age=$age height=$height');
}

// 调用
printInfo1('why'); // name=why age=null height=null
printInfo1('why', age: 18); // name=why age=18 height=null
printInfo1('why', age: 18, height: 1.88); // name=why age=18 height=1.88
printInfo1('why', height: 1.88); // name=why age=null height=1.88

可选参数的默认值

只有可选参数才可以设置默认值。

1
2
3
printInfo1(String name, {int age = 18, double height = 1.88}) {
print('name=$name age=$age height=$height');
}

Frist class citizen

函数在Dart中属于一等公民,这就是说可以将函数赋值给一个变量,也可以将函数作为另外一个函数的参数或者返回值来使用。

函数作为变量使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void say(String msg) {
print("hello ${msg}");
}

var func1 = say;
func1("dart1");

var func2 = (msg){
print("hello $msg");
};
func2("dart2");

var func3 = (msg)=>print("hello $msg");
func3("dart3");

函数作为参数

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
int add(int num1, int num2) {
return num1 + num2;
}

int subtract(int num1, int num2) {
return num1 - num2;
}


typedef Calculate = int Function(int num1, int num2);

int calculate(int num1, int num2, Calculate cal) {
return cal(num1, num2);
}


var ret1 = calculate(20, 10, add);
print(ret1); //30

var ret2 = calculate(10, 20, subtract);
print(ret2); //-10

var ret3 = calculate(10, 20, (num1, num2) => num1 * num2);
print(ret3); //200

var ret4 = calculate(20, 10, (num1, num2){
return num1 ~/ num2;
});
print(ret4); //2

函数作为返回值

1
2
3
Function getFunc() {
return add;
}

运算符

除法与取模运算符

1
2
3
4
var num = 7;
print(num / 3); // 除法操作, 结果2.3333..
print(num ~/ 3); // 整除操作, 结果2;
print(num % 3); // 取模操作, 结果1;

赋值运算符??=

规则:

  • 当变量为null时,使用后面的内容进行赋值
  • 当变量有值时,使用自己原来的值
1
2
3
4
5
6
7
8
main(List<String> args) {
var name1 = 'coderwhy';
print(name1);
// var name2 = 'kobe';
var name2 = null;
name2 ??= 'james';
print(name2); // 当name2初始化为kobe时,结果为kobe,当初始化为null时,赋值了james
}

条件运算符??

expr1 ?? expr2,规则:

  • 如果expr1是null,则返回expr2的结果
  • 如果expr1不是null,直接使用expr1的结果
1
2
3
4
var temp = 'why';
var temp = null;
var name = temp ?? 'kobe';
print(name);

级联运算符

可以使用级联运算符..对一个对象进行连续操作。

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
class Person {
String? name;

void run() {
print("${name} is running");
}

void eat() {
print("${name} is eating");
}

void swim() {
print("${name} is swimming");
}
}

main(List<String> args) {
final p1 = Person();
p1.name = 'why';
p1.run();
p1.eat();
p1.swim();

final p2 = Person()
..name = "why"
..run()
..eat()
..swim();
}

流程控制

if,for,swich语法都与其他语言一致。

类和对象

类的定义

使用class关键字定义一个类:

1
2
3
4
5
6
class 类名 {
类型 成员名;
返回值类型 方法名(参数列表) {
方法体
}
}

可以在成员变量命名前加上下划线_表明该成员变量为私有变量。

构造方法

普通构造方法

  • 当类中没有明确指定构造方法时,将默认拥有一个无参的构造方法
  • 自定义构造方法后,默认的构造方法将会失效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
String? name;
int? age;

Person(String name, int age) {
this.name = name;
this.age = age;
}

@override
String toString() {
return 'name=$name age=$age';
}
}

上面的构造方法也可以简写:

1
2
3
4
5
6
Person(String name, int age) {
this.name = name;
this.age = age;
}
// 等同于
Person(this.name, this.age);

命名构造方法

由于不支持方法(函数)的重载,所以没办法创建相同名称的构造方法, 此时可以使用命名构造方法:

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
class Person {
String?name;
int?age;

Person() {
name = '';
age = 0;
}
// 命名构造方法
Person.withArgments(String name, int age) {
this.name = name;
this.age = age;
}

@override
String toString() {
return 'name=$name age=$age';
}
}

// 创建对象
var p1 = new Person();
print(p1);
var p2 = new Person.withArgments('why', 18);
print(p2);

初始化列表

下面这种初始化变量的方法, 称为初始化列表(Initializer list):

1
2
3
4
5
6
7
8
9
10
11
12
13
class Point {
final num x;
final num y;
final num distance;

// 错误写法
// Point(this.x, this.y) {
// distance = sqrt(x * x + y * y);
// }

// 正确的写法
Point(this.x, this.y) : distance = sqrt(x * x + y * y);
}

重定向构造方法

重定向构造方法是指在一个构造方法中去调用另外一个构造方法:

1
2
3
4
5
6
7
class Person {
String? name;
int? age;

Person(this.name, this.age);
Person.fromName(String name) : this(name, 0);
}

常量构造方法

默认情况下,创建对象时,即使传入相同的参数,创建出来的也不是同一个对象。

在某些情况下,传入相同值时,我们希望返回同一个对象,这个时候,可以使用常量构造方法。拥有常量构造方法的类中,所有的成员变量必须是final修饰的。

1
2
3
4
5
6
7
8
9
10
11
main(List<String> args) {
var p1 = const Person('why');
var p2 = const Person('why');
print(identical(p1, p2)); // true
}

class Person {
final String name;

const Person(this.name);
}

工厂构造方法

Dart提供了factory关键字, 用于通过工厂去获取对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main(List<String> args) {
var p1 = Person('why');
var p2 = Person('why');
print(identical(p1, p2)); // true
}

class Person {
String? name;

static final Map<String, Person> _cache = <String, Person>{};

factory Person(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final p = Person._internal(name);
_cache[name] = p;
return p;
}
}

Person._internal(this.name);
}

getter和setter

getter和setter可以用来监控类的属性被访问的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
main(List<String> args) {
final d = Dog("黄色");
d.setColor = "黑色";
print(d.getColor);
}

class Dog {
String? color;

String get getColor {
return color;
}
set setColor(String color) {
this.color = color;
}

Dog(this.color);
}

类的继承

Dart中的继承使用extends关键字,子类中使用super来访问父类。

父类中的所有成员变量和方法都会被继承,,但是构造方法除外。子类可以拥有自己的成员变量, 并且可以对父类的方法进行重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
main(List<String> args) {
var p = Person();
p.age = 18;
p.run();
print(p.age);
}

class Animal {
int? age;

run() {
print('在奔跑ing');
}
}


class Person extends Animal {
String? name;

@override
run() {
print('$name在奔跑ing');
}
}

子类中可以调用父类的构造方法,对某些属性进行初始化:

  • 子类的构造方法在执行前,将隐含调用父类的无参默认构造方法(没有参数且与类同名的构造方法)。
  • 如果父类没有无参默认构造方法,则子类的构造方法必须在初始化列表中通过super显式调用父类的某个构造方法。
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
class Animal {
int age;

Animal(this.age);

run() {
print('在奔跑ing');
}
}

class Person extends Animal {
String name;

Person(String name, int age) : name=name, super(age);

@override
run() {
print('$name在奔跑ing');
}

@override
String toString() {
return 'name=$name, age=$age';
}
}

抽象类

使用abstract声明一个抽象类。抽象类不能实例化,抽象类中的抽象方法必须被子类实现, 抽象类中的已经被实现方法, 可以不被子类重写。

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
abstract class Shape {
getArea();
}

class Circle extends Shape {
double r;

Circle(this.r);

@override
getArea() {
return r * r * 3.14;
}
}

class Reactangle extends Shape {
double w;
double h;

Reactangle(this.w, this.h);

@override
getArea() {
return w * h;
}
}

隐式接口

Dart中没有一个专门的关键字来声明接口。Dart不支持多继承,可以使用抽象类实现接口的功能。当将一个类当做接口使用时, 那么实现(implements)这个接口的类, 必须实现这个接口中所有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class Runner {
run();
}

abstract class Flyer {
fly();
}

class SuperMan implements Runner, Flyer {
@override
run() {
print('超人在奔跑');
}

@override
fly() {
print('超人在飞');
}
}

Mixin混入

在通过implements实现某个类时,类中所有的方法都必须被重新实现(无论这个类原来是否已经实现过该方法)。

要想服用之前类的实现,可以使用Mixin混入方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
main(List<String> args) {
var superMan = SuperMain();
superMan.run();
superMan.fly();
}

mixin Runner {
run() {
print('在奔跑');
}
}

mixin Flyer {
fly() {
print('在飞翔');
}
}

// implements的方式要求必须对其中的方法进行重新实现
// class SuperMan implements Runner, Flyer {}

class SuperMain with Runner, Flyer {

}

类成员和方法

使用static关键字来定义类成员和方法。

枚举类型

使用enum关键字来进行定义,枚举类型中有两个比较常见的属性:

  • index: 用于表示每个枚举常量的索引, 从0开始
  • values: 包含每个枚举值的List
1
2
3
4
5
6
7
8
9
10
11
main(List<String> args) {
print(Colors.red.index); //0
print(Colors.values); //[Colors.red, Colors.green, Colors.blue]

}

enum Colors {
red,
green,
blue
}

泛型

List和Map

List和Map中泛型的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建List的方式
var names1 = ['why', 'kobe', 'james', 111];
print(names1.runtimeType); // List<Object>

// 限制类型
var names2 = <String>['why', 'kobe', 'james', 111]; // 最后一个报错
List<String> names3 = ['why', 'kobe', 'james', 111]; // 最后一个报错


// 创建Map的方式
var infos1 = {1: 'one', 'name': 'why', 'age': 18};
print(infos1.runtimeType); // _InternalLinkedHashMap<Object, Object>

// 对类型进行显示
Map<String, String> infos2 = {'name': 'why', 'age': 18}; // 18不能放在value中
var infos3 = <String, String>{'name': 'why', 'age': 18}; // 18不能放在value中

类的泛型

定义一个泛型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
main(List<String> args) {
Location l2 = Location<int>(10, 20);
print(l2.x.runtimeType); // int

Location l3 = Location<String>('aaa', 'bbb');
print(l3.x.runtimeType); // String
}
}

class Location<T> {
T x;
T y;

Location(this.x, this.y);
}

还可以限制T只能是指定类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
main(List<String> args) {
Location l2 = Location<int>(10, 20);
print(l2.x.runtimeType);

// 错误的写法, 类型必须继承自num
Location l3 = Location<String>('aaa', 'bbb');
print(l3.x.runtimeType);
}

class Location<T extends num> {
T x;
T y;

Location(this.x, this.y);
}

方法的泛型

1
2
3
4
5
6
7
8
9
main(List<String> args) {
var names = ['why', 'kobe'];
var first = getFirst(names);
print('$first ${first.runtimeType}'); // why String
}

T getFirst<T>(List<T> ts) {
return ts[0];
}

库的使用

Dart中任何一个dart文件都是一个库,即使你没有用关键字library声明。

库的导入

使用import导入一个库:

1
import '库所在的uri';

导入系统库:

1
import 'dart:io';

导入项目中其他库:

1
2
//当然,你也可以用相对路径或绝对路径的dart文件来引用
import 'lib/student/student.dart';

导入Pub包管理工具管理的一些库,包括自己的配置以及一些第三方的库,通常使用前缀package:

1
import 'package:flutter/material.dart';

库命名冲突

当各个库有命名冲突的时候,可以使用as关键字来使用命名空间:

1
2
3
import 'lib/student/student.dart' as Stu;

Stu.Student s = Stu.Student();

库文件的显示和隐藏

如果希望只导入库中某些内容,或者刻意隐藏库里面某些内容,可以使用show和hide关键字:

1
2
3
// 屏蔽其他
import 'lib/student/student.dart' show Student, Person;
import 'lib/student/student.dart' hide Person;

库的定义

使用关键字library定义一个库。当一个库包含多个文件时,可以使用关键字export进行管理:

1
2
3
4
5
6
7
// 定义:
library utils;
export "mathUtils.dart";
export "dateUtils.dart";

// 使用:
import "lib/utils.dart";

异步支持

Dart类库有非常多的返回Future或者Stream对象的函数。 这些函数被称为异步函数:它们只会在设置好一些耗时操作之后返回,比如像 IO操作。而不是等到这个操作完成。

async和await关键词支持了异步编程,允许写出和同步代码很像的异步代码。

Future

Future表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future只会对应一个结果,要么成功,要么失败。

Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用。

Future.then

使用Future.delayed创建了一个延时任务,即2秒后返回结果字符串”hi world!”,然后我们在then中接收异步结果:

1
2
3
4
5
Future.delayed(new Duration(seconds: 2),(){
return "hi world!";
}).then((data){
print(data);
});

Future.catchError

如果异步任务发生错误,可以在catchError中捕获错误,将上面示例改为:

1
2
3
4
5
6
7
8
9
10
Future.delayed(new Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print("success");
}).catchError((e){
//执行失败会走到这里
print(e);
});

then方法还有一个可选参数onError,我们也可以它来捕获异常:

1
2
3
4
5
6
7
8
Future.delayed(new Duration(seconds: 2), () {
//return "hi world!";
throw AssertionError("Error");
}).then((data) {
print("success");
}, onError: (e) {
print(e);
});

Future.whenComplete

whenComplete中的代码无论成功或失败都会被执行:

1
2
3
4
5
6
7
8
9
10
11
12
Future.delayed(new Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print(data);
}).catchError((e){
//执行失败会走到这里
print(e);
}).whenComplete((){
//无论成功或失败都会走到这里
});

Future.wait

等待多个异步任务都执行结束后才进行一些操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Future.wait([
// 2秒后返回结果
Future.delayed(new Duration(seconds: 2), () {
return "hello";
}),
// 4秒后返回结果
Future.delayed(new Duration(seconds: 4), () {
return " world";
})
]).then((results){
print(results[0]+results[1]);
}).catchError((e){
print(e);
});

Async/await

如果代码中有大量异步逻辑,并且出现大量异步任务依赖其它异步任务的结果时,必然会出现Future.then回调中套回调情况。

async用来表示函数是异步的,定义的函数会返回一个Future对象,可以使用then方法添加回调函数。

await后面是一个Future,表示等待该异步任务完成,异步完成后才会往下走;await必须出现在async函数内部。

1
2
3
4
5
6
7
Future<String> getNetworkData() async {
var result = await Future.delayed(Duration(seconds: 3), () {
return "network data";
});

return "请求到的数据:" + result;
}

Flutter环境搭建

发表于 2021-07-20 | 分类于 Flutter |

Flutter SDK下载

下载SDK:
https://flutter.dev/docs/development/tools/sdk/releases

配置环境变量

环境变量配置文件路径: ~/.bash_profile

1
2
3
export FLUTTER_HOME=/YourPath/flutter
export PATH=$PATH:$FLUTTER_HOME/bin
export PATH=$PATH:$FLUTTER_HOME/bin/cache/dart-sdk/bin

然后通过命令: source ~/.bash_profile重新加载即可。

对终端使用zsh的系统版本,环境变量路径为:~/.zshrc

可以通过命令echo $PATH查看环境变量。

遇到的问题

问题描述:

Exception: Flutter failed to create a directory at “YourPath/flutter”. The flutter tool cannot access the file or directory.

解决: sudo chown -R $USER path

LeeCode[3] - 无重复字符的最长子串

发表于 2019-12-21 | 分类于 数据结构和算法 |

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例1:

1
2
3
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例2:

1
2
3
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例3:

1
2
3
4
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
  请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串

解法(C语言):

阅读全文 »

iOS应用的启动时间优化

发表于 2019-12-16 | 分类于 iOS |

App启动步骤

iOS应用的启动可以分为:

  1. pre-main阶段
    • 系统exec()调用
    • 加载Dyld
    • Dyld递归加载App依赖的Dylib
    • Rebase
    • Bind
    • ObjC
    • Initialiser
  2. main阶段
    • main()调用
    • UIApplicationMain()调用
    • application:willFinishLaunchingWithOptions:调用
    • application:didFinishLaunchingWithOptions:调用
    • 首屏渲染

pre-main阶段概括来说,系统内核会先调用exec()函数,将App加载到内存中。然后系统内核会再加载一个Dyld程序,Dyld负责加载App所依赖的所有动态链接库。再接着Dyld会调整这些动态链接库的指针指向。最后加载Runtime组件,向需要初始化的对象发送初始化消息。

本文主要涉及pre-main阶段。

原理篇

内核简介

内核是操作系统的核心。iOS和OS X使用的都是XNU内核。在这里,我们不需要知道XNU内核的详细架构,只需要知道它的功能即可,例如:提供基础服务,像线程,进程管理,IPC(进程间通信),文件系统等等。

Mach-O

Mach-O (Mac object file format) 是一种运行时可执行文件的文件类型。具体的文件类型包括:

  • Executable:应用主要的二进制文件;
  • Dylib: 动态链接库(类似其他平台上的DSO或DLL);
  • Bundle:特殊的Dylib,不能被链接,只能在运行时使用dlopen()函数打开,Mac OS的插件会用到;
  • Image: 代指一个Executable、Dylib或者Bundle;
  • Framework:包含了资源和头文件目录结构的Dylib;

Segment

一个Mach-O文件被划分为多个Segment,Segment使用全大写字母命名。每一个Segment的大小都是一个内存页大小的整数倍。几乎所有的Mach-O文件都包含这三个Segment:

  • __TEXT: readonly,包含了Mach的头文件,代码,以及常量(比如C字符串);
  • __DATA: read-write,包含了全局变量,静态变量等;
  • __LINKEDIT: 包含了如何加载程序的元数据信息;

例如在下图中,TEXT段的大小为3页,DATA段和LINKEDIT段的大小都为1页。

页面大小则取决于具体的硬件。

Section

每个Segment又被划分为多个Section,Section使用全小写字母命名。Segment的大小与页面大小无关,但是Segment之间互不重叠。

Mach-O Universal File

假如一个Mach-O文件需要同时在32bit和64bit的iOS设备上运行,Xcode会生成两个Mach-O文件,一个支持在32bit设备上运行(armv7s),另一个支持在64bit设备上运行(arm64)。然后将这两个文件合并为一个文件,合并生成的文件就叫做Mach-O通用文件。

Mach-O通用文件会包含一个占用一个页面大小的头部,该头部会包含所支持的体系结构的列表以及相应的偏移量。如图所示:

那为什么segment大小必须是页面大小的整数倍?为什么一个头部要占用一个页面大小?这样是否浪费了大量空间?这就涉及到下部分的内容虚拟内存了。

虚拟内存

可以将虚拟内存理解为一个中间层,其要解决的问题是当多个进程同时存在时,如何对物理内存进行管理。虚拟内存技术提高了CPU利用率,使多个进程可以同时、按需加载。

虚拟内存被划分为一个个大小相同的页面(Page),提高管理和读写的效率。 页面大小则取决于具体的硬件,在arm64处理器上一页的大小为16K,其他处理器上一页的大小为4K。

逻辑地址到物理RAM的映射

每个进程都是一个逻辑地址空间,映射到RAM的某个物理页面。这种映射关系不一定是一对一的,逻辑地址可以不对应任何物理RAM,多个逻辑地址也可能映射到同一块物理RAM。

如果一个逻辑地址不映射到任何物理RAM,当进程要访问该地址时,就会产生页面错误(Page fault),内核将中断该进程,寻找可用的物理内存,接着继续执行当前程序。

如果两个进程的逻辑地址映射到了同一块物理RAM,这两个进程将共享这一块物理内存。

File backed mapping

虚拟内存另一个有意思的特性是基于文件的映射(File backed mapping)。在进行文件读取时,不需要将整个文件都读入到RAM,而是调用mmap()函数将文件的某个片段映射到逻辑内存的某个地址范围。如果要访问的文件内容不在内存中,即发生Page fault时,内核才会去读取要访问的文件内容,从而实现了文件的懒加载。

Copy-On-Write

总的来说,Mach-O文件中的__TEXT段可以使用懒加载的方式映射到多个进程中,这些进程会共享这一块内存。__DATA段是可读可写的,这就涉及到了Copy-On-Write技术,简称COW。

当多个进程共享同一个内存页时,一旦其中一个进程要对__DATA段进行写入操作时,就会发生COW。这时,内核会复制这一页内存,并重定向之前的映射关系,这样进行写入操作的进程就拥有了该页内存的拷贝副本,写入操作完成后不会对其他进程造成影响。

由于包含了进程相关的特定信息,拷贝生成的新内存页被称为Dirty Page。与之相对的被称为Clean Page,可以被内核重新生成(比如重新从磁盘读取)。所以,Dirty Page的内存代价要远远大于Clean Page。

所以在多个进程加载同一个Dylib时,__TEXT和__LINKEDIT因为是只读的,是可以共享内存的。而__DATA因为是可读写的,就会产生Dirty Page(参见下文Rebase和Bind的介绍)。当对Dylib的操作执行结束后,__LINKEDIT就没用了,对应的内存页会被回收。

页面权限

可以将一个内存页标记为readable, writable, executable, 或者这三者的任意组合。

在iOS上,当内存不足的时候,会尝试释放那些被标记为只读的页,因为只读的页在下次被访问的时候,可以再从磁盘读取。如果没有可用内存,会通知在后台的App(也就是在这个时候收到了memory warning),如果在这之后仍然没有可用内存,则会杀死在后台的App。

安全性相关

以下两项技术提升了应用安全性:

  • ASLR:Address Space Layout Randomization,地址空间布局随机化
  • Code Signing: 代码签名

ASLR

简单来说,就是当Mach-O文件映射到逻辑地址空间的时候,利用ASLR技术,可以使得文件的起始地址总是随机的,以避免黑客通过起始地址+偏移量找到函数的地址。

Code Signing

在编译阶段,Mach-O文件的每一个页面都会进行单独的哈希算法加密,所有的哈希值存储在__LINKEDIT中。这保证了每个页面在被加载的过程中都能得到及时验证。

exec()调用

exec()是一个系统调用。当打开一个应用时,ASLR会将应用映射到一个起点随机的地址空间。然后将该随机地址到0的整个地址空间标记为不可访问,即不可读,不可写,不可访问(下图中的PAGEZERO)。

PAGEZERO是一个安全领域, NULL指针就是指向这里。其大小为:

  • 32bit进程:4KB+
  • 64bit进程:4GB+

Dylb

Dyld(Dynamic loader)是一个用来加载动态链接库(Dylib)的帮助程序。当系统内核将应用加载到内存的一个随机逻辑地址之后,就会将Dyld加载到内存的另一个随机逻辑地址。然后程序计数器(PC寄存器)的PC指针指向Dyld,由Dyld完成应用的启动。Dyld的任务是加载应用所需要的所有Dylibs,其操作权限与应用权限相同。

Dyld具体的加载步骤为:

  1. Load dylibs:Map all dependent dylibs, recurse
  2. Fix-ups:Rebasing and Binding
  3. ObjC: Objc prepare images
  4. Initializersre: Run initializers

Load dylibs

加载dylibs的过程可以分为以下几步:

  1. Parse list of dependent dylibs:Dyld首先会读取主执行文件的头部,该头部中包含了所有需要依赖的库的列表;
  2. Find requested mach-o file: 找到列表中依赖库对应的Mach-O文件;
  3. Open and read start of file: 打开Mach-O文件,并读取头部信息,确保文件正确;
  4. Validate mach-o: 验证mach-o文件,找到其代码签名;
  5. Register code signature: 将上一步中找到的代码签名注册到内核中;
  6. Call mmap() for each segment: 在该Mach-o文件的每一个segment上调用mmap()函数

首先,应用直接依赖的库的会被加载完成。但是这些被依赖的库自身可能还会依赖其他的库,Dyld会递归加载这些依赖库,直到所以需要依赖的库都加载完成。平均来说,一个进程会加载1到400个库,这个数量很大,好在大部分都是系统库,系统会进行提前计算和缓存,所以系统库的加载速度非常快。

Dyld是开源的,其地址为:https://opensource.apple.com/source/dyld/。

Fix-ups

在加载完所有依赖的dylib之后,这些dylib暂时相互独立,需要将它们绑定到一起。这就是Fix-ups。

由于代码签名的存在,我们不能对指令进行修改。所以如果一个dylib要调用另外一个dylib,只能通过添加间接层来实现。当调用发生时,code-gen,也就是动态PIC(Position Independent Code, 地址无关编码),会在__DATA段创建一个指针,指向要调用的对象,然后加载这个指针并进行跳转。

也就是说,dylibA想调用dylibB的sayHello方法,code-gen会先在dylibA的__DATA段中建立一个指针指向sayHello,再通过这个指针实现间接调用。

所以Dyld要做的就是修正这些指针和数据。

Fix-ups有两种类型:Rebasing和Binding。

Rebasing 和 Binding

Rebasing是修正指向Mach-O文件内部的指针。Binding则是修正指向Mach-O文件外部的指针。如下图所示: 指向_malloc和_free的指针是修正后的外部指针指向。而指向__TEXT的指针则是被修复后的内部指针指向。

这个步骤产生的原因是上文提到过的ASLR。由于起始地址的偏移,所有__DATA段内指向Mach-O文件内部的指针都需要增加这个偏移量, 这些指针的信息包含在__LINKEDIT段中。既然Rebase需要写入数据到__DATA段,那也就意味着Copy-On-Write势必会发生,Dirty page的创建,IO的开销也无法避免。但Rebase的顺序是按地址排列的,所以从内核的角度来看这是个有次序的任务,它会预先读入数据,减少 I/O 消耗。

Binding是__DATA段对外部符号的引用。不过和Rebase不同的是,Binding是靠字符串的匹配来查找符号表的,虽说没有多少IO,但是计算多,效率比Rebasing慢。

可以通过下面的命令查看 rebase 和 bind 的信息:

1
xcrun dyldinfo -rebase -bind -lazy_bind myapp.app/myapp

ObjC Runtime

大部分ObjC数据结构中的指针(比如IMP或superclass)都会在Fix-ups的过程中被修复。不过Runtime还需要进行一些其他操作:

  1. 将所有类的类名注册到一个全局表中(所以ObjC可以通过类名实例化一个对象)
  2. 将分类中的方法添加到类的方法列表中
  3. 检查Selector的唯一性

Initializers

ObjC中有一个+load方法,不过已经不推荐使用,建议使用+initialize方法进行替代。

如果存在+load方法,此时将会被调用。然后会自底向上的,即从底部的dylib开始,逐步向上加载,直到主执行文件,依次进行初始化。这种加载顺序确保了安全性,加载某个 dylib 前,其所依赖的其余 dylib 文件肯定已经被预先加载。

最后 dyld 会调用 main() 函数。main() 会调用 UIApplicationMain()。

实践篇

启动时间的计算标准

应用启动分为冷启动和热启动,这里只讨论冷启动。为了准确测量冷启动耗时,测量前需要重启设备。

应用的冷启动时间与具体的设备有关,但最好控制在400ms以内。一旦启动时间超过20s,系统会认为应用进入了死循环,并终结该进程。启动时间的测试应该以应用支持的最低配置设备为参考。直到 applicationWillFinishLaunching方法被调动,应用才算启动结束。

要注意的是,UIApplicationMain()和applicationWillFinishLaunching都计算在应用的启动时间中。

测量启动时间

Dyld可以测量main()方法执行之前所消耗的时间,在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 1。运行应用,可以看到如下类似的打印输出:

1
2
3
4
5
6
7
8
Total pre-main time: 228.41 milliseconds (100.0%)
dylib loading time: 82.35 milliseconds (36.0%)
rebase/binding time: 6.12 milliseconds (2.6%)
ObjC setup time: 7.82 milliseconds (3.4%)
initializer time: 132.02 milliseconds (57.8%)
slowest intializers :
libSystem.B.dylib : 122.07 milliseconds (53.4%)
CoreFoundation : 5.59 milliseconds (2.4%)

优化建议

Dylib加载阶段

可以减少使用的Dylib数量,将多个Dylib进行合并。Apple的WWDC2016(Session 406)给出了一个例子: 一个项目依赖26个动态库,dylibs加载时间是240毫秒; 当将其合并成2个动态库时,加载时间变为20毫秒,可以说性能显著提高。

也可以使用静态库。

不建议使用dlopen()对Dylib进行懒加载,可能会造成一些其他问题,而且实际工作量可能会更多。

Rebasing/Binding阶段

根据Rebasing和Binding所进行的操作,可以减少要Fix-ups的指针数量。

具体来说,就是减少ObjC元数据(Class,selector,category)的数量。很多设计模式都会生成大量的类,但这会影响应用的启动速度。

还可以减少使用C++的虚函数,C++会生成一个虚函数表,这也会在__DATA段中创建。

最后推荐使用Swift结构体,因为struct是值类型。

ObjC setup阶段

针对这步所能事情很少,Rebasing/Binding阶段的优化工作也会使得这步耗时减少。

Initializer阶段

在iOS平台下,如果项目使用ObjC,尽量减少使用+load方法,。如果项目使用Swift,Apple已经帮我们调用了他们自己的initializer(dispatch_once), 确保Swift的Class不会被初始化多次。

在这一阶段,可以做的优化有:

  • 减少使用+load方法,如果必须使用, 替换为+initialize
  • 减少构造函数的数量,尽量不要在构造函数里进行耗时任务
  • 减少C++静态全局变量的数量

另外,不要在初始化方法中调用 dlopen(),这会对性能有影响。因为 Dyld 在 App 开始前运行,由于此时是单线程运行所以系统会取消加锁,但 dlopen() 开启了多线程,系统不得不加锁,这就严重影响了性能,还可能会造成死锁以及产生未知的后果。所以也不要在初始化器中创建线程。

补充:Dyld3

上文的讲解都是dyld2的加载方式。而在iOS 13系统中,将全面采用新的dyld3以替代之前版本的dyld2。 因为dyld3完全兼容dyld2,API接口是一样的,所以在大部分情况下,不需要做额外的适配就能平滑过渡。

dyld2是纯粹的in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时候,dyld2才能开始执行任务。

dyld3则是部分out-of-process,部分in-process。图中,虚线之上的部分是out-of-process的,在App下载安装和版本更新的时候会去执行,out-of-process会做如下事情:

  • 分析Mach-o Headers
  • 分析依赖的动态库
  • 查找需要Rebase & Bind之类的符号
  • 把上述结果写入缓存

这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度。

参考链接

  • WWDC2016-406
  • iOS App启动的奥秘
  • 深入理解iOS App的启动过程
  • iOS 13中dyld 3的改进和优化

iOS多线程

发表于 2019-12-04 | 分类于 iOS |

iOS中的常见多线程方案

GCD

GCD源码地址:https://github.com/apple/swift-corelibs-libdispatch。

GCD使用概要:Objective-C之GCD概要。

常用函数

GCD中有2个用来执行任务的函数:

  1. 用同步的方式执行任务
1
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
  1. 用异步的方式执行任务
1
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

队列

GCD的队列可以分为2大类型:

  • 并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步(dispatch_async)函数下才有效
  • 串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)

同步和异步主要影响:能不能开启新的线程

  • 同步:在当前线程中执行任务,不具备开启新线程的能力
  • 异步:在新的线程中执行任务,具备开启新线程的能力

并发和串行主要影响:任务的执行方式

  • 并发:多个任务并发(同时)执行
  • 串行:一个任务执行完毕后,再执行下一个任务

各种队列的执行效果:

使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)。

队列组

参见GCD使用概要:Objective-C之GCD概要。

iOS中的线程同步方案

性能从高到低排序:

  • os_unfair_lock
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex(default)
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSCondition
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

OSSpinLock

OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。目前已经不再安全,可能会出现优先级反转问题,如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。

1
2
3
4
5
6
7
8
9
10
#import <libkern/OSAtomic.h>

// 初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
// 尝试加锁,如果需要等待就不加锁直接返回false,否则加锁后返回true
bool result = OSSpinLockLock(&lock);
// 加锁
OSSpinLockLock(&lock);
// 解锁
OSSpinLockUnlock(&lock);

os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持。从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等。

1
2
3
4
5
6
7
8
9
10
#import <os/lock.h>

// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 尝试加锁
os_unfair_lock_trylock(lock);
// 加锁
os_unfair_lock_lock(&lock);
// 解锁
os_unfair_lock_unlock(&lock);

pthread_mutex

mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <pthread.h>

// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 销毁锁
pthread_mutex_destroy(&mutex);

锁的类型有:

1
2
3
4
#define PTHREAD_MUTEX_NORMAL		0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2 //递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL

递归锁:允许同一个线程对一把锁进行重复加锁。

pthread_mutex条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化锁
pthread_mutex_t mutex;
// NULL表示使用默认属性
pthread_mutex_init(&mutex, NULL);
// 初始化条件
pthread_cond_t cond
pthread_cond_init(&cond, NULL);
// 等待条件(进入休眠,放开mutex锁,被唤醒后会再次对mutex加锁
pthread_cond_wait(&cond, &mutex)
// 激活一个等待该条件的线程
pthread_cond_signal(&cond);
// 激活所有等待该条件的线程
pthread_cond_broadcast(&cond);
// 销毁资源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);

NSLock

NSLock是对mutex普通锁的封装。

核心定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking>

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name;

@end

NSRecursiveLock

NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致。

NSCondition

NSCondition是对mutex和cond的封装。

核心定义如下:

1
2
3
4
5
6
7
8
9
@interface NSCondition : NSObject <NSLocking> 
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name;

@end

NSConditionLock

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值。条件值默认为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface NSConditionLock : NSObject <NSLocking>

// 初始化, 同时设置 condition
- (instancetype)initWithCondition:(NSInteger)condition;

// condition值
@property (readonly) NSInteger condition;

// 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁
- (void)lockWhenCondition:(NSInteger)condition;
// 尝试加锁
- (BOOL)tryLock;
// 尝试加锁, 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
// 解锁, 同时设置NSConditionLock实例中的condition值
- (void)unlockWithCondition:(NSInteger)condition;
// 加锁, 如果锁已经使用, 那么一直等到limit为止, 如果过时, 不会加锁
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 加锁, 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁, 时间限制到limit, 超时加锁失败
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
// 锁的name
@property (nullable, copy) NSString *name;

@end

dispatch_queue

直接使用GCD的串行队列,也是可以实现线程同步的。

1
2
3
4
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
//do something;
});

dispatch_semaphore

semaphore叫做”信号量”。信号量的初始值,可以用来控制线程并发访问的最大数量。信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。

1
2
3
4
5
6
// 初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(long value)
// 计数为0时休眠等待,计数为1或大于1时,减去1而不等待继续执行。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 信号量计数器加1
dispatch_semaphore_signal(semaphore);

@synchronized

@synchronized是对mutex递归锁的封装。@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作。

1
2
3
@synchronized(obj) {
//do something
}

自旋锁、互斥锁比较

什么情况使用自旋锁比较划算?

  • 预计线程等待锁的时间很短
  • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
  • CPU资源不紧张
  • 多核处理器

什么情况使用互斥锁比较划算?

  • 预计线程等待锁的时间较长
  • 单核处理器
  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈

atomic

atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的自旋锁。但是它并不能保证使用属性的过程是线程安全的。

可以参考源码objc4的objc-accessors.mm。

iOS中的读写安全

考虑如下场景:

  • 同一时间,只能有1个线程进行写的操作
  • 同一时间,允许有多个线程进行读的操作
  • 同一时间,不允许既有写的操作,又有读的操作

上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有:

  • pthread_rwlock:读写锁
  • dispatch_barrier_async:异步栅栏调用

pthread_rwlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义读写锁
pthread_rwlock_t rwlock;
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 读取加锁
pthread_rwlock_rdlock(&rwlock);
// 尝试读取加锁
pthread_rwlock_tryrdlock(&rwlock);
// 写入加锁
pthread_rwlock_wrlock(&rwlock);
// 尝试写入加锁
pthread_rwlock_trywrlock(&rwlock);
// 解锁
pthread_rwlock_unlock(&rwlock);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);

dispatch_barrier_async

该函数会等到追加到并发队列上的并行处理全部结束之后, 再将指定的处理追加到该并发队列中。 然后在等该函数追加的处理执行完成后, 该并发队列才恢复为一般动作, 开始执行之后追加的并行处理.

该函数传入的并发队列必须通过dispatch_queue_cretate创建。

1
2
3
4
5
6
7
8
9
10
11
dispatch_t queue = dispatch_queue_create("rwQueue", DISPATCH_QUEUE_CONCURRENT)

// 读
dispatch_async(queue, ^{

});

// 写
dispatch_barrier_async(queue, ^{

});

RunLoop

发表于 2019-12-01 | 分类于 iOS |

RunLoop是iOS/macOS下的事件循环机制,同时也是一个OC对象,该对象管理了其需要处理的时间和消息,并提供了一个入口函数来执行。

Runloop的代码逻辑如下:

1
2
3
4
5
6
7
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}

RunLoop的基本作用:

  • 保持程序的持续运行
  • 处理App中的各种事件(比如触摸事件、定时器事件等)
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
  • ……

以下都属于Runloop的应用范畴:

  • 定时器(Timer)、方法调用(PerformSelector)
  • GCD Async Main Queue
  • 事件响应、手势识别、界面刷新
  • 网络请求
  • 自动释放池 AutoreleasePool

Runloop对象

iOS中有2套API来访问和使用RunLoop:

  • Foundation:NSRunLoop
  • Core Foundation:CFRunLoopRef

NSRunLoop和CFRunLoopRef都代表着RunLoop对象,NSRunLoop是基于CFRunLoopRef的一层OC包装。CFRunLoopRef是开源的:https://opensource.apple.com/tarballs/CF/。

1
2
3
4
5
6
//获取当前线程的runloop
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
//获取主线程的runloop
NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop];
CFRunLoopRef mainRunloop2 = CFRunLoopGetMain();

RunLoop与线程

Runloop与线程有如下关系:

  • 每条线程都有唯一的一个与之对应的RunLoop对象;
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value;
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建;
  • RunLoop会在线程结束时销毁;
  • 主线程会在UIApplicationMain方法中获取Runloop,子线程默认没有开启RunLoop;

在这里需要注意的是,performSelector:withObject:afterDelay:方法的的本质是往Runloop中添加定时器,而子线程默认没有启动Runloop,所以在子线程中调用该方法不会得到正确的响应。

RunLoop与线程的相关源码如下:

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
//  全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
// 访问__CFRunLoops的锁
static CFLock_t loopsLock = CFLockInit;

// 获取pthread 对应的 RunLoop。
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
// pthread为空时,获取主线程
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
__CFLock(&loopsLock);
}
// 从全局字典里获取对应的RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 如果取不到,就创建一个新的RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//设值
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
}
return loop;
}

Runloop相关的类

Core Foundation中关于RunLoop的5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};

CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer。

RunLoop启动时只能选择其中一个Mode,作为currentMode,如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入,这样保证了不同组的Model(Source0/Source1/Timer/Observer)能分隔开来,互不影响。

如果当前Runloop的Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。

常见的2种Mode:

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行;
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响;

另外还有个概念叫CommonModes:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

主线程的RunLoop中预置的两个Mode:kCFRunLoopDefaultMode和 UITrackingRunLoopMode都已经被标记为”Common”属性。

CFRunLoopSourceRef

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。

Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程

主要用于:

  • Source0
    • 触摸事件处理
    • performSelector:onThread:
  • Source1
    • 基于Port的线程间通信
    • 系统事件捕捉

屏幕交互事件通过Source1捕捉,然后分发到Source0处理。

CFRunLoopTimerRef

CFRunLoopTimerRef是基于时间的触发器,其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。主要应用有:

  • NSTimer
  • performSelector:withObject:afterDelay:

CFRunLoopObserverRef

CFRunLoopObserverRef是Runloop的观察者,每个Observer都包含了一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化。主要用于:

  • 用于监听RunLoop的状态
  • UI刷新(BeforeWaiting)
  • Autorelease pool(BeforeWaiting)

一个RunLoop有如下几种状态:

1
2
3
4
5
6
7
8
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};

给Runloop添加Observer的代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry - %@", mode);
CFRelease(mode);
break;
}

case kCFRunLoopExit: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit - %@", mode);
CFRelease(mode);
break;
}
default:
break;
}
});
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

Runloop的运行逻辑

RunLoop 内部的逻辑大致如下:

其代码逻辑整理如下:

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;

/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {

/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}

/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}

/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

/// 收到消息,处理消息。
handle_msg:

/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}

/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}

/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}

/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);


if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}

/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}

/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

RunLoop在实际开中的应用

  • 控制线程生命周期(线程保活)
  • 解决NSTimer在滑动时停止工作的问题
  • 监控应用卡顿
  • 性能优化

Runtime

发表于 2019-11-22 | 分类于 iOS |

Objective-C的动态性是由Runtime API来支撑的,Runtime API提供的接口基本都是C语言的,源代码由C\C++\汇编语言编写。

isa详解

在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址。从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构isa_t,还使用位域来存储更多的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//arm64架构下,isa_t的结构参考如下:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19
};
};

字段含义如下:

  • nonpointer: 0代表普通的指针,存储着Class、Meta-Class对象的内存地址; 1代表优化过,使用位域存储更多的信息;
  • has_assoc: 是否有设置过关联对象,如果没有,释放时会更快;
  • has_cxx_dtor: 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快;
  • shiftcls: 存储着Class、Meta-Class对象的内存地址信息;
  • magic: 用于在调试时分辨对象是否未完成初始化;
  • weakly_referenced: 是否有被弱引用指向过,如果没有,释放时会更快;
  • deallocating: 对象是否正在释放;
  • extra_rc: 里面存储的值是引用计数器减1;
  • has_sidetable_rc: 引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中;

arm64架构下取出shiftcls的掩码ISA_MASK为0x0000000ffffffff8ULL,由此可见,class对象和meta-class对象的地址值最后3位都是0.

Class结构

Class本质上为一个结构体类型:

1
typedef struct objc_class *Class;

与该结构体相关的主要定义如下:

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
struct objc_object {
private:
isa_t isa;
//以下省略
}

struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 类的具体信息
class_rw_t *data() {
return bits.data();
}
//以下省略
}

struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods; //方法列表
property_array_t properties; //属性信息
protocol_array_t protocols; //协议列表
}

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; //instance对象占用的内存大小
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name; //类名
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; //成员变量列表

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
}

class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容。

class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容。

method_t是对方法、函数的封装:

1
2
3
4
5
struct method_t {
SEL name; //函数名
const char *types; //编码(返回值类型,参数类型)
MethodListIMP imp; //指向函数的指针(函数地址)
};

IMP代表函数的具体实现:

1
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);

SEL代表方法或函数名,一般叫做选择器,底层结构跟char *类似:

  • 可以通过@selector()和sel_registerName()获得;
  • 可以通过sel_getName()和NSStringFromSelector()转成字符串;
  • 不同类中相同名字的方法,所对应的方法选择器是相同的;
1
typedef struct objc_selector *SEL;

types包含了函数返回值、参数编码的字符串。 相关介绍可以参考:Type Encodings 。

方法缓存

Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。

1
2
3
4
5
6
7
8
9
10
struct cache_t {
struct bucket_t *_buckets; //散列表
mask_t _mask; //散列表长度-1
mask_t _occupied; //已经缓存的方法数量
}

struct bucket_t {
uintptr_t _imp; //函数地址
SEL _sel; //缓存的key
}

方法调用

OC中的方法调用,其实都是转换为objc_msgSend函数的调用。objc_msgSend的执行流程可以分为3大阶段:

  1. 消息发送
  2. 动态方法解析
  3. 消息转发

objc_msgSend源码导读

消息发送

从源码归纳出如下流程:

动态方法解析

假如在消息发送过程中,没有查找到方法,那么就会进入动态方法解析。动态方法解析就是在运行时临时添加一个方法实现,来进行消息的处理。

开发者可以实现以下方法,来动态添加方法实现:

  • resolveInstanceMethod:
  • resolveClassMethod:

下面是代码示例:

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
void notFound_eat(id self, SEL _cmd)
{
// implementation ....
NSLog(@"%@ - %@", self, NSStringFromSelector(_cmd));
NSLog(@"current in method %s", __func__);
}

//对象方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
// 注意添加到self,此处即类对象
class_addMethod(self, sel, (IMP)notFound_eat, "v16@0:8");
return YES;
}
return [super resolveInstanceMethod:sel];
}

//类方法解析
+ (BOOL)resolveClassMethod:(SEL)sel
{
if (sel == @selector(learn)) {
// 第一个参数是object_getClass(self)
class_addMethod(object_getClass(self), sel, (IMP)notFound_learn, "v16@0:8");
return YES;
}
return [super resolveClassMethod:sel];
}

下面是class_addMethod添加的另一种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)notFound_eat
{
// implementation ....
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(test)) {
// 获取其他方法
Method method = class_getInstanceMethod(self, @selector(notFound_eat));

// 动态添加test方法的实现
class_addMethod(self, sel,
method_getImplementation(method),
method_getTypeEncoding(method));

// 返回YES代表有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}

动态解析过后,会重新进入到“消息发送”的流程,“从receiverClass的cache中查找方法”这一步开始执行。

在动态方法解析完成后,会将标识tridResolver设置为YES,表示已经进行过动态解析,避免消息发送和动态方法解析之间出现死循环。

动态方法解析最佳的一个实践用例就是@dynamic的实现。

消息转发

下面是消息转发阶段的流程图:

super

super调用底层会转换为objc_msgSendSuper2()函数调用, 相关定义及注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 
* Sends a message with a simple return value to the superclass of an instance of a class.
*
* @param super A pointer to an \c objc_super data structure. Pass values identifying the
* context the message was sent to, including the instance of the class that is to receive the
* message and the superclass at which to start searching for the method implementation.
* @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method identified by \e op.
*
* @see objc_msgSend
*/
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

struct objc_super2 {
id receiver; //消息接收者
Class cls; // the class to search,消息接收者的父类
}

使用super调用时,消息的接收者仍然是self,只是会从父类中开始寻找方法。

Runtime API

类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls)

销毁一个类
void objc_disposeClassPair(Class cls)

获取isa指向的Class
Class object_getClass(id obj)

设置isa指向的Class
Class object_setClass(id obj, Class cls)

判断一个OC对象是否为Class
BOOL object_isClass(id obj)

判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)

获取父类
Class class_getSuperclass(Class cls)

成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)

拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)

动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)

拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)

动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)

获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

方法

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
获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)

拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)

选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)

用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)

block

发表于 2019-10-24 | 分类于 iOS |

基本认识

block本质上是一个封装了函数调用以及函数调用环境的OC对象,内部也包含一个isa指针。

block的底层实现

简单的block底层实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr; //函数指针
};

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; //内存占用
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

变量捕获-基础数据类型

auto变量

定义如下一个捕获auto类型变量(离开作用域自动销毁)的block:

1
2
3
4
int age = 10;
void (^block)(void) = ^(){
NSLog(@"%d",age);
};

将其转换为c++代码:

1
2
int age = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

block底层结构为:

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

可见,block在捕获auto变量的方式为值传递。

static变量

定义如下一个捕获static类型变量的block:

1
2
3
4
static int age = 10;
void (^block)(void) = ^(){
NSLog(@"%d",age);
};

将其转换为c++代码:

1
2
static int age = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));

block底层结构为:

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

可见,block在捕获static变量的方式为指针传递。

全局变量

定义如下一个捕获全局变量的block:

1
2
3
4
//age此时被定义为全局变量
void (^block)(void) = ^(){
NSLog(@"%d",age);
};

将其转换为c++代码:

1
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

block底层结构为:

1
2
3
4
5
6
7
8
9
10
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

可见,全局变量可以直接使用,不会被block捕获。

总结

变量捕获-对象类型

当block内部访问了对象类型的auto变量时:

  • 如果block是在栈上,将不会对auto变量产生强引用
  • 如果block被拷贝到堆上:

    • 会调用block内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
  • 如果block从堆上移除:

    • 会调用block内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放引用的auto变量(release)

此时block的底层相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

block的类型

blcok有三种类型,都继承自NSBlock。可以通过class方法或者isa指针查看其具体类型。

  • __NSGlobalBlock__ (_NSConcreteGlobalBlock) :没有访问auto变量
  • __NSStackBlock__ (_NSConcreteStackBlock : 访问了auto变量
  • __NSMallocBlock__ (_NSConcreteMallocBlock): __NSStackBlock\调用了copy方法

下面代码的输出结果为:__NSGlobalBlock__

1
2
3
4
void (^block)(void) = ^(){
NSLog(@"Hello World");
};
NSLog(@"%@",[block class]);

下面代码ARC环境下的输出结果为:__NSMallocBlock__,MRC的输出结果为:__NSStackBlock__

1
2
3
4
5
int age = 10;
void (^block)(void) = ^(){
NSLog(@"%d",age);
};
NSLog(@"%@",[block class]);

下面代码的输出结果为:__NSStackBlock__

1
2
3
4
int age = 10;
NSLog(@"%@",[^{
NSLog(@"%d",age);
} class]);

三种类型block的内存存储以及每一种类型的block调用copy后的结果如下所示:

__block修饰符

底层实现

__block可以用于解决block内部无法修改auto变量值的问题。__block不能修饰全局变量、静态变量(static)。

1
2
3
4
5
6
7
__block int age = 10;
__block NSObject *obj = [[NSObject alloc] init];

void (^block)(void) = ^{
obj = nil;
age = 20;
};

上面的代码会被编译为:

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
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_obj_1 *obj; // by ref
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};

struct __Block_byref_obj_1 {
void *__isa;
__Block_byref_obj_1 *__forwarding;
int __flags;
int __size;
//内存管理
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *obj;
};

//实际执行的block代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_obj_1 *obj = __cself->obj; // bound by ref
__Block_byref_age_0 *age = __cself->age; // bound by ref

(obj->__forwarding->obj) = __null;
(age->__forwarding->age) = 20;
}

可见,__block修饰的变量会被包装成一个对象。

__block变量的内存管理

当block在栈上时,并不会对__block变量产生强引用。

当block被copy到堆时:

  • 会调用block内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会对__block变量形成强引用(retain)[仅ARC, MRC不会retain]

当block从堆中移除时:

  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的__block变量(release)

__forwarding指针

为什么会指向自己呢,原因是为了确保当栈中的Block复制到堆中的时候,在栈中仍然能正确访问堆中的变量。

循环引用

循环引用产生的原因:

解决循环引用:

  • ARC: __weak、__unsafe_unretained
  • MRC: __block
12…13

mangox

每天进步一点点

130 日志
4 分类
34 标签
GitHub
© 2021 mangox
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4