0%

基本概念

虚拟内存 & 物理内存

我们在进程和物理内存之间增加一个中间层,这个中间层就是所谓的虚拟内存,主要用于解决当多个进程同时存在时,对物理内存的管理。提高了CPU的利用率,使多个进程可以同时、按需加载。

所以虚拟内存其本质就是一张虚拟地址和物理地址对应关系的映射表。

  • 每个进程都有一个独立的虚拟内存,从0开始,大小是4G固定的,每个虚拟内存又会划分为一个一个的页(page的大小在iOS中是16K,其他的是4K),每次加载都是以页为单位加载的,进程间是无法互相访问的,保证了进程间数据的安全性。
  • 一个进程中,只有部分功能是活跃的,所以只需要将进程中活跃的页放入物理内存,避免物理内存的浪费。
  • 当CPU需要访问数据时,首先是访问虚拟内存,然后通过虚拟内存去寻找真正的物理内存,然后对相应的物理地址进行访问
  • 如果在访问时,虚拟地址的内容未加载到物理内存,会发生缺页异常(pagefault),此时需要先将数据载入到物理内存(这个过程是很快的),然后再读取。这样就避免了内存浪费

ASLR

虚拟内存的起始地址与大小都是固定的,这意味着,当我们访问时,其数据的地址也是固定的,这会导致我们的数据非常容易被破解,为了解决这个问题,所以苹果为了解决这个问题,在iOS4.3开始引入了ASLR技术。

ASLR的概念:(Address Space Layout Randomization ) 地址空间配置随机加载,是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。

其目的的通过利用随机方式配置数据地址空间,使某些敏感数据(例如APP登录注册、支付相关代码)配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击。

由于ASLR的存在,导致可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要在编译时来修复镜像中的资源指针,来指向正确的地址。即正确的内存地址 = ASLR地址 + 偏移值

可执行文件

通用二进制文件是苹果公司提出的一种新的二进制文件的存储结构,可以同时存储多种架构的二进制指令,使CPU在读取该二进制文件时可以自动检测并选用合适的架构,以最理想的方式进行读取。

由于通用二进制文件会同时存储多种架构,所以比单一架构的二进制文件大很多,会占用大量的磁盘空间,但由于系统会自动选择最合适的,不相关的架构代码不会占用内存空间,且执行效率高了。

Mach-O文件是Mach Object文件格式的缩写,它是用于可执行文件、动态库、目标代码的文件格式。作为a.out格式的替代,Mach-O格式提供了更强的扩展性,以及更快的符号表信息访问速度。经常使用MachOView工具来查看Mach-O文件。

Mach-O的组成

  • Header:主要是Mach-O的cpu架构,文件类型以及加载命令等信息
  • Load Commands:描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示。(通常为framework、dylib的加载)
  • Data:数据区域
    • __TEXT:只读,包括函数、静态变量等
    • __DATA:读写,可变的全局变量等
    • __LINKEDIT:方法、变量

启动

启动的过程一般是指从用户点击app图标开始到[AppDelegate didFinishLaunching方法执行完成为止,其中,启动也分为冷启动和热启动

  • 冷启动:内存中不包含app相关数据的启动,一般我们可以通过重启手机来实现冷启动
  • 热启动:是指杀掉app进程后,数据仍然存在时的启动

而我们这里所说的启动优化,一般是指冷启动情况下的,这种情况下的启动主要分为两部分:

  • T1 :pre-main阶段,即main函数之前,操作系统加载App可执行文件到内存,执行一系列的加载、链接等工作,简单来说,就是dyld加载过程。
  • T2:main函数之后,即从main函数开始,到Appdelegate 的didFinishLaunching方法执行完成为止,主要是构建第一个界面,并完成渲染。

所以,T1+T2的过程 就是 从用户点击App图标到用户能看到app主界面的过程,即需要启动优化的部分。

pre-main阶段

pre-main阶段的启动时间其实就是dyld加载过程的时间。

针对main函数之前的启动时间,苹果提供了内建的测量方法,在Edit Scheme -> Run -> Arguments -> Environment Variables点击【+】添加环境变量 DYLD_PRINT_STATISTICS 设为 1,然后运行,就可以直接打印相关数据。

pre-main阶段一共耗时1.7s。

  • dylib loading time:主要是加载动态库,用时320.32ms
  • rebase/binding time:偏移修正、符号绑定耗时,耗时160.52ms
    • rebase:偏移修正,任何一个app生成的二进制文件,在二进制文件内部所有的方法、函数调用,都有一个地址,这个地址是在当前二进制文件中的偏移地址。在APP运行是,每次系统都会随机分配一个ASLR(Address Space Layout Randomization,地址空间布局随机化)地址值,然后在加上偏移值就是运行时确定的内存地址。
    • binding:绑定,例如NSLog方法,在编译时期生成的mach-o文件中,会创建一个符号!NSLog(目前指向一个随机的地址),然后在运行时(从磁盘加载到内存中,是一个镜像文件),会将真正的地址给符号(即在内存中将地址与符号进行绑定,是dyld做的,也称为动态库符号绑定),一句话概括:绑定就是给符号赋值的过程
  • ObjC setup time:OC类注册的耗时,OC类越多,越耗时
  • initializer time:执行load和构造函数的耗时

pre-main优化建议

  1. 尽量减少外部动态库的使用。苹果建议自定义动态库的数量最好不要超过6个,超过6个则合并动态库。
  2. 减少OC类
  3. 讲不要的+load方法去掉,延迟到+initialize
  4. 尽量减少C++函数

main 阶段

在main函数之后的didFinishLaunching方法中,主要是执行了各种业务,有很多并不是必须在这里立即执行的,这种业务我们可以采取延迟加载,防止影响启动时间。

didFinishLaunching中,主要做一下几件事情:

  1. 初始化第三方sdk
  2. app运行环境配置
  3. 自定义工具的初始化等

main 阶段的优化

  1. 减少启动时需要初始化的sdk等,使用懒加载或者多线程处理。
  2. 优化代码,去除非必须的代码逻辑。
  3. 首页UI加载最后使用纯代码,避免使用xib和storybord(需要转换,更耗时)。
  4. 删除废弃的类、方法。

大招 二进制重排

抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%

System Trace

基于Page Fault,App在冷启动过程中,会有大量的类、分类、三方等需要加载和执行,此时的产生的Page Fault所带来的的耗时是很大的。我们使用System Trace看下,在启动阶段的Page Fault的堆栈。

打开Instruments工具,选则System Trace。启动之后,在第一个界面出来之后,直接停掉。

可以看到PageFault有2800次。

所以二进制重排要怎么重排呢?

二进制重排原理

在虚拟内存部分,我们知道,当进程访问一个虚拟内存page,而对应的物理内存不存在时,会触发缺页中断(Page Fault),因此阻塞进程。此时就需要先加载数据到物理内存,然后再继续访问。这个对性能是有一定影响的。

所以我们就需要把先优先调用的方法等按照一定的顺序进行排列,这样就避免了Page Falut多次执行的情况。

可以查看上面抖音的文章,通过hook,大部分的方法都可以拿到,但是block、c++、initialize等方法是没有办法hook到,最后他们提到了clang插庄。

我们写一个Demo,来看一下编译的时期的顺序以及如何进行clang插庄。

target -> Build Setting -> Write Link Map File设置为YES,可以查看linkMap文件代码的执行顺序。

步奏

添加编译参数

Target -> Build Setting -> Custom Complier Flags -> Other C Flags 添加
-fsanitize-coverage=func,trace-pc-guard 这个参数。此时编译会报错。

如果存在swift混编,还需要在 Other Swift Flags 中加入-sanitize-coverage=func-sanitize=undefined

导入头文件,添加代码

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
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>

//原子队列,其目的是保证写入安全,线程安全
static OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体,以链表的形式
typedef struct {
void *pc;
void *next;
} ALNode;

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);

}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// 这个会拦截load方法,所以把这个注释掉
// if (!*guard) return;
//当前函数返回到上一个调用的地址,可以打断点通过lldb调试,使用bt命令,可以看到pc的值与0、1的关系。
void *PC = __builtin_return_address(0);
//创建结构体
SYNode * node = malloc(sizeof(SYNode));
// node赋值
*node = (SYNode){PC,NULL};

// 加入结构!
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}

当第一个页面加载完成,或者didFinishLaunching之后,调用输出,生成order_file。

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
- (void)createOrderFile {
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (true) {
//offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
if (node == NULL) break;
Dl_info info;
dladdr(node->pc, &info);

NSString * name = @(info.dli_sname);

// 添加不是OC方法,需要添加下划线
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];

//去重
if (![symbolNames containsObject:symbolName]) {
[symbolNames addObject:symbolName];
}
}

//取反
NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
NSLog(@"%@",symbolAry);

//将结果写入到文件
NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
if (result) {
NSLog(@"%@",filePath);
}else{
NSLog(@"文件写入出错");
}
}

链接order file

target -> build setting -> order file 导入我们生成的.order文件。

引用:

抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排
iOS优化篇之App启动时间优化

代码注入

我们按照上一章(应用重签名)的逻辑,先把程序跑起来。

然后再.app中显示包内容,查看可执行文件。这里我们使用MachOView工具进行分析,这里我们主要查看的是Load Commands

Load Commands:加载命令。

Load Commands里头,可以看到所有的Framework。点击每一个Framework可以看到这个Framework的执行路径。

Framework注入

创建动态库

  1. 在工程中选择WeChat.xcodeproj,然后在工程配置页面,选择左下角的加号”+“ -> ”iOS“ -> search ”Framework“。

  2. 创建一个类,添加+(void)load方法,打印一串字符串。

    1
    2
    3
    + (void)load {
    NSLog(@"\n\n hock success...\n\n");
    }
  3. 重新运行工程(使用重签名)。这个时候我们的NSLog并不会执行,因为并没有链接到可执行文件。

    然后在项目工程project -> show in finder -> 找到对应的APP -> 显示包内容 -> Framework文件夹。可以看到我们添加的ALHook.framework。

    然后重新用MachOView工具打开可以执行文件(需要这个可执行文件),看看是否有链接ALHook.framework。这里是没有的。

链接动态库

通过yololib工具修改Mach-O文件,目的就是链接我们添加的动态库。

yololib工具放到我们的工程文件中,与appSign.sh文件同级。然后把上面debug的可执行文件也同样复制过来。

然后执行下面的命令。

1
2
3
$ ./yololib [可执行文件] Frameworks/[添加的动态库].framework/[添加的动态库的可执行文件]

$ ./yololib WeChat Frameworks/ALHook.framework/ALHook

执行完命令之后,重新使用MachOView工具打开可执行文件。

重新压缩,打包ipa文件

  1. 重新解压8.0.2.ipa文件。
  2. 替换Payload/xx.app -> 显示包内容中的可执行文件,把上一部链接好的可执行文件做替换。
  3. 重新压缩 $ zip -ry WeChat.ipa Payload/
  4. 把压缩后的ipa包重新放在工程目录APP文件夹下。

重新运行

这个时候重新运行,就可以看到我们的NSLog了。

dylib注入

我们按照上一章(应用重签名)的逻辑,先把程序跑起来。

创建dylib

然后选择target -> “+” -> “macOs” -> 搜索”library” -> 选择”Library”,命名为”ALHook”。步奏与创建动态库类似。

修改ALHook

ALHook -> “Build Setting”中配置

  1. base sdk改为 iOS
  2. code signing identify 改为 iOS Developer

然后再文件中添加load方法,注入代码输出一串文本。

添加依赖 Copy Files

在当前工程中拷贝dylib。

这里需要注意的是,run script的顺序一定是在Copy Files上的,因为如果顺序反了,会导致最后才执行脚本。则把copy的dylib文件给重新覆盖掉,因为脚本文件执行的是app替换。

使用脚本执行

在上方动态库中我们是手动使用/.yololib工具进行链接的,这里我们使用脚本,在原来的appSigh.sh文件末尾添加一句代码:

1
2
# 链接手动添加的库,做代码注入
./yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/libALHook.dylib"

直接运行

直接运行代码,可以在Framework中看到libALHook.dylib文件。

运行成功之后,可以看到我们的输出。

代码注入流程总结

  • Framework手动注入,是为了熟悉原理,真正操作的时候我们使用的都是脚本文件,从繁到简。
  • Framework流程:
    1. Xcode新建Framework。
    2. 通过yololib工具是Mach-O文件链接Framework文件。
      • 所有的Framework加载都是由DYLD加载进入内存被执行的
      • 注入成功的库路径会写入到Mach-O文件的LC_LOAD_DYLIB字段中
  • dylib注入流程:
    1. Xcode新建dylib库,然后修改”Build Setting”
      1. base sdk改为 iOS
      2. code signing identify 改为 iOS Developer
    2. 添加依赖,Copy Files将dylib文件拷贝到APP包中
    3. 通过yololib工具链接dylib文件。

这里需要注意的是,顺序不能错误,如下图:

真正的代码注入

我们使用Framework的形式进行注入。因为比较方便,dylib需要修改一些东西。

这里也直接使用dylib中使用脚本的方式进行注入。

Debug View Hierarchy调试

我们看一下图片上的内容,通过Debug View Hierarchy的方式,先获取到我们想要的东西。

这里可以看到注册按钮的相关信息:

  1. 是一个FixTitleColorButton的类,应该是封装的Button
  2. 指定的target是WCAccountLoginControlLogic
  3. 相应的action是onFirstViewRegister

能拿到这些信息,就可以直接通过runtime的方法替换就可以直接修改了。

1
2
3
4
5
6
7
8
9
10
11
+ (void)load {
Method oldRegister = class_getInstanceMethod(objc_getClass("WCAccountLoginControlLogic"), @selector(onFirstViewRegister));

Method newRegister = class_getInstanceMethod([self class], @selector(new_register));
method_exchangeImplementations(oldRegister, newRegister);

}

- (void)new_register {
NSLog(@"new register...");
}

这就把原来的注册方法给替换掉了,点击注册会输出者一串字符。

接下来我们要在点击登录时,拿到用户密码,并且不影响正常的登录流程。

class-dump工具

如果我们通过使用上面的方式,可以轻松的拿到用户名和密码,但是这是通过响应链一层层的去找的,所以也就有了这么个工具,可以直接输出OC中的类、方法、属性等内容。

WeChat的可执行文件拷贝出来,和class-dump工具放在相同目录中(也可以是其他目录),然后执行下述命令,可以输出所有的header文件。

1
$ ./class-dump -H WeChat -o ./headers/

然后我们通过Debug View Hierarchy找到对应的账号密码登录页面,查看页面的class和文本框的class,进一步在输出的头文件中找。

我们用同样的方式继续找WCAccountTextFieldItem。发现在WCBaseTextFieldItem有一个WCUITextField,继承自UITextField也就是我们要找的文本框。

在通过Debug View Hierarchy找到登录的点击事件。为onNext。能拿到这些信息,就可以操作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+(void)load {
//原始的Method
Method onNext = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));

//添加新方法!
class_addMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(new_onNext), new_onNext, "v@:");
//交换
method_exchangeImplementations(onNext, class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(new_onNext)));
}

//新的IMP
void new_onNext(id self,SEL _cmd){
UITextField * pwd = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];

NSLog(@"密码是:%@",pwd.text);
//调用回原来的逻辑!!
//调用原来的方法!
[self performSelector:@selector(new_onNext)];
}

注意,这里是在原来的WCAccountMainLoginViewController类中添加方法,而不是用注册时的那种方法,是因为会造成crash,因为使用的是exchange方法交换,在WCAccountMainLoginViewController类执行new_onNext方法时找不到对应的方法。

这种是使用class_addMethodWCAccountMainLoginViewController中添加了一个方法。接下来我们使用setImp和getImp的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+(void)load{
//原始的Method
old_onNext = method_getImplementation(class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)));
method_setImplementation(class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)), new_onNext);

}
// 定义原来的IMP
IMP (*old_onNext)(id self,SEL _cmd);

// 定义新的IMP
void new_onNext(id self,SEL _cmd){
UITextField * pwd = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];

NSLog(@"密码是:%@",pwd.text);
//调用回原来的逻辑!!
//调用原来的方法!
old_onNext(self,_cmd);
}

这种方式相对比较清晰,然后重新运行就好了。

总结

通过Framework、Dylib注入

  • Xcode自动打包Framework进入app包
  • macho中load commands里需要有 LC_LOAD_DYLIB字段
  • DYLD加载我们创建的Framework
  • MethodSwizzle - Runtime中也是重点
    • exchange函数交换SEL和IMP的对应关系
      • 这种方案有可能会造成crash。因为没法调用原来的方法。
    • 解决方案:
      • 添加方法列表,然后exchange或者replace
      • getImp、setImp配合使用

LLVM

LLVM是架构编译器的框架系统,以C++编写而成,用于优化任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time)。对开发者保持开放,并兼容已有脚本。

传统编译器设计

源码Source Code + 前端Frontend + 优化器Optimizer + 后端Backend(代码生成器 CodeGenerator)+ 机器码Machine Code,如下图所示

  • 前端Frontend:编译器前端的任务是解析源代码(编译阶段),它会进行 词法分析、语法分析、语义分析、检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree AST),LLVM的前端还会生成中间代码(intermediate representation,简称IR),可以理解为llvm是编译器 + 优化器, 接收的是IR中间代码,输出的还是IR,给后端,经过后端翻译成目标指令集。

  • 优化器 Optimizer:优化器负责进行各种优化,改善代码的运行时间,例如消除冗余计算等。

  • 后端Backend(代码生成器 Code Generator):将代码映射到目标指令集,生成机器代码,并且进行机器代码相关的代码优化。

iOS的编译架构

OC、C、C++使用的编译器前端是Clang,Swift是swift,后端都是LLVM。

LLVM的设计

当编译器决定支持多种源语言或者多种硬件架构时,LLVM最重要的地方就来了。其他的编译器如GCC,他们都非常成功,但是由于它是作为整体应用程序设计的,因此他们的用途受到了很大的限制。

LLVM设计的最重要方面是,使用通用的代码表示形式(IR),它是用来在编译器中表示代码的形式,所有LLVM可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端。

LLVM的设计是前后端分离的,无论前端还是后端发生变化,都不会影响另一个。

Clang简介

clang是LLVM项目中的一个子项目,它是基于LLVM架构图的轻量级编译器,诞生之初是为了替代GCC,提供更快的编译速度,它是负责C、C++、OC语言的编译器,属于整个LLVM架构中的 编译器前端,对于开发者来说,研究Clang可以给我们带来很多好处。

编译流程

创建一个工程,在main.m中添加代码:

1
2
3
4
5
6
7
8
9
#define c 3
typedef int Demo_Int;

int main(int argc, const char * argv[]) {
int a = 1;
Demo_Int b = 2;
printf("%d", a + b + c);
return 0;
}

我们通过命令可以打印源码的编译流程

1
$ clang -ccc-print-phases main.m

流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
输入文件:找到源文件
+- 0: input, "main.m", objective-c

预处理阶段:这个过程处理包括宏的替换,头文件的导入
+- 1: preprocessor, {0}, objective-c-cpp-output

编译阶段:进行词法分析、语法分析、检测语法是否正确,最终生成IR
+- 2: compiler, {1}, ir

后端:这里LLVM会通过一个一个的pass去优化,每个pass做一些事情,最终生成汇编代码
+- 3: backend, {2}, assembler

汇编代码生成目标文件
+- 4: assembler, {3}, object

链接:一个个"xxx.0"文件链接起来,再链接需要的动态库和静态库,生成可执行文件
+- 5: linker, {4}, image(镜像文件)

绑定:通过不同的架构,生成对应的可执行文件
6: bind-arch, "x86_64", {5}, image

预处理阶段

预处理主要是处理:

  1. 头文件导入
    • 包括引入的头文件中的头文件
  2. 宏定义,替换宏
    • 比如上方定义的#define c 3,预处理后,不会看到c直接会用3替代。

通过命令可以查看预处理后的结果:

1
2
3
4
5
// 在终端直接查看预处理的结果
$ clang -E main.m

// 把预处理的结果输出到main2.m文件中
$ clang -E main.m >> main2.m

编译之后的主要代码如下,删除很多头文件的引入:

1
2
3
4
5
6
7
8
9
10
...

typedef int Demo_Int;

int main(int argc, const char * argv[]) {
int a = 1;
Demo_Int b = 2;
printf("%d", a + b + 3;);
return 0;
}
  1. typedef 处理类型别名时,在预处理阶段不会被替换掉
  2. #define 在预处理阶段会被替换掉。在逆向工程中,通常会被用来进行代码混淆,将核心方法等使用系统相似的名称,来达到代码混淆的目的,使代码更安全。

编译阶段

词法分析语法分析生成中级代码IR等组成。

词法分析

预处理完成后就会进行词法分析,这里会把代码切成一个个token,比如大小括号、等号、字符串、关键词等。

1
$ clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

1
2
3
4
5
6
7
// StartOfLine:main.m文件中,第11行,第1个字符开始,是'typedef'
typedef 'typedef' [StartOfLine] Loc=<main.m:11:1>
// LeadingSpace: main.m文件中,第11行,从第9个字符开始
int 'int' [LeadingSpace] Loc=<main.m:11:9>
identifier 'Demo_Int' [LeadingSpace] Loc=<main.m:11:13>
// 符号";",从main.m的第11行,21个字符开始
semi ';' Loc=<main.m:11:21>

如果代码中有书写错误,在词法分析时不会报错。

语法分析

词法分析完成后就是语法分析,它的任务是验证语法是否正确,在词法分析的基础上将单词序列组合成各类此法短语,如程序、语句、表达式 等等,然后将所有节点组成抽象语法树(Abstract Syntax Tree = AST),语法分析判断程序在结构上是否正确。

1
$ clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

如果导入头文件找不到,可以指定SDK

1
2
3
4
$ clang -isysroot (自己Xcode下对应SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m


$ clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk/ -fmodules -fsyntax-only -Xclang -ast-dump main.m

这是截取了语法分析中的一部分:

这是严格的按照语法进行处理的,从代码的对齐上可以看出点东西来。

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
// 头文件引入 stdio
ImportDecl 0x7fc57905a130 <main.m:9:1> col:1 implicit Darwin.C.stdio
// typedef int Demo_Int;
|-TypedefDecl 0x7fc57905a188 <line:11:1, col:13> col:13 referenced Demo_Int 'int'
| `-BuiltinType 0x7fc57901b500 'int'
// 定义了一个方法main,从13行的第1个字符开始到第18行第1个字符结束,返回int类型,有两个参数int和char **
FunctionDecl 0x7fc57905a460 <line:13:1, line:18:1> line:13:5 main 'int (int, const char **)'
// 第1个参数为int 类型,参数名是argc
|-ParmVarDecl 0x7fc57905a1f8 <col:10, col:14> col:14 argc 'int'
// 第2个参数为char ** 类型,参数名是argv
|-ParmVarDecl 0x7fc57905a310 <col:20, col:38> col:33 argv 'const char **':'const char **'
// 函数的内容范围,从第13行第41个字符开始到第18行第1个字符结束,也就是两个大括号的位置,也就是函数的作用域
`-CompoundStmt 0x7fc578966df0 <col:41, line:18:1>
// 声明局部变量
|-DeclStmt 0x7fc578966688 <line:14:5, col:14>
// 局部变量为a,类型为int
| `-VarDecl 0x7fc578966600 <col:5, col:13> col:9 used a 'int' cinit
// 局部变量a的值为1
| `-IntegerLiteral 0x7fc578966668 <col:13> 'int' 1
|-DeclStmt 0x7fc578966b70 <line:15:5, col:19>
| `-VarDecl 0x7fc5789666d0 <col:5, col:18> col:14 used b 'Demo_Int':'int' cinit
| `-IntegerLiteral 0x7fc578966738 <col:18> 'int' 2
// 调用了函数方法,返回int类型
|-CallExpr 0x7fc578966d60 <line:16:5, col:27> 'int'
// 隐式转换
| |-ImplicitCastExpr 0x7fc578966d48 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7fc578966b88 <col:5> 'int (const char *, ...)' Function 0x7fc578966760 'printf' 'int (const char *, ...)'
| |-ImplicitCastExpr 0x7fc578966da8 <col:12> 'const char *' <NoOp>
| | `-ImplicitCastExpr 0x7fc578966d90 <col:12> 'char *' <ArrayToPointerDecay>
// 对应的string类型转化为char[]类型,
| | `-StringLiteral 0x7fc578966be8 <col:12> 'char [3]' lvalue "%d"
// 运算表达式
| `-BinaryOperator 0x7fc578966ce8 <col:18, line:10:11> 'int' '+'
| |-BinaryOperator 0x7fc578966ca8 <line:16:18, col:22> 'int' '+'
| | |-ImplicitCastExpr 0x7fc578966c78 <col:18> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7fc578966c08 <col:18> 'int' lvalue Var 0x7fc578966600 'a' 'int'
| | `-ImplicitCastExpr 0x7fc578966c90 <col:22> 'Demo_Int':'int' <LValueToRValue>
| | `-DeclRefExpr 0x7fc578966c40 <col:22> 'Demo_Int':'int' lvalue Var 0x7fc5789666d0 'b' 'Demo_Int':'int'
| `-IntegerLiteral 0x7fc578966cc8 <line:10:11> 'int' 3
`-ReturnStmt 0x7fc578966de0 <line:17:5, col:12>
`-IntegerLiteral 0x7fc578966dc0 <col:12> 'int' 0

CompoundStmt:函数的作用域,大括号的开始与结束{}
DeclStmt:局部变量声明
CallExpr:函数调用
BinaryOperator:运算表达式

AST语法树关键字解析
AST抽象语法树

如果当我们写的代码有问题时,在编译阶段就会出现问题,比如我们在上面的代码中删除一个分号,再运行一下命令。

在语法分析阶段就会把错误清晰的暴露出来。

生成中级代码IR

完成以上步骤后,就开始生成中间代码IR了,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR。

可以通过下面命令生成xx.ll的文本文件,也就是IR代码。

1
2
3
4
5
6
// 默认不优化
$ clang -S -fobjc-arc -emit-llvm main.m

// IR文件的优化,在Xcode中target - build setting -optimization level可以设置。
// LLVM的优化登记分别为 -O0、 -O1 、-O2、-O3、-Os(第一个字母为大写O)
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll

以下是IR的基本语法:

1
2
3
4
5
6
7
8
9
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32bit,4个字节
store 写入内存
load 读取数据
call 调用函数
ret 返回

编译之后的IR代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
define i32 @main(i32 %0, i8** %1) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
%6 = alloca i32, align 4
%7 = alloca i32, align 4
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
store i32 1, i32* %6, align 4
store i32 2, i32* %7, align 4
%8 = load i32, i32* %6, align 4
%9 = load i32, i32* %7, align 4
%10 = add nsw i32 %8, %9
%11 = add nsw i32 %10, 3
%12 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 %11)
ret i32 0
}

优化后生成的代码就不放了,自己看吧。

bitcode

在Xcode7以后,开启了bitcode,苹果会做进一步的优化,生成.bc的中间代码。

1
$ clang -emit-llvm -c main.ll -o main.bc

后端

LLVM在后端主要是会通过一个个的Pass去优化,每个Pass做一些事情,最终生成汇编代码。

按照整个llvm的流程,是通过.ll文件生成汇编文件:

1
$ clang -S -fobjc-arc main.ll -o main.s

我们也可以直接使用源文件生成汇编代码:

1
$ clang -Os -S -fobjc-arc main.m -o main.s

这里需要注意的是,在上一步中优化后得到的汇编代码与直接使用优化得到的汇编代码是一样的。

生成目标文件

目标文件的生成,是汇编器以汇编代码作为插入,将汇编代码转换为机器代码,最后输出目标文件(object file),.o结尾。

1
clang -fmodules -c main.s -o main.o

接下来我们看看.o文件中有哪些内容(main.o的符号):

1
$ nm -nm main.o

输出的结果为:

1
2
3
4
$ nm -nm main.o
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _test
000000000000000a (__TEXT,__text) external _main

_printf函数是一个是undefined、external类型的:

  • undefined:表示在当前文件暂时找不到符号_printf。
  • external:表示这个符号是外部可以访问的。

之所以找不到,是因为没有运行,有一些动态库、静态库是需要在运行时才被链接进来的。一堆堆的.o文件,链接起来,最后生成我们的可以执行文件。

链接

连接器把编译生成的.o文件和.dyld、·.a·文件链接,生成一个mach-o文件。

其中,静态库和可执行文件合并,动态库是独立的(系统的动态库可以让所有mach-o访问的)。

1
clang main.o -o main

接下来看一下生成的可执行文件main的符号:

1
2
3
4
5
6
7
$ nm -nm main
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003f6d (__TEXT,__text) external _test
0000000100003f77 (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private

可以看到,把dyld相关的库已经链接到可执行文件中了。

这个时候可以直接运行这个可执行文件:

1
2
$ ./main
6%

直接可以输出结果。

绑定

通过不同的架构,生成对应的mach-o格式的可执行文件。

我们可以直接通过file命令查看可执行文件的类型:

1
2
$ file main
main: Mach-O 64-bit executable x86_64

总结

clang插件

引用

AST语法树关键字解析
AST抽象语法树

Block的类型

Block分为 Malloc Block、 Stack Block、Global Block,但是怎么做区分呢?

在 ARC下:

  • 只要没有访问外部变量(或者访问的是全局变量,静态变量),就是全局block

  • 如果访问了外部变量,在block声明的时候是stack block,在执行了copy之后就是malloc block

  • 执行过copy操作的(全局block除外),是malloc block

结论放这里,可以试一下哈。stack block在下面的章节中有提到过。iOS14之后对stack block做了处理。

循环引用

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

typedef void(^testBlock)(void);
@interface BlockViewController ()

@property (nonatomic, copy) testBlock block;
@property (nonatomic, copy) NSString *name;

@end

@implementation BlockViewController

- (void)viewDidLoad {
[super viewDidLoad];
// 循环引用
self.name = @"blcok demo";
[self blockDemo];
}

- (void)blockDemo {
self.block = ^(void){
NSLog(@"%@", self.name);
};
self.block();
}

- (void)dealloc {
NSLog(@"dealloc 来了");
}

@end

从A页面进来BlockViewController,点击返回,就发现没有走dealloc,因为发生了循环引用。

在BlockVC中,self 持有 block, block持有self,就造成了循环引用。导致无法释放。

self -> block -> self
怎么解决这个问题,按照这个环的形成,正常来说应该有2种解决方案:

打破第一个环,self -> block。

1. 现在是使用的copy修饰的block,如果换成weak修饰是否可以打破?
2. 答案是不行的,因为使用weak修饰,没有被强持有,初始化之后就会被释放掉,block压根不会执行。
3. 所以就只能使用第二种方案了。

打破第二个环,block -> self。

weak strong dance

首先我们使用__weak来处理。

1
2
3
4
5
6
7
- (void)blockDemo {
__weak typeof(self) weakSelf = self;
self.block = ^(void){
NSLog(@"%@", weakSelf);
};
self.block();
}

确实可以解决引用循环。但是会有一个问题,__weak持有的self,是存放在weak表中的,如果,self被释放之后,weakSelf也会被释放掉,整个weak表都会释放。所以当延后执行就会发生问题了。

1
2
3
4
5
6
7
8
9
- (void)blockDemo {
__weak typeof(self) weakSelf = self;
self.block = ^(void){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf.name);
});
};
self.block();
}

我们延后2s打印数据,进来之后,直接退出,发现打印的就是空。这不符合我们的使用。所以就添加了__strong来修饰weak。

1
2
3
4
5
6
7
8
9
10
- (void)blockDemo {
__weak typeof(self) weakSelf = self;
self.block = ^(void) {
__strong __typeof(weakSelf)strongSelf = weakSelf; // 可以释放 when
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf.name);
});
};
self.block();
}

这个时候就没有问题了。

weak-strong-dance是系统自动帮助我们解决引用循环。

中介者模式

我们只需要破坏掉block -> self的这个环就可以了,处理weak-strong之外,还有另外一种方法,中介者模式。

中介者模式则需要我们手动解决循环引用。

  1. 使用__block 创建一个变量,替代self

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    - (void)blockDemo {
    __block ViewController *vc = self;
    self.block = ^(void){
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"%@",vc.name);
    vc = nil;
    });
    };
    self.block();
    }

    使用__block修饰代替self,然后在block执行完毕时,重新置空,也可以打破block持有self的环。

  2. 修改block,添加参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    typedef void(^testBlock)(ViewController *);

    - (void)blockDemo {
    self.block = ^(ViewController *vc){
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"%@",vc.name);
    });
    };
    self.block(self);
    }

    我们可以不让block持有self,可以把self对应的BlockViewController当做参数传到block中,这样也可以打破block持有self的环。

当然还有其他的方式可以解决,毕竟怎么传值的方式有很多种,比如NSProxy等。

Block clang分析

我们在main.m文件中添加一个最简单的block,然后通过clang进行编译,查看main.cpp文件,看看block的一些底层原理。

我们直接创建一个main.m文件。不需要使用工程,当然也可以创建一个工程,在main.m中添加代码:

1
2
3
4
5
6
7
8
9
10
11
#include "stdio.h"

int main() {

void (^block)() = ^(){
printf("hello block");
};
block();

return 0;
}

接下来,我们使用clang命令编译该文件
clang -rewrite-objc main.c -o main.cpp

或者 使用

1
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk main.m

我们通过main.cpp文件学习一下block

没有外部变量的block

1
2
3
4
5
6
7
int main(){

void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

return 0;
}

看起来很乱,我们简化一下:

1
2
3
4
5
6
7
8
int main(){
// block = __main_block_impl_0(参数1,参数2)
void (*block)() = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
// block执行,调用方法,参数是block本身
block->FuncPtr(block);

return 0;
}

我们先看一下__main_block_impl_0

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;
}
};

__main_block_impl_0即是一个结构体,又是一个__main_block_impl_0()方法。结构体内部还有两个结构体implDesc

1
2
3
4
5
6
7
8
9
10
11
struct __block_impl {
void *isa; // isa指针
int Flags; // 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)};
  • impl.isa指针,指向的是stack block
  • impl.Flasg = 0。
  • impl.FuncPtr = fp,也就是__main_block_func_0。这种属于函数式,函数做为参数。
  • Desc = desc,&__main_block_desc_0_DATA

__main_block_func_0这个方法长这样:

1
2
3
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("hello block");
}

函数有一个参数就是block本身。之所以需要当成参数传进来,是因为,方便block获取值来使用,在接下来的block引入外部变量一看就知道了。

block引入外部变量

改变一些main.m,加入一个外部变量a。

1
2
3
4
5
int a = 10;
void (^block)() = ^(){
printf("hello block = %d", a);
};
block();

重新使用clang编译一下:

1
2
3
4
5
6
7
8
int main(){

int a = 10;
void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

return 0;
}

再次简化一下clang编译之后的代码:

1
2
3
int a = 10;
void (*block)() = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
block->FuncPtr(block);

当引入外部变量之后,__main_block_impl_0()变成了3个参数,变量a也被传进去了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a; // blcok内部多了一个a的变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { // a进行直接赋值(c++函数)
impl.isa = &_NSConcreteStackBlock; // 还是stack block
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// a重新生成了一个变量,这个a和外界的变量是同一个值,但是是不同的两块地址,都指向10
int a = __cself->a; // bound by copy
printf("hello block = %d", a);
}

当引入外部变量是,会在block的结构体中会增加一个与外界同名的变量。
在block函数内部,会重新生成一个变量a来指向block->a

这种操作,相当于

1
2
3
int a = 10; // 相当于block的外部变量
int b = a; // b相当于block函数__main_block_func_0中生成的a
printf("pa=%p, pb=%p", &a, &b);

可以试一下,打印a和b的地址,是两个不同的栈空间,指向同一个值。

pa=0x7ffee2c23c1c, pb=0x7ffee2c23c18

这也就是我们没有办法在block内部操作外界变量的原因,在block内没有拿到外界的地址,而是重新生成了一份。

__block修饰的外部变量

1
2
3
4
5
6
__block int a = 10;
void (^block)() = ^(){
a++;
printf("hello block = %d", a);
};
block();

clang编译之后

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(){
// 变量a做了处理,变成了__Block_byref_a_0这种结构体
__Block_byref_a_0 a = {(void*)0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0),
10};
// 函数中增加了参数 &a,把地址传过去
void (*block)() = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a, 570425344));
block)->FuncPtr(block);

return 0;
}

我们查看__Block_byref_a_0这个结构体,它的初始化方法和传的参数是一一对应的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__Block_byref_a_0 a = {
(void*)0, // __isa,因为是一个常量,所以没有指向
(__Block_byref_a_0 *)&a, // __forwarding,指向block外部的变量a的地址
0, // flags
sizeof(__Block_byref_a_0), // size
10 // block外部变量a的值
};

struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};

我们在看block的结构体__main_block_impl_0。多了一个__Block_byref_a_0类型的对象。指向block外部生成的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 指针指向,
__Block_byref_a_0 *a = __cself->a; // bound by ref
// a的__forwarding指向的地址就是外界变量a的地址。
(a->__forwarding->a)++;
printf("hello block = %d", (a->__forwarding->a));
}

所以这里在block内部执行a++,是可以的。

block引用对象类型

1
2
3
4
5
6
7
8
9
10
11
12
NSMutableArray *mArray = [NSMutableArray array];
Person *p = [Person new];
p.name = @"1";

Person *p2 = [Person new];
p2.name = @"2";
[mArray addObject:p2];

void (^block)() = ^(){
NSLog(@"p.name=%@, mArray=%@",p.name, mArray);
};
block();

我们声明了两个变量,一个可变数组,一个person对象。clang编译一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 可变数组的初始化
NSMutableArray *mArray = objc_msgSend(objc_getClass("NSMutableArray"), sel_registerName("array"));
// person初始化
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("new"));
// setName
objc_msgSend(p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_v1_79z2l10138z4855091f7nkh40000gn_T_main_4445e3_mi_0);

// p2初始化
Person *p2 = objc_msgSend(objc_getClass("Person"), sel_registerName("new"));
objc_msgSend(p2, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_v1_79z2l10138z4855091f7nkh40000gn_T_main_4445e3_mi_1);
// addObject:
objc_msgSend(mArray, sel_registerName("addObject:"), p2);

void (*block)() = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, p, mArray, 570425344));
block->FuncPtr(block);

这里与使用__block修饰的变量不一样了,对象类型的与正常的初始化没有什么区别。
再看看block的结构体,以及调用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong p; // __strong 修饰
NSMutableArray *__strong mArray;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _p, NSMutableArray *__strong _mArray, int flags=0) : p(_p), mArray(_mArray) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

// block方法调用
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
Person *__strong p = __cself->p; // bound by copy
NSMutableArray *__strong mArray = __cself->mArray; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_v1_79z2l10138z4855091f7nkh40000gn_T_main_4445e3_mi_2,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("name")), mArray);
}

__main_block_impl_0结构体内部,只是增加了两个用strong修饰符修饰的变量。
__main_block_func_0方法中,使用两个对象时,重新生成了一个__strong修饰的变量。

这里需要注意的是,因为在block内部使用的是__strong修饰的变量。在赋值的时候,会进行深拷贝。注意也只是单层深拷贝,内部元素不会做拷贝。
也就是说,外部变量数组mArray,会被拷贝一份放在block内部。但是指向的都是相同的指针。数组中的元素没有变化。

我们添加一些打印信息,然后打印一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSMutableArray *mArray = [NSMutableArray array];
Person *p = [Person new];
p.name = @"1";

Person *p2 = [Person new];
p2.name = @"2";
[mArray addObject:p2];

NSLog(@"p=%p, p=%@", &p, p);
NSLog(@"mArray=%p, mArray=%p, obj=%@", &mArray, mArray, mArray[0]);

void (^block)() = ^(){

NSLog(@"p1=%p, p1=%@", &p, p);
NSLog(@"mArray1=%p, mArray1=%p, obj1=%@", &mArray, mArray, mArray[0]);
};
block();

运行一下,看下结果:

1
2
3
4
5
p=0x7ffeeb105c10, p=<Person: 0x600000814090>
mArray=0x7ffeeb105c18, mArray=0x600000452a60, obj=<Person: 0x6000008140a0>
-------------- blcok 里
p1=0x60000041fb90, p1=<Person: 0x600000814090>
mArray1=0x60000041fb98, mArray1=0x600000452a60, obj1=<Person: 0x6000008140a0>

涉及到深拷贝和浅拷贝的处理可以看这一篇文章

Block的底层原理

我们用block引入外部__block修饰的变量来做例子,看一下:

1
2
3
4
5
6
__block NSString *name = [NSString stringWithFormat:@"%@", @"name"];
void(^block)(void) = ^{ // 这一行打断点。运行
name = @"block";
NSLog(@"name=%@", name);
};
block();

打上断点,打开汇编调试,运行一下:

1
2
3
4
5
6
7
0x1002062b0 <+100>: nop    
0x1002062b4 <+104>: ldr x10, #0x1d4c ; (void *)0x0000000253e91a20: _NSConcreteStackBlock
0x1002062b8 <+108>: str w9, [sp, #0x48]
0x1002062bc <+112>: str x10, [sp, #0x8]
0x1002062c0 <+116>: nop
0x1002062c4 <+120>: ldr d0, 0x100207f68
0x1002062c8 <+124>: adr x9, #0xa4 ; __main_block_invoke at main.m:21

其实汇编代码后面已经给了注释,我们也读一下这个x10寄存器:

1
2
3
(lldb) register read x10
x10 = 0x0000000253e91a20 libsystem_blocks.dylib`_NSConcreteStackBlock
(lldb)

打印的是一个stack block,我们知道引入了外部变量,会执行block_copy操作,我们再添加一个_block_copy的符号断点,继续执行。

进来_block_copy之后,发现属于libsystem_blocks.dylib这个库,然后我们下载对应的源码。

我们想知道它在内部做了什么。在_block_copy的汇编代码中,在最后的return时,加一个断点。在看看寄存器x0的值:

1
2
3
4
(lldb) register read x0
x0 = 0x00000002818c44e0
(lldb) po 0x00000002818c44e0
<__NSMallocBlock__: 0x2818c44e0>

这个block从stack变成了malloc。接下来我们看看源码:

Block_layout

1
2
3
4
5
6
7
8
struct Block_layout {
void *isa; // isa指针
volatile int32_t flags; // contains ref count // 标志状态,是一个枚举
int32_t reserved; // 保留字段,可能有其他的作用
BlockInvokeFunction invoke; // 函数执行
struct Block_descriptor_1 *descriptor; // block的描述信息,size
// imported variables
};

_Block_copy的源码中,第一行就是Block_layout,这个才是block真正的样子,一个结构体。

我们在看看flags都有哪些值,表示什么意思:

1
2
3
4
5
6
7
8
9
10
11
12
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime 标记正在释放
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 存储引用计数的值
BLOCK_NEEDS_FREE = (1 << 24), // runtime 是否增加或减少引用计数的值
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 是否拥有拷贝辅助函数 确定block是否存在Block_descriptor_2这个参数
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code 是否有C++析构函数
BLOCK_IS_GC = (1 << 27), // runtime 是否有垃圾回收
BLOCK_IS_GLOBAL = (1 << 28), // compiler 是否是全局block
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler 是否拥有签名
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler 确定Block_descriptor_3中的layout参数
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 这个结构体是block_layout必有的变量
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};

// 可选 这两个是可选变量。
#define BLOCK_DESCRIPTOR_2 1
// 当flag=BLOCK_HAS_COPY_DISPOSE时才会存在
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};


#define BLOCK_DESCRIPTOR_3 1
// 当flag=BLOCK_HAS_SIGNATURE时才会存在
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

Block_descriptor_3当中存放的是block的签名信息。我们知道block是一个匿名函数,只要是一个函数就会有签名,比如v8@?0这种样式的就是签名。

其中v表示返回值是void@?表示未知的对象,即为block
这和方法签名是有所不同的,方法签名一般是v@:这样的形式(此处只说返回值为void的场景),:表示SEL

接下来,我们看下_Block_copy

_Block_copy

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
void *_Block_copy(const void *arg) {
// 创建一个新的block
struct Block_layout *aBlock;
// arg就是栈上的block
if (!arg) return NULL;

// 直接赋值,新创建的aBlock指向arg
aBlock = (struct Block_layout *)arg;
// 我们已经对flags的值做了解释,这里做判断,其实这里就是一个堆区的block
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
// 处理refcount相关(引用计数)
latching_incr_int(&aBlock->flags);
// 直接返回
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
// 全局区block,直接返回
return aBlock; // 不需要
}
else {
// 这里就只有可能是栈区的block
// Its a stack block. Make a copy.
// 申请内存空间,大小与aBlock一样
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
// 将栈区的数据copy到堆区
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
// 设置invoke,这样堆上的block调用才会与栈上一致
result->invoke = aBlock->invoke;
#endif
// reset refcount
// 重置refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
// 设置flags
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
// 设置isa的为malloc block
result->isa = _NSConcreteMallocBlock;
return result;
}
}

// 处理引用计数
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
return old_value+2;
}
}
}

block copy主要做了以下操作:

  • malloc block处理引用计数,直接返回
  • global block不做任何处理
  • stack block
    1. 申请内存空间
    2. 将栈区的数据拷贝到堆区
    3. 设置isa指向malloc block

Block_byref

还记得上面的引用__block变量的block,clang编译之后的样子吗?改用NSString变量之后,又是另一种风味。

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
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_name_0 name = {
(void*)0,
(__Block_byref_name_0 *)&name,
33554432,
sizeof(__Block_byref_name_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_nw_tqjtztpn1yq6w0_wmgdvn_vc0000gn_T_main_41740c_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_nw_tqjtztpn1yq6w0_wmgdvn_vc0000gn_T_main_41740c_mi_1)
};

void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_name_0 *)&name, 570425344));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
}
}

struct __Block_byref_name_0 {
void *__isa; // 8
__Block_byref_name_0 *__forwarding; // 8
int __flags; // 4
int __size; // 4
void (*__Block_byref_id_object_copy)(void*, void*); // 8
void (*__Block_byref_id_object_dispose)(void*); // 8
NSString *name;
};

__block修饰的name对象被转化成了一个__Block_byref_name_0的结构体,在源码中也有一个Block_byref与之对应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};

// 可选变量
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep;
BlockByrefDestroyFunction byref_destroy;
};

// 可选变量
struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};

我们把__Block_byref_name_0Block_byref放在一起比较一下:

1
2
3
4
5
6
7
__Block_byref_name_0                ->  Block_byref
(void*)0, -> isa
(__Block_byref_name_0 *)&name, -> forwarding
33554432, -> flags
sizeof(__Block_byref_name_0), -> size
__Block_byref_id_object_copy_131, -> byref_kep
__Block_byref_id_object_dispose_131,-> byref_destroy

在上面的章节中,有一个点没有说,这里再重新说一下,这个外界的变量是怎么被拷贝进block里头的。

1
void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_name_0 *)&name, 570425344));

在block声明的时候,还记得吧,其中有__main_block_desc_0_DATA这个参数。

1
2
3
4
5
6
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};

__main_block_desc_0_DATA的生成也是通过方法调用来产生的,而变量的拷贝就是发生在这里__main_block_copy_0

1
2
3
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->name, (void*)src->name, 8/*BLOCK_FIELD_IS_BYREF*/);
}

接下来看看_Block_object_assign是怎么实现的,在源码里头:

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
//_Block_object_assign有三个参数,第三个参数与下面的枚举值相对应。
enum {
BLOCK_FIELD_IS_OBJECT = 3, // 截获的是对象 __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // 截获的是block变量(不是block的参数)
BLOCK_FIELD_IS_BYREF = 8, // 截获的是__block修饰的对象
BLOCK_FIELD_IS_WEAK = 16, // 截获的是__weak修饰的对象
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};

// 根据传入的对象的类型
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
// 对象类型
case BLOCK_FIELD_IS_OBJECT:
// retatin count +1,
_Block_retain_object(object);
// 持有object,多了一次强引用
*dest = object;
break;
// block
case BLOCK_FIELD_IS_BLOCK:
// 把block从栈上拷贝到堆上,详情看上方_Block_copy的分析,只是参数变化
*dest = _Block_copy(object);
break;

case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
// __block修饰的变量
case BLOCK_FIELD_IS_BYREF:
*dest = _Block_byref_copy(object);
break;

case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
*dest = object;
break;

case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
*dest = object;
break;

default:
break;
}
}

__block修饰的变量拷贝

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
static struct Block_byref *_Block_byref_copy(const void *arg) {

// Block_byref 结构体
struct Block_byref *src = (struct Block_byref *)arg;

if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
// 创建新值,申请内存空间
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
// 设置flags
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;

// copy的forwarding指向自己,自己在堆里
copy->forwarding = copy; // patch heap copy to point to itself
// src是老值,指向的也是自己。指向的是堆,所以block内部改变,外部也会改变
src->forwarding = copy; // patch stack to point to heap copy

copy->size = src->size;

if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
// 通过内存偏移,获取Block_byref_2
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
// 存值
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;

// 这里会判断,有没有Block_byref_3,有的话也是通过内存地址偏移来获取
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
// 执行拷贝__Block_byref_id_object_copy
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}

return src->forwarding;
}

在新生成的结构体__Block_byref_name_0中,还有一个名为__Block_byref_id_object_copy_131的方法:

1
2
3
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

就是根据地址便宜来获取name的值,这里偏移的是40个字节。看一下__Block_byref_name_0中的变量,name上边的所有变量加起来所占的内存是40个字节。所以这里是对name做了一次拷贝。

对于__block修饰的变量可以在block内部可以修改,主要是因为:

  1. block从栈区,拷贝到堆区
  2. __block修饰的变量name,会生成一个新的结构体__Block_byref_name_0,拷贝到blcok内部
  3. 对元类的值进行拷贝,并修改原来值的指向(指向为block内部值)

block释放

__main_block_desc_0_DATA中还有另外一个参数__main_block_dispose_0.

1
2
3
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->lg_name, 8/*BLOCK_FIELD_IS_BYREF*/);
}

我们看一下_Block_object_dispose是怎么释放的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void _Block_object_dispose(const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
// 如果是__block修饰的,使用_Block_byref_release
case BLOCK_FIELD_IS_BYREF:
// get rid of the __block data structure held in a Block
_Block_byref_release(object);
break;
// block
case BLOCK_FIELD_IS_BLOCK:
_Block_release(object);
break;
// 对象类型的变量
case BLOCK_FIELD_IS_OBJECT:
// 调用系统方法,不用处理
_Block_release_object(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
break;
default:
break;
}
}

_Block_byref_release的释放,消耗新创建的Block_byref结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void _Block_byref_release(const void *arg) {
struct Block_byref *byref = (struct Block_byref *)arg;

// dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
byref = byref->forwarding;

if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
os_assert(refcount);
if (latching_decr_int_should_deallocate(&byref->flags)) {
if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
(*byref2->byref_destroy)(byref);
}
free(byref);
}
}
}

block的释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void _Block_release(const void *arg) {
struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;
// 全局block不用释放
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
// 还有引用计数则没办法释放
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;

if (latching_decr_int_should_deallocate(&aBlock->flags)) {
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
free(aBlock);
}
}

总结

  • block的分类:global block、stack block,malloc block
  • block的的内部实现
  • block调用外部变量
  • __block修饰的变量

应用重签名

codesign 命令

Xcode提供了签名工具codesign,我们可以通过几个命令iu可以完成重签名。

  • codesign -vv -d [WeChat.app] 查看签名
  • $ security find-identity -v -p codesigning 查看钥匙串力可以签名的证书
  • $ Codesign –fs [证书串] [filename] 强制替换签名
  • $ Chmod +x [filename] 给文件添加可执行权限权限
  • $ security cms -D -i ../embedded.mobileprovision 查看描述文件
  • $ codesign -fs [证书串] --no-strict --entitlements=权限文件.plist APP包
  • $ Zip –ry [file] [outfile] 将file压缩为outfile
  • $ otool -l [WeChat] 查看mach-O(可执行)文件, cryptid等于0说明是砸壳之后的(没有加密),等于1说明是加密的
  • $ otool -l WeChat > ~/Desktop/123.txt 把文件导出到123.txt中

codesign 签名步奏

首先我们需要拿到砸壳之后的.ipa文件,然后对这个文件解压缩,在Payload文件夹下看到对应的app文件。

查看签名

$ codesign -vv -d WeChat.app

如果没有重签名,使用的证书还是微信自己的证书。

查看本地钥匙串中可用的证书

$ security find-identity -v -p codesigning

会列出钥匙串中可用的证书。

强制签名

选中WeChat.app,显示包内容

app包中主要有3个文件需要处理,我们需要对这3个文件做重签名:

  1. 是资源文件的签名
  2. Mach-O文件的签名
  3. Framework的签名

但是内部的PlugIns文件夹中的插件,watch文件夹下的内容,使用免费的证书是无法签名的。可以删除掉。

然后我们先对Framework文件夹下的内容签名:

$ codesign -fs "Apple Development: xxx" andromeda.framework

所有的framework都要重签。

创建新的描述文件

  1. Xcode新建工程,命名为WeChat,选择对framework重签名使用的证书。然后在手机上运行一下。
  2. 在工程中选中Products,打开WeChat.app所在的文件夹。然后显示包内容,找到embedded.mobileprovision这个描述文件,复制到WeChat.app内。
  3. 添加了描述文件之后,还需要修改info.plist的bundle id,因为描述文件中是有bundle id信息的。

更改权限

接下来我们看看embedded.mobileprovision的内容,因为我们需要拿到它的权限部分对WeChat重签名。

  1. $ security cms -D -i embedded.mobileprovision查看embedded.mobileprovision的内容,找到Entitlements这个key对应的内容(注意拷贝时不需要key),复制到一个新的plist文件中,命名为Entitlements
  2. 然后把这个Entitlements.plist文件拷贝到Payload文件夹下,与WeChat.app同级。
  3. $ ls -l WeChat查看Mach-O文件是否有可以执行权限,没有的话使用命令$ chmod a+x WeChat设置可执行权限。

app重签

对APP重签,其实就是对内部Mach-O重签。
退回到Payload文件夹下,对WeChat.app签名:

$ codesign -fs "Apple Development: xxx" --no-strict --entitlements=Entitlements.plist WeChat.app

安装

使用Xcode - Window - Devices and Simulators,点击”+“,选择对应的app安装,安装完成之后,点击就能直接运行了。

注意:别登录,容易被封号,我们只是完成了万里长征的第一步而已。

这就是通过命令行使用codesign进行重签名的流程。

Xcode重签名

我们对拿到的砸壳之后ipa文件进行解压缩,打开Payload文件夹:

  1. 删除PlugInsWaatch文件夹。
  2. 使用命令行对Framework重签名。
  3. 创建并运行一个空项目工程,项目名字需要与重签的xx.app文件名一致。并运行到手机上。
  4. 修改xx.app内部的info.plist文件下的bundle id,与新工程一致。
  5. 在工程下选择Products,打开xx.app所在的文件夹,把第4步修改后的app复制过来,直接替换xx.app
  6. 直接运行。

codesign 与 Xcode签名比较

codesign签名相比,直接使用Xcode重签名少了好几步:

  1. 省略了添加描述文件
  2. 省略了添加描述文件的权限
  3. 省略了通过权限重签app

Shell 重签

shell是一种特殊的交互式工具,它为用户提供了启动程序、管理文件系统中文件以及运行在系统上的进程的途径。
Shell一般是指命令行工具。它允许你输入文本命令,然后解释命令,并在内核中执行。
Shell脚本,也就是用各类命令预先放入到一个文本文件中,方便一次性执行的一个脚本文件。

iTerm 切换bash和zsh的
$ chsh -s /bin/bash
$ chsh -s /bin/zsh

文件类型与权限

可以通过$ls -l命令查看权限

1
2
drwxr-xr-x  5  Alan  staff      160    10 19 2015  Public
[文件权限] 连接 所有者 所属组 文件大小 最后修改时间 文件名称

d:文件类型 表示目录。-表示 文件
前3位 rwx:文件所有者(user)的权限
中3位 r-x:这一组其他用户(group)的权限
后3为 r-x:非本组的用户(other)的权限,

缩写 释义 二进制 十进制
r read 读 0100 4
w write 写 0010 2
x execut 可执行 0001 1

我们经常会用的一个命令 $chmod 755 filename来改变file的权限

755表示的就是权限,分别为user、group、other的权限。
7: 4 + 2 + 1 = r + w + x
5: 4 + 1 = r + x

也可以使用符号类型来表示:

$ chmod [u、g、o、a][+(加入)、-(除去)、=(设置)] [r、w、x] 文件名称
u:user
g:group
0:other
a:all

例如 $ chmod a+x 123.txt 表示所有人都有可执行权限。

shell重签名

就是在Xcode中,运行时执行shell脚本,自动帮我们处理签名的过程。在写shell脚本的时候,有以下几个步奏:

  1. 解压ipa文件到Temp文件夹下
  2. 将解压出来的app拷贝到工程下
  3. 删除PlugIns和Watch相关的文件
  4. 更新info.plist文件中bundle id
  5. 给mach-o文件执行权限
  6. 重签名framework
  7. 替换签名
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
# ${SRCROOT} 它是工程文件所在的目录
TEMP_PATH="${SRCROOT}/Temp"
#资源文件夹,我们提前在工程目录下新建一个APP文件夹,里面放ipa包
ASSETS_PATH="${SRCROOT}/APP"
#目标ipa包路径
TARGET_IPA_PATH="${ASSETS_PATH}/*.ipa"
#清空Temp文件夹
rm -rf "${SRCROOT}/Temp"
mkdir -p "${SRCROOT}/Temp"

#----------------------------------------
# 1. 解压IPA到Temp下
unzip -oqq "$TARGET_IPA_PATH" -d "$TEMP_PATH"
# 拿到解压的临时的APP的路径
TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1")
# echo "路径是:$TEMP_APP_PATH"

#----------------------------------------
# 2. 将解压出来的.app拷贝进入工程下
# BUILT_PRODUCTS_DIR 工程生成的APP包的路径
# TARGET_NAME target名称
TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
echo "app路径:$TARGET_APP_PATH"

rm -rf "$TARGET_APP_PATH"
mkdir -p "$TARGET_APP_PATH"
cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH"

#----------------------------------------
# 3. 删除extension和WatchAPP.个人证书没法签名Extention
rm -rf "$TARGET_APP_PATH/PlugIns"
rm -rf "$TARGET_APP_PATH/Watch"

#----------------------------------------
# 4. 更新info.plist文件 CFBundleIdentifier
# 设置:"Set : KEY Value" "目标文件路径"
# PlistBuddy工具修改info.plist文件中的BundleID为工程的BundleID
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"

#----------------------------------------
# 5. 给MachO文件上执行权限
# 拿到MachO文件的路径
APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
#上可执行权限
chmod +x "$TARGET_APP_PATH/$APP_BINARY"

#----------------------------------------
# 6. 重签名第三方 FrameWorks
TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
then
for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
do

#签名
#--force --sign 替换签名
# EXPANDED_CODE_SIGN_IDENTITY 当前工程的证书
/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
done
fi

这里在第一次运行Xcode时,注意要先注释掉该shell脚本,运行一次之后再打开该shell脚本,重新运行。

总结

  • codesign重签名
    1. 删除不能签名的文件:PlugIns(Extension)、Watch
    2. 重签名Framework
    3. 给Mach-O可执行权限(chmod命令)
    4. 修改info.plist文件中的bundle id
    5. 创建新工程运行,拷贝描述文件(该描述文件要在iOS系统中信任过)
    6. 利用描述文件中的权限文件签名整个APP包
  • Xcode重签名
    1. 删除不能签名的文件:PlugIns(Extension)、Watch
    2. 重签名Framework
    3. 给Mach-O文件可执行权限(chmod命令)
    4. 替换掉Xcode运行后产生的APP包
  • Shell重签名
    • shell切换
    • 老版本的Mac系统默认是bash,新系统(Sierra之后)默认shell是zsh
    • zsh的环境变量在~/.zshrc中,bash的环境变量在~/.bash_profile
    • 用户权限 chmod
      • 数字 r:4 w:2 x:1 chmod 751 filename
      • 字符 u:创建者 g:组 o:其他 a:所有 chmod a+x filename

还是推荐使用shell脚本重签名,因为以上两种方式虽然可行,但是会很麻烦,稍有不慎则会出现意外。
第二种使用Xcode签名,无法控制顺序,虽然做了app包替换,但是在运行时,还是会替换info.plist,导致有一些app安装之后,运行会出现问题。

代码签名

代码签名是对可执行文件或脚本进行数字签名,用来确认软件在签名后未被修改或损坏的措施。和数字签名原理一样,只不过签名的数据是代码。

简单的代码签名

在iOS出来之前,以前的主流操作系统(Mac/Windows)软件随便从哪里下载都能运行,系统安全存在隐患,盗版软件、病毒入侵、静默安装等等,那么苹果希望解决这样的问题,要保证每一个安装到 iOS 上的 APP 都是经过苹果官方允许的,为了保证都是经过苹果官方允许的,就有了代码签名

如果要实现验证,其实最简单的方式就是通过苹果官方生成非对称加密的一对公私钥,在iOS的系统中内置一个公钥私钥由苹果后台保存,我们传APP到AppStore时,苹果后台用私钥对APP数据进行签名,iOS系统下载这个APP后,用公钥验证这个签名,若签名正确,这个APP肯定是由苹果后台认证的,并且没有被修改过,也就达到了苹果的需求:保证安装的每一个APP都是经过苹果官方允许的。

如果我们iOS设备安装APP只从App Store这一个入口这件事就简单解决了,没有任何复杂的东西,一个数字签名搞定。

但是实际上iOS安装APP还有其他渠道。比如对于我们开发者而言,我们是需要在开发APP时直接真机调试,而且苹果还开放了企业内部分发的渠道,企业证书签名的APP也是需要顺利安装的。

苹果需要开放这些方式安装APP,这些需求就无法通过简单的代码签名来办到了。

双重签名

为了解决上述的问题,苹果采用了双层签名。

首先我们需要两个设备,一个iOS系统的设备,一个Mac系统的设备。iOS开发是在Mac环境下进行的,这也就是双层签名的基础。

  1. 在Mac上生成非对称加密算法的公钥、私钥。我们称之为公钥M、私钥M。M=Mac
  2. 苹果自己有一套公钥、私钥,与简单签名中的逻辑一致,私钥在苹果后台,公钥在每一个iOS系统的设备中,称之为公钥A,私钥A,A=Apple
  3. 公钥M打包开发者的信息,上传到苹果后台(通过CSR请求文件申请证书)。苹果后台用私钥A去签名公钥M,得到一份包含了公钥M的Hash值(签名)以及开发者信息等。这个东西就是证书。然后下载到本地
  4. 在开发时,编译完一个APP,用本地的私钥M(p12文件),对这个APP进行签名,其实就是对Mach-O文件签名,同时步奏3得到的证书也一同打包,生成.ipa文件。
  5. 这个ipa文件也就是我们的APP,安装到手机时,手机上有公钥A,通过公钥A解密步奏3得到的证书,可以拿到公钥M。
  6. 然后再用公钥M验证步奏4对APP的签名。如果签名验证通过,则APP就是合法的。

但是这样还是会有问题,我们只要有了证书,任何设备都可以安装,而且没有设备数限制。

描述文件

苹果为了解决上面的问题,就又加了限制,引入了描述文件。

  1. 对开发者来说,只有添加了udid的设备才能安装。
  2. 只能安装某一个,或者某一类的APP。(指定了bundle id,或者是通配类型:com.apple.*)
  3. 还指定了APP的权限(Entitlements),比如:iCloud、Push、后台运行等等权限,由xcode生成。

苹果把这些内容,放在一个文件中,叫做描述文件(Provisioning Profile)。后缀名为mobileprovision

描述文件一般包括三样东西:证书、App ID、设备信息、权限信息。当我们在真机运行或者打包一个项目的时候,证书用来证明我们程序的安全性和合法性。

所以我们再来看看真正定双重签名。

真正的双重签名、验证

  1. 在Mac上生成非对称加密算法的公钥、私钥。我们称之为公钥M、私钥M。M=Mac
  2. 苹果自己有一套公钥、私钥,与简单签名中的逻辑一致,私钥在苹果后台,公钥在每一个iOS系统的设备中,称之为公钥A,私钥A,A=Apple
  3. 公钥M打包开发者的信息,上传到苹果后台(通过CSR请求文件申请证书)。苹果后台用私钥A去签名公钥M,得到一份包含了公钥M的Hash值(签名)以及开发者信息,这个东西就是证书(CRT。可以下载到本地,如果多开发者协同开发,需要把证书的密钥导出来,生成p12文件)。同时打包bundle id,设备的udid、APP权限等和证书一起生成描述文件。然后下载到本地。(多开发者协同开发,需要把对应的p12文件、描述文件一起给开发人员)
  4. 在开发时,选择对应的描述文件,用本地的私钥M(p12文件),对Mach-O文件签名,同时步奏3得到的描述文件也一同打包,生成.ipa文件。
    权限文件为embedded.mobileprovision(由xcode生成),也在ipa内。包含了设备udid的数组、权限数组、app id、有效时间等信息
  5. 这个ipa文件也就是我们的APP,安装到手机时,通过公钥A解密步奏3得到的证书,可以拿到公钥M。
  6. 然后再用公钥M验证步奏4对APP的签名。如果签名验证通过,则APP就是合法的。

总结:

  • 苹果的双重签名验证流程
  • 生成的证书,需要导出p12文件和描述文件一起发给其他开发人员。

关于证书相关的配置这里推荐一篇来自知乎的文章,iOS证书配置。写的比较详细了。

NSLock

老规矩,直接上代码:

1
2
NSLock *lock = [[NSLock alloc] init];
[lock lock];

之所以这么写,就是为了找到NSLock所在的底层库,我们在[lock lock]这一行打个断点,然后使用符号断点symbolic breakpoint添加一个lock的符号,全靠盲猜。因为使用符合断点是最有效的方法,有时候汇编也不一定能进去。

很容易就找到了,这是在foundation框架中的,但是这个框架是没有开源的,怎么搞呢?

还有swift源码哈~莫慌

NSLock.swift文件中可以找到NSLock的类,下方代码对其做了删减。

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
open class NSLock: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif
// init 初始化,内部是pthread_mutex_init,是个互斥锁
public override init() {
pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
}
// dealloc,析构方法
deinit {
pthread_mutex_destroy(mutex)
mutex.deinitialize(count: 1)
mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
}
// 加锁
open func lock() {
pthread_mutex_lock(mutex)
}
// 解锁
open func unlock() {
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
}
// tryLock
open func `try`() -> Bool {
return pthread_mutex_trylock(mutex) == 0
}
// lockBeforeDate
open func lock(before limit: Date) -> Bool {
if pthread_mutex_trylock(mutex) == 0 {
return true
}

#if os(macOS) || os(iOS) || os(Windows)
return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#endif
}

open var name: String?
}

这些方法在iOS中都可以找到,我们通过源码可以看到内部的实现逻辑。

1
2
3
4
5
6
7
8
9
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}

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

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

NSLock内部调用的就是pthread的互斥锁。

首先我们运行一下下面对的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NSLock *lock = [[NSLock alloc] init];
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
for (int i = 0; i < 100; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 1. lock位置 1
//[lock lock];
//[recursiveLock lock];
// 这里定义了一个block
static void(^testMethod)(int value);
testMethod = ^(int value) {
// 2. lock位置 2
//[lock lock];
//[recursiveLock lock];
if (value > 0) {
NSLog(@"i = %d, current value = %d", i, value);
// block内部调用block,形成了嵌套-递归
testMethod(value - 1);
}
//[lock unlock];
//[recursiveLock unlock];
};
testMethod(10);
});
}

我虽然把锁都写出来了。但是一开始的情况下,我们不加锁运行一下,看是怎么打印的。

打印出来的就是一堆无序的数字。

然后我们使用NSLock,把1处的lock打开,底部unlock也需要打开,再执行一下。

诶~~ 怎么还发生崩溃了呢?就是因为内部发生了递归调用,这个锁已经玩不了了。
那我们该用递归锁试一下,我们使用NSRecursiveLock再运行一下,把1处对应的lock打开。

这次没问题了哈~ 为啥呢?

NSRecursiveLock

我们看一下NSRecursiveLock的源码是啥?

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
open class NSRecursiveLock: NSObject, NSLocking {
internal var mutex = _RecursiveMutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif

public override init() {
super.init()
var attrib = pthread_mutexattr_t()

withUnsafeMutablePointer(to: &attrib) { attrs in
pthread_mutexattr_init(attrs)
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
pthread_mutex_init(mutex, attrs)
}
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
}

deinit {
pthread_mutex_destroy(mutex)

mutex.deinitialize(count: 1)
mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
}

open func lock() {
pthread_mutex_lock(mutex)
}

open func unlock() {
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
}

open func `try`() -> Bool {
return pthread_mutex_trylock(mutex) == 0
}

open func lock(before limit: Date) -> Bool {
if pthread_mutex_trylock(mutex) == 0 {
return true
}

#if os(macOS) || os(iOS) || os(Windows)
return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#else
guard var endTime = timeSpecFrom(date: limit) else {
return false
}
return pthread_mutex_timedlock(mutex, &endTime) == 0
#endif
}

open var name: String?
}

嗯哼???怎么跟NSLock中的源码没有什么区别?在init()中有略微的不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public override init() {
super.init()
var attrib = pthread_mutexattr_t()

withUnsafeMutablePointer(to: &attrib) { attrs in
pthread_mutexattr_init(attrs)
// 设置互斥锁的类型 RECURSIVE(递归)
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
pthread_mutex_init(mutex, attrs)
}
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
}

这下就清晰了,NSRecursiveLock在内部是一个互斥锁设置了递归的类型,只有这一点点的区别,就导致上方的代码运行出现那么大的区别。

lock的位置

我们继续看上方的代码,把lock的位置放在2.的位置。分别打开NSLock和NSRecursiveLock看看有什么不同:

  1. 打开NSLock

    1
    2021-05-20 00:05:11.816871+0800 LockDemo[10150:520495] i = 0, current value = 10

    只有一条输出?

    因为NSLock只是一个互斥锁,执行了lock操作之后,就必须等待unlock才能继续执行,否则就一直等待。

  2. 打开NSRecursiveLock

    竟然发生了crash,但是current value的值是有序的,i的值却是无序的,这是个啥情况?

    也就是说,递归锁我用了,但是递归执行中,线程与线程直接,递归没有直接的联系,也就是递归锁在多线程中跑偏了~造成了锁内部发生了错乱,导致了crash。

所以啊,别总乱加锁。那么有人可能会说,我直接在调用的时候加锁不就可以了吗,还很省事,不用担心发生crash。

1
2
3
[lock lock];
testMethod(10);
[lock unlock];

肯定不行啊,这样只是控制了,递归调用不会出现问题,但是i的值呢?

不管你这这里使用NSLock或NSRecursiveLock都是一样的效果。所以,这种情况,我们经常会使用@synchronized (self),不用管什么时候加锁和什么时候解锁,也不用担心会发生等待、崩溃。而且性能上,并没有差多少。

1
2
3
4
5
6
7
8
testMethod = ^(int value) {
@synchronized (self) {
if (value > 0) {
NSLog(@"i = %d, current value = %d", i, value);
testMethod(value - 1);
}
}
}

NSCondition 条件变量

NSCondition的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@interface NSCondition : NSObject <NSLocking> {
// 让当前线程处于等待状态
- (void)wait;
// 在时间到达之前让当前线程一直等待
- (BOOL)waitUntilDate:(NSDate *)limit;
/**
* CPU发送信号,唤醒等待条件的一个线程,可以执行。可以多次调用,唤醒多个线程。
* 没有线程等待,则什么也不处理。只有在被锁的情况下才可以调用
*/
- (void)signal;
/**
* 唤醒等待的所有线程,如果没有等待,则什么也不做。只有在被锁的情况下才可以调用
*/
- (void)broadcast;

@property (nullable, copy) NSString *name;

@end

// 一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock外等待,只到unlock ,才可访问
- (void)lock;
// 与lock同时使用。
- (void)unlock;

接下来我们看代码:

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
- (void)conditionTest {
self.ticketCount = 0;
for (int i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self addTicket];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTicket];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTicket];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self addTicket];
});
}
}

- (void)addTicket {
self.ticketCount += 1;
NSLog(@"有票了 %ld", self.ticketCount);
}

- (void)saleTicket {
self.ticketCount -= 1;
NSLog(@"卖了一张 %ld", self.ticketCount);
}

运行一下代码。

都有可能卖超了。怎么解决这个问题,虽然上面的锁能解决,但是我们使用NSCondition来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)addTicket {
// 加锁
[self.condition lock];
self.ticketCount += 1;
NSLog(@"有票了 count = %ld", self.ticketCount);
// 通知,有票了,等待的线程可以执行了
[self.condition signal];
// 解锁
[self.condition unlock];
}

- (void)saleTicket {
// 加锁
[self.condition lock];
if (self.ticketCount == 0) {
NSLog(@"wait...没有票了 count = 0");
// 没有票,就等待,不执行,等到有票了再执行
[self.condition wait];
}
// 有票了,就执行了
self.ticketCount -= 1;
NSLog(@"卖了一张 count = %ld", self.ticketCount);
[self.condition unlock];
}

运行一下,看看结果

很是完美,NSCondition条件变量是很少使用的,因为用起来比较麻烦。其主要的点就是在什么时候执行wait、什么时候发送signal

看了代码的运用,我们看看内部是怎么实现的。

NSCondition 内部实现

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
open class NSCondition: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
// 条件
internal var cond = _ConditionVariablePointer.allocate(capacity: 1)

public override init() {
#if os(Windows)
InitializeSRWLock(mutex)
InitializeConditionVariable(cond)
#else
// 内部封装了pthread 的互斥锁
pthread_mutex_init(mutex, nil)
pthread_cond_init(cond, nil)
#endif
}

deinit {
#if os(Windows)
// SRWLock do not need to be explicitly destroyed
#else
pthread_mutex_destroy(mutex)
pthread_cond_destroy(cond)
#endif
mutex.deinitialize(count: 1)
cond.deinitialize(count: 1)
mutex.deallocate()
cond.deallocate()
}

open func lock() {
#if os(Windows)
AcquireSRWLockExclusive(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}

open func unlock() {
#if os(Windows)
ReleaseSRWLockExclusive(mutex)
#else
pthread_mutex_unlock(mutex)
#endif
}

// 等待
open func wait() {
#if os(Windows)
SleepConditionVariableSRW(cond, mutex, WinSDK.INFINITE, 0)
#else
pthread_cond_wait(cond, mutex)
#endif
}

open func wait(until limit: Date) -> Bool {
#if os(Windows)
return SleepConditionVariableSRW(cond, mutex, timeoutFrom(date: limit), 0)
#else
guard var timeout = timeSpecFrom(date: limit) else {
return false
}
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
#endif
}

// 发送信号
open func signal() {
#if os(Windows)
WakeConditionVariable(cond)
#else
pthread_cond_signal(cond)
#endif
}

// 唤醒所有条件
open func broadcast() {
#if os(Windows)
WakeAllConditionVariable(cond)
#else
// 根据window的释义,唤醒所有条件锁
pthread_cond_broadcast(cond)
#endif
}

open var name: String?
}

NSCondition内部封装的是一个pthread的互斥锁。

NSConditionLock 条件锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 初始化当前的条件
- (instancetype)initWithCondition:(NSInteger)condition;
// 只读,当前的条件
@property (readonly) NSInteger condition;

// 当前线程加锁,如果当前条件与参数一致,则执行,否则等待,直到有对应的条件被解锁。
- (void)lockWhenCondition:(NSInteger)condition;
// 解锁,然后把条件传尽来,如果有等待的线程,并且条件与condition相同则执行,没有不处理。
- (void)unlockWithCondition:(NSInteger)condition;

// 加锁,没有条件,可以的话直接执行
- (void)lock;
// 解锁
- (void)unlock;

接下来,看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)conditonLockTest {
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// [conditionLock lockWhenCondition:1];
NSLog(@"1");
// [conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
// [conditionLock lockWhenCondition:2];
NSLog(@"2");
// [conditionLock unlockWithCondition:1];
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
// [conditionLock lock];
NSLog(@"3");
// [conditionLock unlock];
});
}

我们先不使用NSConditionLock,跑一下代码看看,输出情况:

根据之前GCD全局队列的内容,该输出可能很随意,每次输出都可能不一样。那接下来把注释的代码打开,在有锁的情况下,再跑一次。

这次的输出结果为:3-2-1,也有可能是2-3-1。
因为初始的NSConditionLock的条件是2,而且是全局并发队列,所以2和3都有可能先执行,但是1肯定是最后执行。因为在2执行完毕之后,才解锁,并把条件该成了1。这个时候才通知条件为1的锁执行。

条件锁有点类似信号量。

NSConditionLock 源码:

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
open class NSConditionLock : NSObject, NSLocking {
// 内部封装了一个NSCondition
internal var _cond = NSCondition()
// int类型的数据,当做锁的条件
internal var _value: Int
internal var _thread: _swift_CFThreadRef?

// 默认init操作,condition == 0
public convenience override init() {
self.init(condition: 0)
}

public init(condition: Int) {
_value = condition
}

// 加锁
open func lock() {
let _ = lock(before: Date.distantFuture)
}

// 解锁,直接_thread=nil,并且唤醒其他线程
open func unlock() {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_cond.broadcast()
_cond.unlock()
}

// 可读的condition
open var condition: Int {
return _value
}

// 加锁,并且当条件满足时执行,
open func lock(whenCondition condition: Int) {
// 这里详细说明
let _ = lock(whenCondition: condition, before: Date.distantFuture)
}

open func `try`() -> Bool {
return lock(before: Date.distantPast)
}

open func tryLock(whenCondition condition: Int) -> Bool {
return lock(whenCondition: condition, before: Date.distantPast)
}

// 解锁,并且设置条件
open func unlock(withCondition condition: Int) {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
// 设置条件锁的条件,并且执行等待的条件锁
_value = condition
_cond.broadcast()
_cond.unlock()
}

open func lock(before limit: Date) -> Bool {
_cond.lock()
while _thread != nil {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
#if os(Windows)
_thread = GetCurrentThread()
#else
_thread = pthread_self()
#endif
_cond.unlock()
return true
}

//
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
// NSCondition 加锁
_cond.lock()
// 这里是一个while循环,当条件不满足时,就一直循环下去,除非不是等待状态。
while _thread != nil || _value != condition {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
// 当条件满足,直接跳出while循环,向下执行。
#if os(Windows)
_thread = GetCurrentThread()
#else
_thread = pthread_self()
#endif
_cond.unlock()
return true
}

open var name: String?
}

NSConditionLock内部封装了NSCondtion,使用起来比NSCondtion更加方便。这个也是一个互斥锁。

总结

  • @synchronized(obj)
    • objc_sync_enter加锁、objc_sync_exit解锁
    • 查找obj的流程:当前链表查找 - 线程池中查找 - hash表中查找
    • 不要乱用obj
  • NSLock - pthread的互斥锁
    • 在递归调用中会发生等待
  • NSRecursiveLock - 递归锁,封装的也是互斥锁,内部原理与NSLock一致,只是设置了递归的类型。
    • 注意递归锁,在多线程中会发生crash
  • NSCondtion - 互斥锁
    • 注意wait和signal调用时机
  • NSConditionLock - 互斥锁
    • 内部封装了NSCondtion

锁的分类

自旋锁

线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。

自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。在iOS中,声明属性,默认修饰符atomic,原子性操作自带一把自旋锁。

  • OSSpinLock (已经不安全,不在使用)
  • atomic

互斥锁

是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成

这里属于互斥锁的有:

  • NSLock
  • NSRecursiveLock
  • @synchronized
  • pthread_mutex
  • NSCondition
  • NSCondtionLock

自旋锁与互斥锁的区别

  1. 原理:
    1. 自旋锁:线程一直处于 加锁 - 解锁 - 忙等,消耗CPU资源较高。
    2. 互斥锁:线程处于 加锁 - 解锁 - 休眠(等待被唤醒)
  2. 如果共享数据已有其他线程加锁:
    • 自旋锁:死循环的方式等待,一旦被访问的资源被解锁,则立即执行。
    • 互斥锁:线程会进入休眠状态,等待解锁。
  3. 使用自旋锁应及时释放自旋锁,否则等待中的自旋锁会浪费CPU资源。

条件锁

就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

  • NSCondition
  • NSConditionLock

递归锁

就是同一个线程可以加锁N次而不会引发死锁。递归锁是特殊的互斥锁,一种带有递归性质的互斥锁

  • NSRecursiveLock
  • pthread_mutex(recursive)

信号量(semaphore)

是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。它是一个互斥锁。

  • dispatch_semaphore

总结:

基本的锁包括两大类:自旋锁和互斥锁,其它锁都是在这两种的封装。

@synchronized 底层原理

我们把如下代码放在main.m文件中执行。

1
2
3
@synchronized (appDelegateClassName) {
// 进行读写操作
}

我们通过xcrun的命令生成main.cpp文件之后,才看文件可以到@synchronized的内部逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id _sync_obj = (id)appDelegateClassName;
objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {
objc_sync_exit(sync_exit);
}
id sync_exit;
}
_sync_exit(_sync_obj);

} catch (id e) {
_rethrow = e;
}

这里有两个操作,看着像是我们要找的重点内容objc_sync_enterobjc_sync_exit

为了以防万一,我们再汇编模式下debug一下代码。
在汇编模式下,同样发现了这两处代码。

1
2
3
libobjc.A.dylib`objc_sync_enter:

libobjc.A.dylib`objc_sync_exit:

并且这两处代码在libobjc.A.dylib中,这是个啥,感觉跟runtime的源码有点关系。是的,就是在runtime的源码中。接下来分析一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
// 1. obj有值
if (obj) {
// 2. 生成SyncData类型的data,这是重点,注意参数ACQUIRE
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
// 3. 加互斥锁
data->mutex.lock();
} else {
// 4.
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}

return result;
}

我们一步步分析源码:

  1. obj就是@synchronized (self)传进来的参数。有值的情况下会执行 2. 没有值则执行 4.
  2. SyncData是一个结构体
  3. 加锁
  4. 执行objc_sync_nil();直接返回

我们先看一下objc_sync_exit的内部结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
// 1. 判断是否有obj
if (obj) {
// 获取data,注意传的值的参数是RELEASE
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
// 有值的情况下,就进行解锁
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}
return result;
}

我们发现objc_sync_exitobjc_sync_enter的内部大同小异。主要的内容都是在id2data函数内部。

接下来,我们重点看一下这个函数:

这个函数太大了,先把一些代码逻辑隐藏掉了,接着一步一步分析:

SyncData 链表

1
2
3
4
5
6
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData; // 指向下一个值,
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
  1. nextData指向下一个值,类似与链表结构。
  2. object:hash map的关联对象,就是@synchronized (self)中self这个参数。
  3. threadCount:有多少个线程执行了这个block
  4. mutex:这是一个递归锁,递归锁是互斥锁的一种

SyncList

1
2
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);

首先出现的就是spinlock_t类型的锁,这是一个自旋锁,通过LOCK_FOR_OBJ(object)来获取。
SyncData指针类型的数据,通过LIST_FOR_OBJ获取。都是通过SyncList获取的

1
2
3
4
5
6
7
8
9
10
11
struct SyncList {
SyncData *data;
spinlock_t lock;

constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

SyncList是一个结构体类型,内部有一个链表,spinlock_t是一个自旋锁。它存放的是一个总表。

tls:是一个线程缓存的表,通过set和get方法获取对应key的值。

  • SYNC_DATA_DIRECT_KEY: 数据data对应的key
  • SYNC_COUNT_DIRECT_KEY:锁的个数对应的key
1
2
3
4
5
6
7
8
9
10
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;

typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;

SyncCache也是一个结构体,主要作用是存储线程,内部有一个list数组,存储不同的线程。list[0]存放的是当前线程的SyncData链表。

第一步 有data

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
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
// 1. 通过get方法,从tls缓存表中获取data
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
// 快速缓存查找 为YES
fastCacheOccupied = YES;
// 如果缓存中获取的data->object等于@synchronized(object),说明之前有使用过object
if (data->object == object) {
// Found a match in fast cache.
// 有几个锁,也就是有几次执行synchronized
uintptr_t lockCount;

result = data;
// 在快速缓存中
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
// 表中中data,但是对应的线程数和锁的个数都小于0,说明获取的data有问题
if (result->threadCount <= 0 || lockCount <= 0) {
_objc_fatal("id2data fastcache is buggy");
}
// 还记得上层方法enter、exit调用这个方法传的参数吗?
switch(why) {
case ACQUIRE: {
// enter -- 锁的个数+1
lockCount++;
// 然后通过set方法存的tls表中
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE:
// exit -- 锁的个数-1
lockCount--;
// 存放到tls表中
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
if (lockCount == 0) {
// 锁的个数等于 0 ,直接从表中移除
// remove from fast cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}

return result;
}
}

第二步 cache

SyncCache还有印象吗?内部list[0]存放的是当前的SyncData

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
// Check per-thread cache of already-owned locks for matching object
// 从线程的缓存池里捞数据,先看看这个函数,
SyncCache *cache = fetch_cache(NO);
if (cache) {
unsigned int i;
for (i = 0; i < cache->used; i++) {
// list存放的是SyncCacheItem的数组,判断每一个item中对应的object是不是我们使用@synchronized传的参数。
SyncCacheItem *item = &cache->list[i];
if (item->data->object != object) continue;

// Found a match. 恰好在线程的缓存池中找到了
result = item->data;
// 判断错误
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
// 这是直接在线程池中的数据做的操作
switch(why) {
case ACQUIRE:
item->lockCount++;
break;
case RELEASE:
item->lockCount--;
if (item->lockCount == 0) {
// remove from per-thread cache
// 直接从线程池中移除
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
// 找到了就返回
return result;
}
}

fetch_cache 函数

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
static SyncCache *fetch_cache(bool create)
{
_objc_pthread_data *data;

// 这个函数有解释,而且一目了然,没有从线程缓存池中拿到数据,并且参数create==NO,然后null,如果参数是YES,则创建一个。所以这里没有找到的话返回的是null,这个函数的主要作用就是获取cache
data = _objc_fetch_pthread_data(create);
if (!data) return NULL;

if (!data->syncCache) {
if (!create) {
return NULL;
} else {
int count = 4;
data->syncCache = (SyncCache *)
calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem));
data->syncCache->allocated = count;
}
}

// Make sure there's at least one open slot in the list.
if (data->syncCache->allocated == data->syncCache->used) {
data->syncCache->allocated *= 2;
data->syncCache = (SyncCache *)
realloc(data->syncCache, sizeof(SyncCache)
+ data->syncCache->allocated * sizeof(SyncCacheItem));
}

return data->syncCache;
}

我们先看一下_objc_fetch_pthread_data,这个方法已经说的很清楚了,有注释,代码都不用看。

其主要目的是从tls表中获取通过key获取_objc_pthread_data(结构体),内部有一个SyncCache,我们的最终目的也就是通过这个SyncCache中的list获取我们@synchronized(object)中object对应的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/***********************************************************************
* _objc_fetch_pthread_data
* Fetch objc's pthread data for this thread.
* If the data doesn't exist yet and create is NO, return NULL.
* If the data doesn't exist yet and create is YES, allocate and return it.
**********************************************************************/
_objc_pthread_data *_objc_fetch_pthread_data(bool create)
{
_objc_pthread_data *data;

data = (_objc_pthread_data *)tls_get(_objc_pthread_key);
if (!data && create) {
data = (_objc_pthread_data *)
calloc(1, sizeof(_objc_pthread_data));
tls_set(_objc_pthread_key, data);
}

return data;
}

第三步

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
// 首先执行的就上锁,这是一个自旋锁
lockp->lock();

{
SyncData* p;
SyncData* firstUnused = NULL;
// 执行for循环,这里的目的就是一直在遍历SyncData这个链表,看其中是否有数据
for (p = *listp; p != NULL; p = p->nextData) {
// 有数据,正好是我们用的那个。直接执行done语句
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
// 没有发现,则直接赋值
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}

// no SyncData currently associated with object
if ( (why == RELEASE) || (why == CHECK) )
goto done;

// an unused one was found, use it
if ( firstUnused != NULL ) {
// for循环中捞到的值赋给result
result = firstUnused;
// object指向@synchronized的参数
result->object = (objc_object *)object;
// 线程数是1
result->threadCount = 1;
goto done;
}
}

// 上面的流程也没有得到数据,哪就自己创建一个,并且加到线程池中。
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;

我们接下来看done的操作。

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
done:
// 自旋锁解锁, 这个锁只在第三步有使用。因为这是一个耗时操作
lockp->unlock();
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// 啥都没有呢,就释放,返回nil
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
// 错误判断
if (why != ACQUIRE) _objc_fatal("id2data is buggy");
if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
// 走了第一步就会变成YES。
if (!fastCacheOccupied) {
// Save in fast thread cache
// 把SyncData、lockCount=1锁的个数存到tls表中,快速查找的表
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
{
// Save in thread cache
// 存到线程池的表中
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
// 返回结果。
return result;
}

图上,左边就是线程list,每一个线程有一个链表。 先从当前的链表中查找,如果找到直接处理。没有找到则从线程中找到对应的链表再处理。

所以性能是最差的,内部有两个锁,一个自旋锁,一个递归锁,再加上快速查找,线程池缓存查找,等一系列操作,十分消耗性能,但是还是使用非常广泛,就是因为使用简单,不用自己加锁、解锁。

再看另外一个例子:

1
2
3
4
5
for (int i = 0; i < 100000; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.mArray = [NSMutableArray array];
});
}

运行上面的代码,是否会发生crash?

会发生crash,就是因为点属性是一个set方法,在set方法内部,会对旧值relase,新值retain,在某一个节点,就有可能release的次数过多,造成重复释放,发生了野指针,就发生了crash。

最简单的做法就是使用@synchronized(self),那能不能锁@synchronized(_mArray)呢?

1
2
3
@synchronized (self) {
self.mArray = [NSMutableArray array];
}

答案是不能,因为在运行的过程中,_mArray会有等于nil的情况,那还能锁住啥?

@synchronized (self) 锁的对象一定要有声明周期,是可以释放的,并且block中执行的变量最好与锁住的对象有关系,两人存在声明周期上的关联。

但是也不要乱用self,当在不同线程执行不同的block时,@synchronized的查找会更加耗时,因为链表的查找需要从头开始找。这个时候就需要使用其他的锁来处理了。

总结

  • 锁的分类

    1. 自旋锁 :atomic
    2. 互斥锁 :@synchronized,NSLock,pthread
    3. 条件锁 :NSCondition,NSConditionLock
    4. 递归锁 :NSRecursiveLock
    5. 信号量 :dispatch_semaphore
    6. 主要分为两大类:自旋锁、互斥锁
  • @synchronized底层原理

    1. 通过objc_sync_enter加锁
    2. 通过objc_sync_exit解锁
    3. 内部有一个自旋锁、一个递归锁。自旋锁用于hash表中的查找,查找线程池。递归锁用于锁住的对象的操作。
    4. 查找分为3个步奏:
      1. 通过当前链表查找,找到了直接返回
      2. 当前链表没有则通过线程池缓存查找,
      3. 在hash表中查找
      4. 锁住的对象最好与block执行的变量有关联关系,不要乱用self

引用

objc源码

信号量

先看代码:

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
- (void)semaphore {
// 全局队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 初始化一个信号量可以并发执行2个任务
dispatch_semaphore_t sem = dispatch_semaphore_create(2);

//任务1
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"1 start..");
sleep(1);
NSLog(@"1 end...");
dispatch_semaphore_signal(sem);
});

//任务2
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"2 start..");
sleep(1);
NSLog(@"2 end...");
dispatch_semaphore_signal(sem);
});

//任务3
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"3 start..");
sleep(1);
NSLog(@"3 end...");
dispatch_semaphore_signal(sem);
});
}

我们先运行一下这个demo,看输出的结果是什么样子。

1
2
3
4
5
6
2021-05-16 17:38:16.018150+0800 GCDDemo[972:7272767] 1 start..
2021-05-16 17:38:16.018154+0800 GCDDemo[972:7272769] 2 start..
2021-05-16 17:38:17.027319+0800 GCDDemo[972:7272767] 1 end...
2021-05-16 17:38:17.027332+0800 GCDDemo[972:7272769] 2 end...
2021-05-16 17:38:17.027556+0800 GCDDemo[972:7272765] 3 start..
2021-05-16 17:38:18.031174+0800 GCDDemo[972:7272765] 3 end...

我们也需要看打印的时间点,先执行1、2 start,1s之后执行了1、2 end,紧接着执行3。

如果把信号量的初始值改成1呢?

1
2
3
4
5
6
2021-05-16 18:07:48.061949+0800 GCDDemo[1220:7292903] 1 start..
2021-05-16 18:07:49.063438+0800 GCDDemo[1220:7292903] 1 end...
2021-05-16 18:07:49.063639+0800 GCDDemo[1220:7292901] 2 start..
2021-05-16 18:07:50.063905+0800 GCDDemo[1220:7292901] 2 end...
2021-05-16 18:07:50.064202+0800 GCDDemo[1220:7292906] 3 start..
2021-05-16 18:07:51.064940+0800 GCDDemo[1220:7292906] 3 end...

结果就是先执行1,然后2,3每次间隔都是1s。

为什么能确定并发执行的个数呢?就是因为其中有两个语句,确定了最大的并发数。

1
2
3
4
// 执行-1操作
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
// 执行+1操作
dispatch_semaphore_signal(sem);

分别看一下内部实现。

dispatch_semaphore_create

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dispatch_semaphore_t
dispatch_semaphore_create(intptr_t value)
{
dispatch_semaphore_t dsema;
if (value < 0) {
return DISPATCH_BAD_INPUT;
}
// 初始化信号量结构体
dsema = _dispatch_object_alloc(DISPATCH_VTABLE(semaphore),
sizeof(struct dispatch_semaphore_s));
dsema->do_next = DISPATCH_OBJECT_LISTLESS;
dsema->do_targetq = _dispatch_get_default_queue(false);
// 用来保存初始化的最大并发数
dsema->dsema_value = value;
_dispatch_sema4_init(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
dsema->dsema_orig = value;
return dsema;
}

看到了吧,最开始就已经有判断了,如果创建时的参数小于0,直接返回一个DISPATCH_BAD_INPUT

1
#define DISPATCH_BAD_INPUT		((void *_Nonnull)0)

知道时啥了吗?就是一个野指针。

那我们接下来看dispatch_semaphore_wait函数。

dispatch_semaphore_wait

1
2
3
4
5
6
7
8
9
intptr_t
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
{
long value = os_atomic_dec2o(dsema, dsema_value, acquire);
if (likely(value >= 0)) {
return 0;
}
return _dispatch_semaphore_wait_slow(dsema, timeout);
}

这里调用了os_atomic_dec2o,但是有一个dec,猜测应该是减法操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
#define os_atomic_dec2o(p, f, m) \
os_atomic_sub2o(p, f, 1, m)

#define os_atomic_sub2o(p, f, v, m) \
os_atomic_sub(&(p)->f, (v), m)

#define os_atomic_sub(p, v, m) \
_os_atomic_c11_op((p), (v), m, sub, -)

#define _os_atomic_c11_op(p, v, m, o, op) \
({ _os_atomic_basetypeof(p) _v = (v), _r = \
atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), _v, \
memory_order_##m); (__typeof__(_r))(_r op _v); })

一层一层的宏定义,最后可以得出的函数是atomic_fetch_sub_explicit
这是一个C语言的原子类型减的函数。也就是dsema->dsema_value - 1

大于等于0都可以正常执行,否则发生等待。

dispatch_semaphore_signal

1
2
3
4
5
6
7
8
9
10
11
12
13
intptr_t
dispatch_semaphore_signal(dispatch_semaphore_t dsema)
{
long value = os_atomic_inc2o(dsema, dsema_value, release);
if (likely(value > 0)) {
return 0;
}
if (unlikely(value == LONG_MIN)) {
DISPATCH_CLIENT_CRASH(value,
"Unbalanced call to dispatch_semaphore_signal()");
}
return _dispatch_semaphore_signal_slow(dsema);
}

dispatch_semaphore_signaldispatch_semaphore_wait正好相反,执行的是加法操作,每次加1。

这里的判断条件是value > 0,都可以正常执行,也就是必须有1个才行,否则阻塞线程,一直处于等待状态。

总结

dispatch_semaphore_waitdispatch_semaphore_signal肯定是成对出现的。

dispatch_group 调度组

当我们在业务中,需要两个网络请求都返回之后,才能处理某一个业务逻辑时,调度组就能很好的发挥作用,而且用起来很简单。

先看一下简单的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)groupDemo {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globle = dispatch_get_global_queue(0, 0);

// 1.
dispatch_group_enter(group);
dispatch_async(globle, ^{
NSLog(@"1 start ...");
sleep(3);
dispatch_group_leave(group);
});
// 2.
dispatch_group_enter(group);
dispatch_async(globle, ^{
NSLog(@"2 start ...");
sleep(3);
dispatch_group_leave(group);
});

// 3.
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"3 ...");
});
}

我们执行一下代码,发现3是最后执行的。

如果出现dispatch_group_enterdispatch_group_leave多的情况呢?
不会发生crash。

如果出现dispatch_group_leavedispatch_group_enter多的情况呢?
会发生crash。

这里需要注意的是:

  1. dispatch_group_enterdispatch_group_leave是成对出现的。
    不管执行的顺序如何,必须保证成对出现,否则不会触发dispatch_group_notify
  2. dispatch_group_notify一般出现在最下边,有dispatch_group_leave就会触发,可以把3的代码放在最上边试一下。
  3. dispatch_group_enterdispatch_group_leave可以使用dispatch_group_async来代替,减少代码量以及没有成对出现可能带来的问题。
  4. dispatch_group_wait设置超时时间,如果超时了任务还没有执行完成,则会直接触发dispatch_group_notify

这些代码可以试一下哈,我们接下来看内部实现原理。

dispatch_group_create

首先我们先看一下group的创建。

1
2
3
4
5
dispatch_group_t
dispatch_group_create(void)
{
return _dispatch_group_create_with_count(0);
}

调用了_dispatch_group_create_with_count,参数是一个0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline dispatch_group_t
_dispatch_group_create_with_count(uint32_t n)
{
// 1. 初始化dg,dispatch_group_s是一个结构体
dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group),
sizeof(struct dispatch_group_s));
// 赋默认值
dg->do_next = DISPATCH_OBJECT_LISTLESS;
dg->do_targetq = _dispatch_get_default_queue(false);
// n = 0,所以创建的时候永远不会执行这里。
if (n) {
os_atomic_store2o(dg, dg_bits,
(uint32_t)-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed);
os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411>
}
return dg;
}

创建group还是很好理解的,接下来看dispatch_group_enter

dispatch_group_enter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void
dispatch_group_enter(dispatch_group_t dg)
{
// The value is decremented on a 32bits wide atomic so that the carry
// for the 0 -> -1 transition is not propagated to the upper 32bits.
// dg->dg_bits的值从0 变成 -1
uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits,
DISPATCH_GROUP_VALUE_INTERVAL, acquire);
uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK;
// 等于0
if (unlikely(old_value == 0)) {
_dispatch_retain(dg); // <rdar://problem/22318411>
}
// 我们上面执行了多个enter之后,没有发生crash,但是这里也有解释
// 超过一个最大值时也会发生crash
if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) {
DISPATCH_CLIENT_CRASH(old_bits,
"Too many nested calls to dispatch_group_enter()");
}
}

os_atomic_sub_orig2o这个函数跟上面信号量的函数有点类似,并且已经给了注释:dg->dg_bits的值从0 变成 -1。

dispatch_group_leave

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
void
dispatch_group_leave(dispatch_group_t dg)
{
// The value is incremented on a 64bits wide atomic so that the carry for
// the -1 -> 0 transition increments the generation atomically.
// dg_state从-1 -> 0
uint64_t new_state, old_state = os_atomic_add_orig2o(dg, dg_state,
DISPATCH_GROUP_VALUE_INTERVAL, release);
uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK);

if (unlikely(old_value == DISPATCH_GROUP_VALUE_1)) {
old_state += DISPATCH_GROUP_VALUE_INTERVAL;
do {
new_state = old_state;
if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) {
new_state &= ~DISPATCH_GROUP_HAS_WAITERS;
new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
} else {
// If the group was entered again since the atomic_add above,
// we can't clear the waiters bit anymore as we don't know for
// which generation the waiters are for
new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
}
if (old_state == new_state) break;
} while (unlikely(!os_atomic_cmpxchgv2o(dg, dg_state,
old_state, new_state, &old_state, relaxed)));
return _dispatch_group_wake(dg, old_state, true);
}

// old_value
if (unlikely(old_value == 0)) {
DISPATCH_CLIENT_CRASH((uintptr_t)old_value,
"Unbalanced call to dispatch_group_leave()");
}
}

我们一步一步的分析:

1
2
uint64_t new_state, old_state = os_atomic_add_orig2o(dg, dg_state,
DISPATCH_GROUP_VALUE_INTERVAL, release);

这里有注释,写了解释内容,从-1变成了0,所以:
old_state = -1, new_state = 0 。

1
2
3
uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK);

#define DISPATCH_GROUP_VALUE_MASK 0x00000000fffffffcULL

old_value 是通过&运算得来的, -1 = 0xfffffffff,是一个全是1的二进制数,所以&运算之后old_value = DISPATCH_GROUP_VALUE_MASK。

如果再执行一次leave操作,那么old_state=0,然后经过&运算,old_value=0,也就是最后会发生crash。所以enter和leave一定要成对出现。

接下来就到重点了,当enter和leave达到平衡时,就会触发_dispatch_group_wake函数。

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
static void
_dispatch_group_wake(dispatch_group_t dg, uint64_t dg_state, bool needs_release)
{
uint16_t refs = needs_release ? 1 : 0; // <rdar://problem/22318411>
// &运算
if (dg_state & DISPATCH_GROUP_HAS_NOTIFS) {
dispatch_continuation_t dc, next_dc, tail;

// Snapshot before anything is notified/woken <rdar://problem/8554546>
dc = os_mpsc_capture_snapshot(os_mpsc(dg, dg_notify), &tail);
do {
dispatch_queue_t dsn_queue = (dispatch_queue_t)dc->dc_data;
next_dc = os_mpsc_pop_snapshot_head(dc, tail, do_next);
// 执行这个操作,在第一章中有介绍,执行dx_push操作。直到block执行完成
_dispatch_continuation_async(dsn_queue, dc,
_dispatch_qos_from_pp(dc->dc_priority), dc->dc_flags);
_dispatch_release(dsn_queue);
} while ((dc = next_dc));

refs++;
}

if (dg_state & DISPATCH_GROUP_HAS_WAITERS) {
_dispatch_wake_by_address(&dg->dg_gen);
}
// 释放
if (refs) _dispatch_release_n(dg, refs);
}

dispatch_group_notify

接下来我们看一下dispatch_group_notify操作。

1
2
3
4
5
6
7
8
void
dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_block_t db)
{
dispatch_continuation_t dsn = _dispatch_continuation_alloc();
_dispatch_continuation_init(dsn, dq, db, 0, DC_FLAG_CONSUME);
_dispatch_group_notify(dg, dq, dsn);
}

_dispatch_continuation_init 内部是对属性赋值,保存dg(group)、dq(queue)、db(block)。在第一章有介绍。

_dispatch_group_notify的函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static inline void
_dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_continuation_t dsn)
{
uint64_t old_state, new_state;
dispatch_continuation_t prev;

dsn->dc_data = dq;
_dispatch_retain(dq);

prev = os_mpsc_push_update_tail(os_mpsc(dg, dg_notify), dsn, do_next);
if (os_mpsc_push_was_empty(prev)) _dispatch_retain(dg);
os_mpsc_push_update_prev(os_mpsc(dg, dg_notify), prev, dsn, do_next);
if (os_mpsc_push_was_empty(prev)) {
os_atomic_rmw_loop2o(dg, dg_state, old_state, new_state, release, {
new_state = old_state | DISPATCH_GROUP_HAS_NOTIFS;
// 重点哈~
if ((uint32_t)old_state == 0) {
os_atomic_rmw_loop_give_up({
return _dispatch_group_wake(dg, new_state, false);
});
}
});
}
}

这里会有一系列的判断,当old_value=0的时候,执行wake操作。

dispatch_group_async

1
2
3
4
5
6
7
8
9
10
11
12
void
dispatch_group_async(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_block_t db)
{
dispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC;
dispatch_qos_t qos;
// 任务包装器,存储block等信息
qos = _dispatch_continuation_init(dc, dq, db, 0, dc_flags);
// 重点 _dispatch_continuation_group_async(dg, dq, dc, qos);
}
#endif

这里的重点代码是_dispatch_continuation_group_async.

1
2
3
4
5
6
7
8
static inline void
_dispatch_continuation_group_async(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_continuation_t dc, dispatch_qos_t qos)
{
dispatch_group_enter(dg);
dc->dc_data = dg;
_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}

看到了吧,其内部有自动执行dispatch_group_enter操作。但是什么时候执行的leave呢?还记得上一章中介绍_dispatch_continuation_invoke_inline的时候吗?里头有一局代码是关于group的操作,我还专门写了注释。

_dispatch_continuation_async是执行dx_push操作。

_dispatch_continuation_invoke_inline在group的情况会值执行group相关的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void
_dispatch_continuation_with_group_invoke(dispatch_continuation_t dc)
{
struct dispatch_object_s *dou = dc->dc_data;
unsigned long type = dx_type(dou);
if (type == DISPATCH_GROUP_TYPE) {
_dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
_dispatch_trace_item_complete(dc);
dispatch_group_leave((dispatch_group_t)dou);
} else {
DISPATCH_INTERNAL_CRASH(dx_type(dou), "Unexpected object type");
}
}

嗯哼~这里也会执行dispatch_group_leave操作,这也就是dispatch_group_async可以代替enter和leave的原因。
下面的代码可以很好的解释dispatch_group_async的原理

1
2
3
4
5
6
7
8
9
10
dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block)
{
dispatch_retain(group);
dispatch_group_enter(group);
dispatch_async(queue, ^{
block();
dispatch_group_leave(group);
dispatch_release(group);
});
}

dispatch_source

dispatch_source是一个更为底层,直接与内核交互的东西,所以它执行起来会更快,效率更高。所以type类型是time的时候,准确率是最高的。

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
NSUInteger totalComplete = 0
// 创建DISPATCH_SOURCE_TYPE_DATA_ADD类型的source,再主线程执行
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());

// 设置block回调
dispatch_source_set_event_handler(self.source, ^{
// 获取数据
NSUInteger value = dispatch_source_get_data(self.source);
totalComplete += value;
NSLog(@"进度: %.2f", totalComplete/100.0);
});

// 开始执行
dispatch_resume(source);

// 异步执行for
for (int i= 0; i<100; i++) {
dispatch_async(self.queue, ^{
sleep(1);
// merge数据,每次+1,每次merge就会触发dispatch_source_set_event_handler
dispatch_source_merge_data(self.source, 1);
});
}


dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(100 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 停止
dispatch_suspend(_source);
});

上面内容就是source相关的。不是很常用,了解一下。

总结

  1. dispatch_semaphore信号量
    1. dispatch_semaphore_signaldispatch_semaphore_wait成对出现。
    2. dispatch_semaphore_signal是+1操作
    3. dispatch_semaphore_wait是-1操作
  2. dispatch_group调度组
    1. enter和leave是成对出现的,否则可能发生crash
    2. dispatch_group_notify一般放在最下边执行。
    3. dispatch_group_async可以替代enter和leave两个操作。
    4. dispatch_group_wait超时操作。
  3. source,每次执行merge操作就会执行block。

以上就是GCD的相关内容了,写了3章,有不对的地方欢迎指正。

对称加密

明文通过密钥加密得到密文。密文通过密钥解密得到明文。

这类算法再加密和解密时使用相同的密钥,或是使用两个可以简单的互相推算的密钥。与公开密钥加密相比,要求双方获取相同的密钥是对称加密的主要缺点之一。

DES - 数据加密标准

数据加密标准(Data Encryption Standard),简称DES。是一种对称密钥加密块密码算法。

DES是一种典型的块密码。一种将明文分成若干个固定长度的小块,再通过一系列的加密生成同样长度的密文的算法。对DES而言,块长度为64位(8个字节)。

强度不够,现在使用较少。

3DES - 三重数据加密算法

英文名称:Triple Data Encryption Algorithm,缩写为TDEA。也被称为3DES(Triple DES)。是一种对称密钥加密块密码,相当于是对每个数据块应用三次DES算法。

就是因为DES强度过低容易被暴利破解,3DES则是在DES的基础上演变过来的。采用3个密钥,即通过增加DES密钥长度来避免被暴利破解。

AES - 高级加密标准

AES(Advanced Encryption Standard)是用来代替DES的高级加密算法。AES的区块长度固定为128比特,密钥长度则可以是128,192或256比特。

分组密码

分组密码的数学模型是将明文消息编码表示后的数字(简称明文数字)序列,划分成长度为n的组(可看成长度为n的矢量),每组分别在密钥的控制下变换成等长的密文序列。

ECB模式

ECB(Electronic Codebook,电码本)模式是分组密码的一种最基本的工作模式。在该模式下,待处理信息被分为大小合适的分组,然后分别对每一分组独立进行加密或解密处理。

ECB的特点

  1. 是分组密码最基本的工作模式,操作简单,易于实现。
  2. 所有分组的加密方式一致,明文中重复内容会在密文中体现。不能很好的隐藏数据。

CBC模式

CBC指的是密码分组链接。

在CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有明文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量。

CBC的特点

  1. CBC是最常用的工作模式
  2. CBC加密依赖分组的上下文,加密过程是串行的,无法并行化。
  3. 明文中微小的变动,会影响后续所有的密文块多改变。
  4. 其中一个分组丢失(错误),后续所有的都将作废

CBC可以有效的保证密文的完整性,如果一个分组数据块丢失或改变,后续的数据都将无法正常解密。

IV 初始化向量

初始化向量(IV,Initialization Vector)是许多任务作模式中用于将加密随机化的一个位块,由此即使同样的明文被多次加密也会产生不同的密文,避免了较慢的重新产生密钥的过程。

在CBC模式下,同一密钥的情况下重用IV会导致泄露明文首个块的信息。

终端命令

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
/**
* 终端测试指令
*
* DES(ECB)加密,key是必须的,
* $ echo -n hello | openssl enc -des-ecb -K 'abc' -nosalt | base64
*
* DES(ECB)解密
* $ echo -n P8TeEbkBDds= | base64 -D | openssl enc -des-ecb -K 'abc' -nosalt -d
* DES(CBC)加密
* $ echo -n hello | openssl enc -des-cbc -iv 12345678 -K 'abc' -nosalt | base64
*
* DES(CBC)解密
* $ echo -n fGkwzGbNomY= | base64 -D | openssl enc -des-cbc -iv 12345678 -K 'abc' -nosalt -d
*
* ----------------------
*
* AES(ECB)加密 密钥长度128,192或256比特
* $ echo -n hello | openssl enc -aes-128-ecb -K 'abc' -nosalt | base64
*
* AES(ECB)解密
* $ echo -n p+nWPDBu7iGX0ZXgw3jAXw== | base64 -D | openssl enc -aes-128-ecb -K 'abc' -nosalt -d
*
* AES(CBC)加密
* $ echo -n hello | openssl enc -aes-128-cbc -iv 12345678 -K 'abc' -nosalt | base64
*
* AES(CBC)解密
* $ echo -n jK4VTW/k7P8PsmW16ztlCw== | base64 -D | openssl enc -aes-128-cbc -iv 12345678 -K 'abc' -nosalt -d
*
*/

加密过程是先加密,然后再base64编码。
解密过程是先base64解码。然后再解密。

引用

DES-数据加密标准
3DES
AES - 高级加密标准
分组密码工作模式-ECB、CBC等
ECB模式-百度百科