朝暮的闲暇时刻


  • 首页

  • 归档

玩玩Clang

发表于 2018-06-07 | 分类于 iOS

0x01 配置

  Clang是LLVM的编译前端,我们的OC、Swift通过Clang经过预处理、词语分析、语法分析,最后生成IR中间码后,交由LLVM进行优化,最后针对不同的平台将IR转换成对应平台的汇编代码。

  本篇需要对Clang进行一些定制化,所以我们需要重新编译llvm,步骤如下:

  • 下载源码

    • cd 需要下载源码的目录

      git clone http://llvm.org/git/llvm.git

    • cd llvm/tools

      git clone http://llvm.org/git/clang.git

    • cd ../projects

      git clone http://llvm.org/git/compiler-rt.git

    • cd ../tools/clang/tools

      git clone http://llvm.org/git/clang-tools-extra.git

  • 编译

    • 安装cmake

      brew install cmake

    • 编译

      • mkdir 新建的目录名 (因为不支持源代码目录内编译)
      • cd 新建的目录名
      • cmake llvm源代码目录
      • cmake –build .

  参考链接:http://llvm.org/docs/CMake.html,编译完所有的工具都在编译目录下的bin目录下。

0x02 试玩

  我们有代码如下

1
2
3
4
5
6
7
8
9
10
11
12
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "Animal.h"

#define NUM_1 100
#define NUM_2 200


int main(int argc, char * argv[]) {
int a = NUM_1 + NUM_2;
return 0;
}

预处理

  经过预处理后的代码会是什么样的?使用Clang命令如下

clang -E main.m

1
2
3
4
5
6
7
8
9
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 185 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 10 "./Animal.h" 2

int main(int argc, char * argv[]) {
int a = 100 + 200;

return 0;
}

  预处理的过程中,导入了头文件,并把宏替换了。

词法分析

  词法分析的命令

clang -fsyntax-only -Xclang -dump-tokens 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
int 'int'	 [StartOfLine]	Loc=<main.m:17:1>
identifier 'main' [LeadingSpace] Loc=<main.m:17:5>
l_paren '(' Loc=<main.m:17:9>
int 'int' Loc=<main.m:17:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:17:14>
comma ',' Loc=<main.m:17:18>
char 'char' [LeadingSpace] Loc=<main.m:17:20>
star '*' [LeadingSpace] Loc=<main.m:17:25>
identifier 'argv' [LeadingSpace] Loc=<main.m:17:27>
l_square '[' Loc=<main.m:17:31>
r_square ']' Loc=<main.m:17:32>
r_paren ')' Loc=<main.m:17:33>
l_brace '{' [LeadingSpace] Loc=<main.m:17:35>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:18:5>
identifier 'a' [LeadingSpace] Loc=<main.m:18:9>
equal '=' [LeadingSpace] Loc=<main.m:18:11>
numeric_constant '100' [LeadingSpace] Loc=<main.m:18:13 <Spelling=main.m:13:16>>
plus '+' [LeadingSpace] Loc=<main.m:18:19>
numeric_constant '200' [LeadingSpace] Loc=<main.m:18:21 <Spelling=main.m:14:16>>
semi ';' Loc=<main.m:18:26>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:19:5>
identifier 'b' [LeadingSpace] Loc=<main.m:19:9>
equal '=' [LeadingSpace] Loc=<main.m:19:11>
numeric_constant '1' [LeadingSpace] Loc=<main.m:19:13>
plus '+' [LeadingSpace] Loc=<main.m:19:15>
numeric_constant '2' [LeadingSpace] Loc=<main.m:19:17>
semi ';' Loc=<main.m:19:18>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:21:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:21:12>
semi ';' Loc=<main.m:21:13>
r_brace '}' [StartOfLine] Loc=<main.m:25:1>
eof '' Loc=<main.m:25:2>

语法分析

  语法分析命令如下

clang -fsyntax-only -Xclang -ast-dump main.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-FunctionDecl 0x7ff34f2eeae8 <main.m:17:1, line:25:1> line:17:5 main 'int (int, char **)'
|-ParmVarDecl 0x7ff34f2ee918 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7ff34f2ee9d0 <col:20, col:32> col:27 argv 'char **':'char **'
`-CompoundStmt 0x7ff34f2eedb8 <col:35, line:25:1>
|-DeclStmt 0x7ff34f2eec70 <line:18:5, col:26>
| `-VarDecl 0x7ff34f2eeba8 <col:5, line:14:16> line:18:9 a 'int' cinit
| `-BinaryOperator 0x7ff34f2eec48 <line:13:16, line:14:16> 'int' '+'
| |-IntegerLiteral 0x7ff34f2eec08 <line:13:16> 'int' 100
| `-IntegerLiteral 0x7ff34f2eec28 <line:14:16> 'int' 200
|-DeclStmt 0x7ff34f2eed68 <line:19:5, col:18>
| `-VarDecl 0x7ff34f2eeca0 <col:5, col:17> col:9 b 'int' cinit
| `-BinaryOperator 0x7ff34f2eed40 <col:13, col:17> 'int' '+'
| |-IntegerLiteral 0x7ff34f2eed00 <col:13> 'int' 1
| `-IntegerLiteral 0x7ff34f2eed20 <col:17> 'int' 2
`-ReturnStmt 0x7ff34f2eeda0 <line:21:5, col:12>
`-IntegerLiteral 0x7ff34f2eed80 <col:12> 'int' 0

生成IR

  IR是作为Clang的输出,llvm的输入,命令如下

clang -S -fobjc-arc -emit-llvm main.m -o main.ll

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
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.13.0"

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main(i32, i8**) #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 300, i32* %6, align 4
store i32 3, i32* %7, align 4
ret i32 0
}

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6}
!llvm.ident = !{!7}

!0 = !{i32 1, !"Objective-C Version", i32 2}
!1 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!3 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!4 = !{i32 1, !"Objective-C Class Properties", i32 64}
!5 = !{i32 1, !"wchar_size", i32 4}
!6 = !{i32 7, !"PIC Level", i32 2}
!7 = !{!"Apple LLVM version 9.1.0 (clang-902.0.39.2)"}

其他

  • 生成字节码

    clang -emit-llvm -c main.m -o main.bc

  • 生成汇编

    clang -S -fobjc-arc main.m -o main.s

  • 生成目标文件

    clang -fmodules -c main.m -o main.o

  • 生成可执行文件

    clang main.o -o main

0x03 第一个插件

初始化一个插件项目

  打开源码路径llvm/tools/clang/example,example目录下新建目录,此例中为DemoPlugin。

  接着,修改example目录的CMakeLists.txt文件,添加如下:

1
add_subdirectory(DemoPlugin)

  来到testPlugin目录,新建如下三个文件

  • CMakeLists.txt
  • DemoPlugin.exports
  • DemoPlugin.cpp

  其中CMakeLists.txt内容如下,可以参考example目录下其他例子的CMakeLists.txt内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# If we don't need RTTI or EH, there's no reason to export anything
# from the plugin.
if( NOT MSVC )
if( NOT LLVM_REQUIRES_RTTI )
if( NOT LLVM_REQUIRES_EH )
set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/DemoPlugin.exports)
endif()
endif()
endif()

add_llvm_loadable_module(DemoPlugin DemoPlugin.cpp PLUGIN_TOOL clang)

if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
target_link_libraries(DemoPlugin PRIVATE
clangAST
clangBasic
clangFrontend
LLVMSupport
)
endif()

  然后开始最重要的testPlugin.cpp代码编写,

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
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/Frontend/CompilerInstance.h"

using namespace clang;

namespace
{
class DemoPluginConsumer : public ASTConsumer
{
CompilerInstance &Instance;
std::set<std::string> ParsedTemplates;
public:
DemoPluginConsumer(CompilerInstance &Instance,
std::set<std::string> ParsedTemplates)
: Instance(Instance), ParsedTemplates(ParsedTemplates) {}
};

class DemoPluginASTAction : public PluginASTAction
{
std::set<std::string> ParsedTemplates;
protected:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
llvm::StringRef) override
{
return llvm::make_unique<DemoPluginConsumer>(CI, ParsedTemplates);
}
// 插件的入口函数
bool ParseArgs(const CompilerInstance &CI,
const std::vector<std::string> &args) override {

DiagnosticsEngine &D = CI.getDiagnostics();
// Report的作用是向编译器报错
D.Report(D.getCustomDiagID(DiagnosticsEngine::Error,
"Hello Plugin"));

return true;
}
};
}

static clang::FrontendPluginRegistry::Add<DemoPluginASTAction>
X("DemoPlugin", "demo plugin");

  编译,在我们的之前编译源码时新建的目录

  • cmake

    • cmake llvm源代码目录
    • cmake –build .
    • make DemoPlugin
    • 插件出现在lib目录下,DemoPlugin.dylib
  • Xcode

    • cmake -G Xcode .llvm源代码目录 -DCMAKE_BUILD_TYPE:STRING=MinSizeRel
    • 打开LLVM.xcodeproj
    • 选择Automatically Create Schemes
    • 编译clang、DemoPlugin
    • 插件出现在Debug/lib目录下,DemoPlugin.dylib

配置Xcode

  Xcode的Build Setting里的Other C Flags添加如下,也就是DemoPlugin.dylib所在目录

-Xclang -load -Xclang /Users/ex-lifenglei/Desktop/llvm/myBuild/lib/DemoPlugin.dylib -Xclang -add-plugin -Xclang DemoPlugin

Build_Setting

  这时,我们build下我们的项目大多数会遇到下面的报错

Build_Error

  这是因为Xcode的Clang版本跟我们自己编译的Clang版本不一致。Clang插件需要对应的Clang版本来加载。所以我们还得修改Xcode指定的Clang。在Xcode的Build Setting里新增两个自定义项。

CC = /Users/ex-lifenglei/Desktop/llvm/myBuild/bin/clang-7

CXX = /Users/ex-lifenglei/Desktop/llvm/myBuild/bin/clang-7

Build_Setting

  然而在Xcode 9版本上,继续报错了,如下

Build_Error

  解决办法就是关掉Build Setting里的Index-While-Building

Build_Setting

  紧接着再次编译,我们成功编译失败,那是当然的,我们本来就设置了一个编译错误提示,说明我们的插件成功运行了!!!

Build_Success

  但是,因为这里是error,所以编译终止了,如果上面代码改成Warning让代码继续执行下去的话,可能会遇到下面这个错误

Build_Error

  解决方法就是把Xcode程序下的libarclite_iphonesimulator拷贝到编译目录下的../lib/arc下。如果是真机,也是同样的方法把libarclite_iphoneos.a复制过来。

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a

  我们再做个试验,我们把所有的类名和方法给打出来,代码如下

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
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"

using namespace clang;

namespace
{
// 可以深度优先搜索整个AST,并访问每一个基类,遍历需要处理的节点
class DemoPluginVisitor : public RecursiveASTVisitor<DemoPluginVisitor>
{
private:
CompilerInstance &Instance;
ASTContext *Context;

public:
void setASTContext (ASTContext &context)
{
this -> Context = &context;
}

DemoPluginVisitor (CompilerInstance &Instance)
:Instance(Instance) {}

// 查找类名
bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration) {
if(isUserSourceCode(declaration)) {
DiagnosticsEngine &D = Instance.getDiagnostics();
unsigned diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "查找到一个类名: %0");
D.Report(declaration->getLocStart(), diagID) << declaration->getName();
}
return true;
}

// 查找方法名
bool VisitObjCMethodDecl(ObjCMethodDecl *declaration) {
if(isUserSourceCode(declaration)) {
DiagnosticsEngine &D = Instance.getDiagnostics();
unsigned diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "查找到一个方法名: %0");
// D.Report(declaration->getLocStart(), diagID).AddString(declaration->getSelector().getAsString());
D.Report(declaration->getLocStart(), diagID) << declaration->getSelector().getAsString();
}
return true;
}

// 是否用户代码
bool isUserSourceCode (Decl *decl){
std::string filename = Instance.getSourceManager().getFilename(decl->getSourceRange().getBegin()).str();

if (filename.empty())
return false;
// 定义非Xcode中的源码都是用户源码
if(filename.find("/Applications/Xcode.app/") == 0)
return false;

return true;
}
};

class DemoPluginConsumer : public ASTConsumer
{
private:
DemoPluginVisitor visitor;
CompilerInstance &Instance;
std::set<std::string> ParsedTemplates;
public:
DemoPluginConsumer(CompilerInstance &Instance,
std::set<std::string> ParsedTemplates)
: Instance(Instance), ParsedTemplates(ParsedTemplates), visitor(Instance) {}

// 每次分析到一个顶层定义时会回调此函数,返回true表示处理
bool HandleTopLevelDecl(DeclGroupRef DG) override
{
return true;
}

// ASTConsumer的入口函数
void HandleTranslationUnit(ASTContext& context) override
{
visitor.setASTContext(context);
visitor.TraverseDecl(context.getTranslationUnitDecl());
}
};

class DemoPluginASTAction : public PluginASTAction
{
std::set<std::string> ParsedTemplates;
protected:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
llvm::StringRef) override
{
return llvm::make_unique<DemoPluginConsumer>(CI, ParsedTemplates);
}
// 插件的入口函数
bool ParseArgs(const CompilerInstance &CI,
const std::vector<std::string> &args) override {
return true;
}
};
}

static clang::FrontendPluginRegistry::Add<DemoPluginASTAction>
X("DemoPlugin", "demo plugin");

  运行结果如下,我们所有的类名和方法名都被打出来了。

Build_Demo

  更多文档查看:https://clang.llvm.org/doxygen/namespaceclang.html

iOS逆向的基本操作

发表于 2018-05-30 | 分类于 逆向

0x01 使用OpenSSH登录iPhone

   首先在Cydia安装OpenSSH插件

OpenSSH插件示意图

SSL:Secure Sockets Layer,是为了网络通信提供安全及数据完整性的一种安全协议,在传输层对网络连接进行加密。

OpenSSL:是SSL的开源实现,绝大部分的https其实就是http+openssl

OpenSSH:远程登录,并且可以保证登录的安全性,安全性指的就是其加密是由OpenSSL完成的。

   接着,需要保证手机和Mac是在同一个wifi环境下。在Mac上的终端里输入

ssh 用户名@手机ip地址

  这时候会让你保存散列值,这个散列值是用散列算法把公钥信息算出来的。

login示意图

  输入yes后,会问你手机root账户的登录密码,密码默认是alpine。(通过passwd和passwd mobile修改两个账号的密码)

login示意图

  密码输入后,就已经成功登录到你的手机上了,如果想要退出登录,终端输入exit即可。

SSL

  连接有三个步骤

  • 建立安全连接

    第一次登录时,iPhone会将其自己的公钥等信息发给Mac端,会问我们是否保存,即我们第一次连接手机时出现的提示。

    /etc/ssh/ssh_host_rsa_key.pub -> ~/.ssh/known_hosts

  • 客户端认证

    基于密码的认证

    基于密钥的认证

    Mac端生成公钥和私钥(终端输入:ssh-keygen),将公钥的内容追加到iPhone端(~/.ssh/authorized_keys,终端打开进.ssh目录:ssh-copy-id root@手机ip地址)

  • 数据传输


0x02 使用USB登录iPhone

  WIFI连接操作iPhone的效率低于直接用USB连接,所以我们有时候需要用USB进行登录手机的操作。

  默认情况下SSH走的是TCP协议,所以需要用到WIFI环境,所以我们需要设置将SSH通过USB连接手机,一般我们Mac电脑都有一个usbmuxd服务程序,首先我们需要将Mac电脑自己ssh到自己电脑的端口,比如端口10010,然后通过usbmuxd将这个10010端口映射到iPhone的22端口,这样相当于ssh到了手机的22端口。

  下载usbmuxd工具包:https://cgit.sukimashita.com/usbmuxd.git/snapshot/usbmuxd-1.0.8.tar.gz ,需要用到里面的tcprelay.py和usbmux.py脚本

usbmuxd_tool示意图

  映射端口,如果要保持端口映射就不能关闭或者结束当前终端命令

python tcprelay.py -t 22:10010

usbmuxd操作示意图

  新开一个终端窗口,ssh连接到10010端口

ssh root@localhost -p 10010

  这种方式建立的连接,也可以进行拷贝文件操作

pscp -P 10010 Mac上文件路径 root@localhost:iPhone文件夹路径

  默认情况下,iOS终端不支持中文输入和显示 新建一个~/.inputrc文件,文件内容设置为

set convert-meta off :不将中文字符转化为转义序列

set output-meta on :允许向终端输出中文

set meta-flag on \ set input-meta on :允许向终端输入中文


0x03 Cycript

  首先在手机Cydia上安装Cycript插件,安装完成后会要求重启Springboard。

  Cycript的常规操作,连接到手机后进行操作

  • cycript开启

    • cycript

    进入cy示意图

    • cycript -p 进程id
    • cycript -p 进程名称
  • 退出cycript:ctrl + d

  • 取消输入:ctrl + c

  • 查询进程:ps

    • ps -A:查询所有进程
    • ps -A | grep 关键字,过滤进程

1. 简单实战

  我们有如下页面,需要把红色的view隐藏掉

隐藏红色框示意图

  首先找到我们的app,此例中我们的进程是594

ps -A

寻找进程示意图

  连上我们的app,然后打印下UIApplication对象

cycript -p 594

打印UIApplication示意图

  如果想要打印所有成员变量

* 变量(或#地址)

打印成员变量示意图

  如果要递归打印所有子view

① view对象(或#地址).recursiveDescription().toString()

② choose(UILabel):可以筛选出所有子view是UILabel的

③ 层级打印:[view _printHierarchy].toString()

打印子View示意图

  至此,已经拿到我们要隐藏的Label的内存地址0x13ee12230,只需要设置hidden为YES即可

[#0x13ee12230 setHidden:YES]

  更多使用,可以查看官方手册:http://www.cycript.org/manual/

2. 封装Cycript常用方法

  比如想封装一个返回keyWindow的方法以及一个打印对应view所有子控件的方法,我们定义代码如下,函数前面加上exports表示调用的时候需要加上工具的名字,比如tool.keyWindow;如果不加exports,表示直接使用函数名调用。

1
2
3
4
5
6
(function(exports){
exports.keyWindow = function() {
return UIApp.keyWindow;
};

})(exports);

  保存的文件格式以cy结尾,并在文件保存到手机的/usr/lib/cycript0.9/com文件夹下,比如在此文件夹下新建一个文件夹myCycript,那么刚刚新建的cy文件保存到/usr/lib/cycript0.9/com/myCycript下

tool.cy保存示意图

  导入的时候@import com.myCycript.tool,并且进行使用

导入tool示意图


0x04 Reveal的使用

  首先手机的Cydia里面搜索安装Reveal Loader

手机安装reveal保存示意图

  然后设置 —> Reveal —> Enabled Applications —> 勾选要调试的app

勾选app示意图

  mac打开Reveal,菜单栏help —> Show Reveal Library in Finder —> iOS Library

Reveal找到iOS库示意图

  打开库,找到里面的RevealServer

Reveal库示意图

  在iPhone的/Library下找到RHRevealLoader目录,如果没有就新建一个,并将之前的RevealServer文件复制到这个目录,并改名为libReveal.dylib

scp /路径/RevealServer root@iPhone地址:/Library/RHRevealLoader/libReveal.dylib

Reveal获取app示意图


0x05 脱壳

  脱壳分为硬脱壳和动态脱壳,硬脱壳是根据解密算法进行脱壳,也是官方的方式;动态脱壳是在内存中将程序导出。可以查看Mach-O格式来验证是否加壳

① Mach-O View:Load Commands —> LC_ENCRYPTION_INFO —> Crypt ID的值为0是未加密

② otool工具:otool -l 文件路径 | grep crypt

1. Clutch

  首先下载 https://github.com/KJCracks/Clutch/releases ,将Clutch文件拷贝到手机的/usr/bin目录,使用方法如下

① clutch -i : 列出已安装的app

② clutch -d app序号或bundle id:脱壳

③ /private/var/mobile/Documents/Dumped下就是脱壳好的文件

2. Dumpdecrypted

  首先下载https://github.com/stefanesser/dumpdecrypted。

  在源代码目录下make进行编译,得到dumpdecrypted.dylib动态库文件。将这个文件拷贝到手机的/var/root目录下,使用方法如下

① 终端进入/var/root目录下

② 将这个动态库注入可执行文件中:DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib 文件路径/文件.app/dump出文件的名字

③ 生成的.dycrypted就是脱壳后的文件


0x06 动态调试

  动态调试需要用到debugserver,一开始是放在Xcode里面,当识别到手机设备时,Xcode会将debugserver放到手机的/Developer/usr/bin/目录下。

  但是一般情况下debugserver缺少权限,无法调试其他的app,需要重签名加上2个调试相关的权限。

  • get-task-allow
  • task_for_pid-allow

  先将/Developer/usr/bin/目录下的debugserver导出到mac,接着导出原来的签名权限

ldid -e debugserver > debugserver.entitlements

  给debugserver.entitlements 加上get-task-allow和task-for-pid_allow权限。接着重新签名

ldid -S debugserver.entitlements debugserver

如果没有安装ldid:brew install ldid

加上权限示意图

  也可以用codesign重签

① 查看权限信息:codesign -d —entitilements - debugserver

② 签名权限:codesign -f -s - –entitlements debugserver.entitlements debugserver

  最后放到/usr/bin/目录,注意不是之前的那个/Developer/usr/bin。

  我们在手机终端敲下debugserver,如果拒绝执行,需要加上运行权限

chmod +x debugserver

简单实战

  我们想要在button的点击事件打个断点

程序示意图

  找到进程id,此例是607。接着debugserver进行附加程序

debugserver *:10011 -a 607

表示的意思是:debugserver 地址:端口 -a 进程id

  电脑上如果是wifi连接手机的话,命令如下:

process connect connect://手机ip地址:debugserver的端口号(即上面的607)

  如果是usb映射连接手机的话,mac上需要多映射一个端口

python tcprelay.py -t 10011:10011

  接着,lldb连接debugserver服务

process connect connect://localhost:10011

  我们用私有方法_shortMethodDescription找到控制器当下所有的方法

po [[UIApplication sharedApplication].keyWindow.rootViewController _shortMethodDescription]

查找方法示意图

  我们找到了一个buttonClick的方法,我们设置断点

b buttonClick

设置方法断点示意图

  我们输入c,让程序继续运行,然后点下按钮,发现我们的方法被断下了

方法断点成功示意图

lldb其他命令

  • br l :查看所有断的点
  • br del 8:把标号为8的断点删除
  • b name:所有方法叫name的都下断点
  • b -[NSString stringWithFormat:]:给NSString类的stringWithFormat方法下断点
  • br s -S count:给所有selector为count的方法下断点
  • br s -a:内存地址断点
  • s:源码级别单步执行,遇到子函数进入
  • si:单步执行,遇到子函数进入
  • n:源码级别单步执行,遇到子函数不进入
  • ni:单步执行,遇到子函数不进入
  • f:退出子函数
  • image lookup –address 内存地址
  • image list -o -f:查看当前进程中的所有模块,一般第一个使我们的主程序,会出现两个值,左边的是ASLR偏移地址,右边是偏移后的地址。右边减去前面就是hopper查看到的基地址

参考页面:http://lldb.llvm.org/lldb-gdb.html

更多私有函数

  • recursiveDescription:按层次打印视图结构,递归方法,所以子视图下的子视图层级也会被打印
  • _printHierarchy:直接打印某个view的信息
  • _autolayoutTrace:recursiveDescription的简化版,去掉了view的相关描述
  • _ivarDescription:所有成员变量的名字和值
  • _methodDescription:打印对象的属性、实例方法和类方法

0x07 theos

1. 修改Mac的环境变量

  • 新建一个theos目录,此例中我们为~/Documents/theos

  • 编辑配置文件

    vi ~/.bash_profile

  • 新加入如下两行内容

    export THEOS=~/Documents/theos

    export PATH=$THEOS/bin:$PATH

  • 使环境变量生效

    source ~/.bash_profile

2. 下载theos

   下载到我们刚才的~/Documents/theos目录

git clone --recursive https://github.com/theos/theos.git $THEOS

3. 新建一个Tweak项目

  新建一个目录存放项目,本例中使用~/Documents/hack,进入该目录后输入命令

nic.pl

新建tweak示意图

  我们选择序号11

tweak项目初始化示意图

① 项目名称,随便写

② 项目ID,随便写

③ 作者,随便写,不写直接回车的话默认选择Mac用户名

④ 要被hack的项目Bundle ID,可以用Cycript找到你要项目的Bundle ID

⑤ 安装后要关闭的程序列表,直接回车即可

4. 编辑Makefile

  打开Makefile,添加两个变量说明是通过什么ip和什么端口来访问手机

  • THEOS_DEVICE_IP
  • THEOS_DEVICE_PORT
1
2
3
4
5
6
7
8
9
10
11
12
export THEOS_DEVICE_IP=127.0.0.1
export THEOS_DEVICE_PORT=10010

include $(THEOS)/makefiles/common.mk

TWEAK_NAME = firstTweak
firstTweak_FILES = Tweak.xm

include $(THEOS_MAKE_PATH)/tweak.mk

after-install::
install.exec "killall -9 SpringBoard"

  每次操作嫌麻烦的话,也可以直接到~/.bach_profile中添加。

5. 安装

  安装到手机,我们需要三个步骤:编译 –> 打包为deb –>安装

  • 编译:make

    如果装了多个Xcode可能会导致make报错,需要制定Xcode

    sudo xcode-select –switch /Application/Xcode.app/Contents/Developer

    如果继续报错,清理下之前的缓存:make clean

  • 打包:make package

    打包的时候如果遇到错误:

    make package报错示意图

    那就是压缩问题,改成gzip压缩就可以了

    • 修改dm.pl文件,vim $THEOS/vendor/dm.pl/dm.pl。用#号注释掉use IO::Compress::Lzma;和use IO::Compress::Xz;

    • 修改deb.mk内的压缩方式为gzip

      $ vim $THEOS/makefiles/package/deb.mk

      _THEOS_PLATFORM_DPKG_DEB_COMPRESSION ?= gzip

  • 安装:make install

6. 演示

  做个简单demo,我们如下图有5个cell,要求新增2个cell,分别展示6娃和7娃。

tweak_demo示意图

  我们在我们的Tweak项目里,打开Tweak.xm文件,编写代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
%hook ViewController

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return %orig + 2;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.row == 5 || indexPath.row == 6) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if(!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
}

cell.textLabel.text = [NSString stringWithFormat:@"%@娃", @(indexPath.row + 1)];
return cell;
}


return %orig;
}

%end

  安装到手机后,会需要确认两次手机root用户的密码

tweak_install示意图

  看下我们软件的变化,我们已经完成了我们的需求。即使重启app,也会一直生效。

tweak_demo示意图

  theos常用语法:

  • %hook:与%end配对,表示hook的类名

  • %orig:调用原来的实现

  • %new:新建类或方法

  • %log:打印到控制台

  • %property:新加属性

    更多语法参见:http://iphonedevwiki.net/index.php/Logos


0x08 Frida

1. 安装

  Mac的安装

sudo pip install frida

  iPhone的安装

① 添加源:https://build.frida.re/

② 源加完后,搜索Frida进行安装

2. 使用

所有进程:frida-ps -U

附加进程:frida -U 进程名字

dyld启动流程

发表于 2018-05-28 | 分类于 iOS

0x01 launchd

   launchd是第一个被内核启动的用户态进程,负责直接或间接的启动系统中的其他进程。它是用户模式里所有进程的父进程,同时也将负责两种后台作业:守护程序和代理程序。

守护程序:后台服务,通常和用户没有交互。比如push通知、外接设备插入的处理和XPC等。

代理程序:可以和用户交互,比如Mac的Finder或iOS的SpringBoard就是其中之一,即广义上我们理解的桌面。

   launchd是如何被创建的,得先看下下面这张XNU启动流程图

xnu启动示意图

  • start(iOS):初始化MSR、物理页映射、安装中断处理函数

  • arm_init(iOS):初始化平台,为启动内核做准备

  • machine_startup:解析命令行参数和调试参数

  • kernel_bootstrap:安装和初始化mach内核的子系统,包括:进程间通信、时钟、访问策略、进程和线程调度。

  • kernel_bootstrap_thread:创建idle线程,初始化iokit设备驱动框架,初始化应用程序和dyld运行所需的共享模块。如果内核开启了mac(强制访问控制)策略,则会进行mac的初始化,以确保系统的安全。

  • bsd_init:内核部分剩余的事情都由其来做,初始化各个子系统。网络、文件系统、管道、内存cache、线程、进程、同步对象、权限策略等等。 一切完成后,会执行/sbin/launchd来创建一个launchd。

我们看下源码的初始化过程,launchd是怎么被启动起来的

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
void bsd_init(void) {
......
bsd_utaskbootstrap();
......
}

void bsd_utaskbootstrap(void) {
thread_t thread;
struct uthread *ut;

// 从内核进程克隆引导进程,但不从内核继承任何任务特性或内存
thread = cloneproc(TASK_NULL, COALITION_NULL, kernproc, FALSE, TRUE);

/* Hold the reference as it will be dropped during shutdown */
initproc = proc_find(1);

/*
* Since we aren't going back out the normal way to our parent,
* we have to drop the transition locks explicitly.
*/
proc_signalend(initproc, 0);
proc_transend(initproc, 0);

ut = (struct uthread *)get_bsdthread_info(thread);
ut->uu_sigmask = 0;
// 为了真正地创建出任务,对创建出的线程调用这个函数
// 执行后产生一个异步系统陷阱(AST),Mach的AST异步处理程序会特别处理这个情况,即调用bsd_ast()
act_set_astbsd(thread);
task_clear_return_wait(get_threadtask(thread));
}

void bsd_ast(thread_t thread) {
......
if (!bsd_init_done) {
bsd_init_done = 1;
bsdinit_task();
}
......
}

void bsdinit_task(void)
{
proc_t p = current_proc();
struct uthread *ut;
thread_t thread;

// 将这个从内核态克隆到用户态的第一个线程的名字设置为init
process_name("init", p);
// 内部创建了一个Mach内核线程处理ux_handler,而ux_handler设置了一个消息循环用于监听异常,如果接收到异常,将异常转换为UNIX信号,并投递到出错线程。
ux_handler_init();

thread = current_thread();
// ux_handler_init()返回时,ux_handler已经在另一个线程中执行了,并注册好了ux_exception_port。
// 这个函数将所有的Mach异常消息都重定向到ux_exception_port
// 由于所有程序都是launchld后代,所以都会继承这个异常端口
(void) host_set_exception_ports(host_priv_self(),
EXC_MASK_ALL & ~(EXC_MASK_RPC_ALERT),//pilotfish (shark) needs this port
(mach_port_t) ux_exception_port,
EXCEPTION_DEFAULT| MACH_EXCEPTION_CODES,
0);

ut = (uthread_t)get_bsdthread_info(thread);

vm_init_before_launchd();


bsd_init_kprintf("bsd_do_post - done");
// 加载launchd
load_init_program(p);
lock_trace = 1;
}

void load_init_program(proc_t p)
{
uint32_t i;
int error;
vm_map_t map = current_map();
mach_vm_offset_t scratch_addr = 0;
mach_vm_size_t map_page_size = vm_map_page_size(map);

(void) mach_vm_allocate_kernel(map, &scratch_addr, map_page_size, VM_FLAGS_ANYWHERE, VM_KERN_MEMORY_NONE);

error = ENOENT;
// 加载“init”程序,这里指的是launchd
// init_programs保存着要运行程序的路径
for (i = 0; i < sizeof(init_programs)/sizeof(init_programs[0]); i++) {
printf("load_init_program: attempting to load %s\n", init_programs[i]);
// 使用从系统克隆出的那个第一个线程加载这个"init"程序,即加载launchd
error = load_init_program_at_path(p, (user_addr_t)scratch_addr, init_programs[i]);
if (!error) {
return;
} else {
printf("load_init_program: failed loading %s: errno %d\n", init_programs[i], error);
}
}

panic("Process 1 exec of %s failed, errno %d", ((i == 0) ? "<null>" : init_programs[i-1]), error);
}

static int load_init_program_at_path(proc_t p, user_addr_t scratch_addr, const char* path)
{
return execve(p, &init_exec_args, retval);
}

   init_programs装的就是launchd程序的路径

1
2
3
4
5
6
7
8
9
static const char * init_programs[] = {
#if DEBUG
"/usr/local/sbin/launchd.debug",
#endif
#if DEVELOPMENT || DEBUG
"/usr/local/sbin/launchd.development",
#endif
"/sbin/launchd",
};

    我们知道iOS和Mac执行的都是Mach-O格式的文件,即使是launchd也是一样,所以接下来的步骤,同样适用于其他进程加载app程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int execve(proc_t p, struct execve_args *uap, int32_t *retval)
{
struct __mac_execve_args muap;
int err;

memoryshot(VM_EXECVE, DBG_FUNC_NONE);

muap.fname = uap->fname;
muap.argp = uap->argp;
muap.envp = uap->envp;
muap.mac_p = USER_ADDR_NULL;
err = __mac_execve(p, &muap, retval);

return(err);
}

0x02 MACH-O格式

  Mach-O是OS X和iOS的可执行文件,类似于安卓的elf和微软的PE,但又不仅限于可执行文件,比如iOS的动态库其实也可以Mach-O格式。其格式如下图:

Mach-O格式示意图

  Mach-O在加载过程中,在内核态的处理主要是对进程的一些基本设置,比如分配虚拟内存、创建主线程以及代码签名、加密等任务。而在转由去用户态的时候调用动态加载器dyld会继续对Mach-O做处理,比如库加载和符号解析等。

1. header

  头信息的格式如下:

1
2
3
4
5
6
7
8
9
10
struct mach_header_64 {
uint32_t magic; /* 0xfeedfacf表示64位,而0xfeedface表示32位 */
cpu_type_t cputype; /* CPU平台:arm还是i386 */
cpu_subtype_t cpusubtype; /* armv7、armv8等等 */
uint32_t filetype; /* 文件类型,比如是可执行程序还是动态库等 */
uint32_t ncmds; /* load commands的数量 */
uint32_t sizeofcmds; /* load commands的大小 */
uint32_t flags; /* 标签参数 */
uint32_t reserved; /* reserved,保留字段,暂时没用 */
};

1.1 filetype

  常见的Mach-O文件类型有以下几种:

  • MH_OBJECT

    目标文件,比如编译后得到的.o文件

    静态库文件,比如.a文件

  • MH_EXECUTE

    可执行文件,广义上我们口中常说的app文件,即ipa拆包后得到的文件

  • MH_DYLIB

    动态库文件,比如.dylib或.framework

  • MH_DYLINKER

    动态链接器,启动dyld

  • MH_DSYM

    存储着二进制文件符号信息的文件,常用于分析闪退信息等

1.2 flags

  常见的标签参数有以下几种

  • MH_DYLDLINK

    作为动态链接器的输入文件,不能再次被静态链接编辑

  • MH_PIE

    加载主程序在一个随机地址。仅文件类型是MH_EXECUTE的才有效

2. Load Commands

  这个主要描述的是文件在虚拟内存中的逻辑结构和布局,可以在被调用的时候清晰地知道如何设置并加载二进制数据。其结构如下

1
2
3
4
struct load_command {
uint32_t cmd; /* load command类型 */
uint32_t cmdsize; /* 大小 */
};

  Load Commands紧跟着mach_header,其总的大小保存在mach_header里的sizeofcmds里。所有的load commands都必须有自己的两个成员cmd和cmdsize,其中cmdsize在64架构中必须是8的倍数。而cmd表示的是类型,常见的类型如下

  • LC_SEGMENT(LC_SEGMENT_64)

    将文件中(32位或64位)的段映射到进程地址空间。包括__text代码区、常量区和OC类信息等。

  • LC_LOAD_DYLINKER

    启动动态链接器,dyld

  • LC_UUID

    这个id是匹配一个二进制文件及其对应的符号,是个唯一值

  • LC_THREAD

    开启一个Mach线程,不分配栈

  • LC_UNIXTHREAD

    开启一个Unix线程,现被LC_MAIN替代

  • LC_CORE_SIGNATURE

    代码签名,如果签名与代码本身不匹配,进程会被杀掉

  • LC_ENCRYPTION_INFO

    加密信息

  load_commands在Mach-O中的实例结构如下:

load_commands示意图

3. 通用 Mach-O

  根据编译配置,我们可以生成只包含一种架构的Mach-O文件,比如armv7。当然也可以编译生成多架构的的Mach-O文件,这种包含多种架构的我们称之为通用Mach-O,也可以称为Fat Mach-O。运行通用Mach-O的时候,加载器会选择合适的架构的代码去执行。

0x03 地址空间随机布局(ASLR)

  如果应用启动的时候都是进程空间某个固定地址开始,这也就意味着内存中的地址分布具有非常强的可预测性,这就给黑客很大的利用机会。所以现在大部分操作系统都会采用ASLR这样的技术,这将有效防止被攻击。

  进程每一次启动时,地址空间都将被随机化,即偏移。实现方法是通过内核将Mach-O的Segment平移某个随机系数。后面的代码阅读中,我们将会遇到这个技术。

0x04 dyld被加载流程

  在UNIX中,进程不能被创建出来,只能通过fork( ) 系统调用复制出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
int __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval)
{
char *bufp = NULL;
struct image_params *imgp;
struct vnode_attr *vap;
struct vnode_attr *origvap;
int error;
int is_64 = IS_64BIT_PROCESS(p);
struct vfs_context context;
struct uthread *uthread;
task_t new_task = NULL;
boolean_t should_release_proc_ref = FALSE;
boolean_t exec_done = FALSE;
boolean_t in_vfexec = FALSE;
void *inherit = NULL;

context.vc_thread = current_thread();
context.vc_ucred = kauth_cred_proc_ref(p);

// 分配一大块内存
MALLOC(bufp, char *, (sizeof(*imgp) + sizeof(*vap) + sizeof(*origvap)), M_TEMP, M_WAITOK | M_ZERO);
imgp = (struct image_params *) bufp;
if (bufp == NULL) {
error = ENOMEM;
goto exit_with_error;
}
vap = (struct vnode_attr *) (bufp + sizeof(*imgp));
origvap = (struct vnode_attr *) (bufp + sizeof(*imgp) + sizeof(*vap));

// 初始化
imgp->ip_user_fname = uap->fname;
imgp->ip_user_argv = uap->argp;
imgp->ip_user_envv = uap->envp;
imgp->ip_vattr = vap;
imgp->ip_origvattr = origvap;
imgp->ip_vfs_context = &context;
imgp->ip_flags = (is_64 ? IMGPF_WAS_64BIT : IMGPF_NONE) | ((p->p_flag & P_DISABLE_ASLR) ? IMGPF_DISABLE_ASLR : IMGPF_NONE);
imgp->ip_seg = (is_64 ? UIO_USERSPACE64 : UIO_USERSPACE32);
imgp->ip_mac_return = 0;
imgp->ip_cs_error = OS_REASON_NULL;

uthread = get_bsdthread_info(current_thread());
if (uthread->uu_flag & UT_VFORK) {
imgp->ip_flags |= IMGPF_VFORK_EXEC;
in_vfexec = TRUE;
// 程序启动需要fork一条新的进程,会走这个else分支
} else {
imgp->ip_flags |= IMGPF_EXEC;
// fork进程
imgp->ip_new_thread = fork_create_child(current_task(),
NULL, p, FALSE, p->p_flag & P_LP64, TRUE);
/* task and thread ref returned by fork_create_child */
if (imgp->ip_new_thread == NULL) {
error = ENOMEM;
goto exit_with_error;
}

new_task = get_threadtask(imgp->ip_new_thread);
context.vc_thread = imgp->ip_new_thread;
}

// 解析程序
error = exec_activate_image(imgp);

if (imgp->ip_new_thread != NULL) {
new_task = get_threadtask(imgp->ip_new_thread);
}

if (!error && !in_vfexec) {
p = proc_exec_switch_task(p, current_task(), new_task, imgp->ip_new_thread);

should_release_proc_ref = TRUE;
}

kauth_cred_unref(&context.vc_ucred);

if (!error) {
task_bank_init(get_threadtask(imgp->ip_new_thread));
proc_transend(p, 0);

/* Sever any extant thread affinity */
thread_affinity_exec(current_thread());

/* Inherit task role from old task to new task for exec */
if (!in_vfexec) {
proc_inherit_task_role(get_threadtask(imgp->ip_new_thread), current_task());
}

thread_t main_thread = imgp->ip_new_thread;
// 设置进程的主线程
task_set_main_thread_qos(new_task, main_thread);
}
.......
}

static int exec_activate_image(struct image_params *imgp)
{
......
// 调用格式对应的加载函数
// 比如胖指令集有对应的胖指令集加载函数
for(i = 0; error == -1 && execsw[i].ex_imgact != NULL; i++) {

error = (*execsw[i].ex_imgact)(imgp);
......
}
......
}

  execsw的结构如下

1
2
3
4
5
6
7
8
9
struct execsw {
int (*ex_imgact)(struct image_params *);
const char *ex_name;
} execsw[] = {
{ exec_mach_imgact, "Mach-o Binary" },
{ exec_fat_imgact, "Fat Binary" },
{ exec_shell_imgact, "Interpreter Script" },
{ NULL, NULL}
};

  对应的指令加载,load_machfile函数加载mach-o文件,activate_exec_state处理拿到的结果信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int exec_mach_imgact(struct image_params *imgp)
{
.......
lret = load_machfile(imgp, mach_header, thread, &map, &load_result);
.......
lret = activate_exec_state(task, p, thread, &load_result);
}

load_return_t load_machfile(
struct image_params *imgp,
struct mach_header *header,
thread_t thread,
vm_map_t *mapp,
load_result_t *result
)
{

lret = parse_machfile(vp, map, thread, header, file_offset, macho_size,
0, aslr_page_offset, dyld_aslr_page_offset, result,
NULL, imgp);
}

static int activate_exec_state(task_t task, proc_t p, thread_t thread, load_result_t *result)
{
......
// 设置入口点
thread_setentrypoint(thread, result->entry_point);
......
}

  我们再解析完mach-o文件后,就会拿到结果信息取做处理,其中就有一个设置入口点,也就是在解析完毕后就会跳转到这个入口点运行程序,所以这个入口点很关键,那这个入口点是什么呢?其赋值肯定是在解析mach-o的过程中,所以还是得先来看看解析mach-o文件的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
static
load_return_t
parse_machfile(
struct vnode *vp,
vm_map_t map,
thread_t thread,
struct mach_header *header,
off_t file_offset,
off_t macho_size,
int depth,
int64_t aslr_offset,
int64_t dyld_aslr_offset,
load_result_t *result,
load_result_t *binresult,
struct image_params *imgp
)
{
uint32_t ncmds;
struct load_command *lcp;
struct dylinker_command *dlp = 0;
integer_t dlarchbits = 0;
void * control;
load_return_t ret = LOAD_SUCCESS;
void * addr;
vm_size_t alloc_size, cmds_size;
size_t offset;
size_t oldoffset; /* for overflow check */
int pass;
proc_t p = current_proc(); /* XXXX */
int error;
int resid = 0;
size_t mach_header_sz = sizeof(struct mach_header);
boolean_t abi64;
boolean_t got_code_signatures = FALSE;
boolean_t found_header_segment = FALSE;
boolean_t found_xhdr = FALSE;
int64_t slide = 0;
boolean_t dyld_no_load_addr = FALSE;
boolean_t is_dyld = FALSE;
vm_map_offset_t effective_page_mask = MAX(PAGE_MASK, vm_map_page_mask(map));
#if __arm64__
uint32_t pagezero_end = 0;
uint32_t executable_end = 0;
uint32_t writable_start = 0;
vm_map_size_t effective_page_size;

effective_page_size = MAX(PAGE_SIZE, vm_map_page_size(map));
#endif /* __arm64__ */

if (header->magic == MH_MAGIC_64 ||
header->magic == MH_CIGAM_64) {
mach_header_sz = sizeof(struct mach_header_64);
}

/*
* Break infinite recursion
*/
if (depth > 1) {
return(LOAD_FAILURE);
}
// 此函数会被遍历两次,第一次解析主程序的Mach-O,第二次解析dyld
depth++;

/*
* 校验文件的CPU架构和当前运行环境的CPU架构是否一致
*/
if (((cpu_type_t)(header->cputype & ~CPU_ARCH_MASK) != (cpu_type() & ~CPU_ARCH_MASK)) ||
!grade_binary(header->cputype,
header->cpusubtype & ~CPU_SUBTYPE_MASK))
return(LOAD_BADARCH);

abi64 = ((header->cputype & CPU_ARCH_ABI64) == CPU_ARCH_ABI64);

// 根据文件类型,区别处理
switch (header->filetype) {

// 如果是应用程序,即app
case MH_EXECUTE:
if (depth != 1) {
return (LOAD_FAILURE);
}
#if CONFIG_EMBEDDED
// 如果需要作为动态链接器的输入文件,肯定会进入这里,因为dyld还需要解析一次主程序
if (header->flags & MH_DYLDLINK) {
/* Check properties of dynamic executables */
if (!(header->flags & MH_PIE) && pie_required(header->cputype, header->cpusubtype & ~CPU_SUBTYPE_MASK)) {
return (LOAD_FAILURE);
}
result->needs_dynlinker = TRUE;
} else {
/* Check properties of static executables (disallowed except for development) */
#if !(DEVELOPMENT || DEBUG)
return (LOAD_FAILURE);
#endif
}
#endif /* CONFIG_EMBEDDED */

break;

// 如果是动态链接器
case MH_DYLINKER:
if (depth != 2) {
return (LOAD_FAILURE);
}
is_dyld = TRUE;
break;

default:
return (LOAD_FAILURE);
}

addr = kalloc(alloc_size);
if (addr == NULL) {
return LOAD_NOSPACE;
}

......

// 如果是dyld动态链接器,并且设置了随机地址加载这个动态链接器,就将随机地址的偏移值赋给slide
if ((header->flags & MH_PIE) || is_dyld) {
slide = aslr_offset;
}

/*
* 遍历四次,每次只做一件事
* 0: 检查代码段和数据段是否对齐
* 1: 进程状态, uuid, 代码签名
* 2: segments
* 3: dyld, encryption, check entry point
*/

boolean_t slide_realign = FALSE;
#if __arm64__
if (!abi64) {
slide_realign = TRUE;
}
#endif

for (pass = 0; pass <= 3; pass++) {
// 如果不需要做对齐校验,直接下一轮
if (pass == 0 && !slide_realign && !is_dyld) {
/* if we dont need to realign the slide or determine dyld's load
* address, pass 0 can be skipped */
continue;
} else if (pass == 1) {
#if __arm64__
boolean_t is_pie;
int64_t adjust;

is_pie = ((header->flags & MH_PIE) != 0);
if (pagezero_end != 0 &&
pagezero_end < effective_page_size) {
/* need at least 1 page for PAGEZERO */
adjust = effective_page_size;
MACHO_PRINTF(("pagezero boundary at "
"0x%llx; adjust slide from "
"0x%llx to 0x%llx%s\n",
(uint64_t) pagezero_end,
slide,
slide + adjust,
(is_pie
? ""
: " BUT NO PIE ****** :-(")));
if (is_pie) {
slide += adjust;
pagezero_end += adjust;
executable_end += adjust;
writable_start += adjust;
}
}
if (pagezero_end != 0) {
result->has_pagezero = TRUE;
}
if (executable_end == writable_start &&
(executable_end & effective_page_mask) != 0 &&
(executable_end & FOURK_PAGE_MASK) == 0) {

// 数据段或代码段校对,让其页对齐
adjust =
(effective_page_size -
(executable_end & effective_page_mask));
MACHO_PRINTF(("page-unaligned X-W boundary at "
"0x%llx; adjust slide from "
"0x%llx to 0x%llx%s\n",
(uint64_t) executable_end,
slide,
slide + adjust,
(is_pie
? ""
: " BUT NO PIE ****** :-(")));
if (is_pie)
slide += adjust;
}
#endif /* __arm64__ */

if (dyld_no_load_addr && binresult) {
// dyld在用户态的地址 = 随机地址 + 文件最大的虚拟地址
slide = vm_map_round_page(slide + binresult->max_vm_addr, effective_page_mask);
}
}


offset = mach_header_sz;
ncmds = header->ncmds;

while (ncmds--) {

/*
* 获取要解析的load_command地址
*/
lcp = (struct load_command *)(addr + offset);
oldoffset = offset;


switch(lcp->cmd) {
// 指导内核如何设置新运行进行的内存空间。这些段直接从Mach-O加载到内存中
case LC_SEGMENT: {
struct segment_command *scp = (struct segment_command *) lcp;

......

// segment映射和解析
// segment下还有区的概念,比如__objc_classlist,__objc_protolist
ret = load_segment(lcp,
header->filetype,
control,
file_offset,
macho_size,
vp,
map,
slide,
result);

break;
}
// 映射文件中的特定的字节到虚拟内存
case LC_SEGMENT_64: {
struct segment_command_64 *scp64 = (struct segment_command_64 *) lcp;

......

ret = load_segment(lcp,
header->filetype,
control,
file_offset,
macho_size,
vp,
map,
slide,
result);

break;
}
// UNIX线程,包含堆栈
case LC_UNIXTHREAD:
if (pass != 1)
break;
ret = load_unixthread(
(struct thread_command *) lcp,
thread,
slide,
result);
break;
// 替换LC_UNIXTHREAD
case LC_MAIN:
......
ret = load_main(
(struct entry_point_command *) lcp,
thread,
slide,
result);
break;
// 加载动态链接器
case LC_LOAD_DYLINKER:
if (pass != 3)
break;
if ((depth == 1) && (dlp == 0)) {
// 动态解析器地址
dlp = (struct dylinker_command *)lcp;
dlarchbits = (header->cputype & CPU_ARCH_MASK);
} else {
ret = LOAD_FAILURE;
}
break;
// uuid
case LC_UUID:
if (pass == 1 && depth == 1) {
ret = load_uuid((struct uuid_command *) lcp,
(char *)addr + cmds_size,
result);
}
break;
// 代码签名
case LC_CODE_SIGNATURE:
/* CODE SIGNING */
if (pass != 1)
break;
/* pager -> uip ->
load signatures & store in uip
set VM object "signed_pages"
*/
ret = load_code_signature(
(struct linkedit_data_command *) lcp,
vp,
file_offset,
macho_size,
header->cputype,
result,
imgp);

.......

break;
#if CONFIG_CODE_DECRYPTION
// 加密的段信息
case LC_ENCRYPTION_INFO:
case LC_ENCRYPTION_INFO_64:
if (pass != 3)
break;
ret = set_code_unprotect(
(struct encryption_info_command *) lcp,
addr, map, slide, vp, file_offset,
header->cputype, header->cpusubtype);
......
}
break;
#endif
default:
/* Other commands are ignored by the kernel */
ret = LOAD_SUCCESS;
break;
}
if (ret != LOAD_SUCCESS)
break;
}
if (ret != LOAD_SUCCESS)
break;
}

if (ret == LOAD_SUCCESS) {

/* Make sure if we need dyld, we got it */
if (result->needs_dynlinker && !dlp) {
ret = LOAD_FAILURE;
}

if ((ret == LOAD_SUCCESS) && (dlp != 0)) {
/*
* 加载动态解析器, 会再次调用一次parse_machfile
*/
ret = load_dylinker(dlp, dlarchbits, map, thread, depth,
dyld_aslr_offset, result, imgp);
}

.......
}

if (ret == LOAD_BADMACHO && found_xhdr) {
ret = LOAD_BADMACHO_UPX;
}

kfree(addr, alloc_size);

return ret;
}

  上面的过程得到的结果会被赋值进load_result_t这个结果体

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
typedef struct _load_result {
user_addr_t mach_header;
user_addr_t entry_point;

user_addr_t user_stack;
mach_vm_size_t user_stack_size;

user_addr_t user_stack_alloc;
mach_vm_size_t user_stack_alloc_size;

mach_vm_address_t all_image_info_addr;
mach_vm_size_t all_image_info_size;

int thread_count;
unsigned int
/* boolean_t */ unixproc :1,
needs_dynlinker : 1,
dynlinker :1,
validentry :1,
has_pagezero :1,
using_lcmain :1,
is64bit :1,
:0;
unsigned int csflags;
unsigned char uuid[16];
mach_vm_address_t min_vm_addr;
mach_vm_address_t max_vm_addr;
unsigned int platform_binary;
off_t cs_end_offset;
void *threadstate;
size_t threadstate_sz;
} load_result_t;

  那么在哪里设置entry_point,其实entry_point的设置在load_dylinker里

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 load_return_t load_dylinker{
.......
*myresult = load_result_null;
myresult->is64bit = result->is64bit;

ret = parse_machfile(vp, map, thread, header, file_offset,
macho_size, depth, slide, 0, myresult, result, imgp);

if (ret == LOAD_SUCCESS) {
if (result->threadstate) {
/* don't use the app's threadstate if we have a dyld */
kfree(result->threadstate, result->threadstate_sz);
}
result->threadstate = myresult->threadstate;
result->threadstate_sz = myresult->threadstate_sz;

result->dynlinker = TRUE;
// 将load_result_t的entry_point,设置为dyld动态链接库的entrypoint,所以启动的时候首先加载的会是dyld。
result->entry_point = myresult->entry_point;
result->validentry = myresult->validentry;
result->all_image_info_addr = myresult->all_image_info_addr;
result->all_image_info_size = myresult->all_image_info_size;
if (myresult->platform_binary) {
result->csflags |= CS_DYLD_PLATFORM;
}
}
....
}

  最后,梳理下这个app启动流程:

  • fork一条新的进程出来

  • 激活app

    a. 区分文件,Mach-o Binary和Fat Binary都有对应的加载函数

    b. 分配内存

    c. 解析主程序的Mach-O信息

    d. 读取主程序Mach-O头信息

    e. 遍历主程序每条load command信息,装载进内存

    f. 解析dyld,再把d,e的内容再做一遍,期间会将entry_point入口地址改为dyld的入口地址。

  • 进入entry_point对应的入口,启动dyld

  • 设置进程的主线程

  所有的操作做完,这时候也已经从内核态进入用户态了。

0x05 dyld加载程序流程

  上面在最后一次加载完dyld后,就进入dyld的入口函数,即__dyld_start,这段其实是一段汇编代码

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
__dyld_start:
// 一些准备工作,获取头、参数等等这类的信息
mov x28, sp
and sp, x28, #~15 // force 16-byte alignment of stack
mov x0, #0
mov x1, #0
stp x1, x0, [sp, #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp, #16 // make room for local variables
ldr x0, [x28] // get app's mh into x0
ldr x1, [x28, #8] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add x2, x28, #16 // get argv into x2
adrp x4,___dso_handle@page
add x4,x4,___dso_handle@pageoff // get dyld's mh in to x4
adrp x3,__dso_static@page
ldr x3,[x3,__dso_static@pageoff] // get unslid start of dyld
sub x3,x4,x3 // x3 now has slide of dyld
mov x5,sp // x5 has &startGlue

// 启动引导,入口为dyldbootstrap::start函数
bl __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
// 会返回主程序的入口地址,并保存到x16寄存器
mov x16,x0
ldr x1, [sp]
cmp x1, #0
b.ne Lnew

// LC_UNIXTHREAD,由LC_MAIN代替,所以直接看下面的LC_MAIN
add sp, x28, #8
br x16

// LC_MAIN ,设置栈信息,并跳入到主程序的入口
Lnew: mov lr, x1 // simulate return address into _start in libdyld.dylib
ldr x0, [x28, #8] // 参数1 = argc
add x1, x28, #16 // 参数2 = argv
add x2, x1, x0, lsl #3
add x2, x2, #8 // 参数3 = &env[0]
mov x3, x2
Lapple: ldr x4, [x3]
add x3, x3, #8
cmp x4, #0
b.ne Lapple // 参数4 = apple
// 跳转到主程序的main函数
br x16

  __dyld_start首先会调用dyldbootstrap::start函数对主程序再次进行一些处理,比如加载动态库,处理完成后会返回主程序的入口地址,然后设置好主程序入口的一些参数后就进入到主程序的main函数。我们关注的是主程序启动前还做了些什么事情?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
intptr_t slide, const struct macho_header* dyldsMachHeader,
uintptr_t* startGlue)
{
if ( slide != 0 ) {
rebaseDyld(dyldsMachHeader, slide);
}

mach_init();

// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];

// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;

// set up random value for stack canary
__guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif

// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

uintptr_t _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
dyld3::kdebug_trace_dyld_signpost(DBG_DYLD_SIGNPOST_START_DYLD, 0, 0);

// Grab the cdHash of the main executable from the environment
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
mainExecutableCDHash = mainExecutableCDHashBuffer;

// Trace dyld's load
notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
#if !TARGET_IPHONE_SIMULATOR
// Trace the main executable's load
notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif

uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;
sMainExecutableSlide = mainExecutableSlide;

CRSetCrashLogMessage("dyld: launch started");
// 设置上下文运行环境
setContext(mainExecutableMH, argc, argv, envp, apple);

// Pickup the pointer to the exec path.
sExecPath = _simple_getenv(apple, "executable_path");

// Remember short name of process for later logging
sExecShortName = ::strrchr(sExecPath, '/');
if ( sExecShortName != NULL )
++sExecShortName;
else
sExecShortName = sExecPath;

// 配置进程限制
configureProcessRestrictions(mainExecutableMH);

checkEnvironmentVariables(envp);

defaultUninitializedFallbackPaths(envp);

if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
getHostInfo(mainExecutableMH, mainExecutableSlide);


checkSharedRegionDisable((mach_header*)mainExecutableMH);
// 加载共享缓存库
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
mapSharedCache();
}

......

// install gdb notifier
stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
stateToHandlers(dyld_image_state_mapped, sSingleHandlers)->push_back(updateAllImages);
// make initial allocations large enough that it is unlikely to need to be re-alloced
sImageRoots.reserve(16);
sAddImageCallbacks.reserve(4);
sRemoveImageCallbacks.reserve(4);
sImageFilesNeedingTermination.reserve(16);
sImageFilesNeedingDOFUnregistration.reserve(8);


try {
// add dyld itself to UUID list
addDyldImageToUUIDList();

#if SUPPORT_ACCELERATE_TABLES
bool mainExcutableAlreadyRebased = false;
if ( (sSharedCacheLoadInfo.loadAddress != nullptr) && !dylibsCanOverrideCache() && !sDisableAcceleratorTables && (sSharedCacheLoadInfo.loadAddress->header.accelerateInfoAddr != 0) ) {
struct stat statBuf;
if ( ::stat(IPHONE_DYLD_SHARED_CACHE_DIR "no-dyld2-accelerator-tables", &statBuf) != 0 )
sAllCacheImagesProxy = ImageLoaderMegaDylib::makeImageLoaderMegaDylib(&sSharedCacheLoadInfo.loadAddress->header, sSharedCacheLoadInfo.slide, mainExecutableMH, gLinkContext);
}

reloadAllImages:
#endif

CRSetCrashLogMessage(sLoadingCrashMessage);
// 初始化主程序
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);


gLinkContext.strictMachORequired = true;

#if SUPPORT_ACCELERATE_TABLES
sAllImages.reserve((sAllCacheImagesProxy != NULL) ? 16 : INITIAL_IMAGE_COUNT);
#else
sAllImages.reserve(INITIAL_IMAGE_COUNT);
#endif

// Now that shared cache is loaded, setup an versioned dylib overrides
#if SUPPORT_VERSIONED_PATHS
checkVersionedPaths();
#endif


// dyld_all_image_infos image list does not contain dyld
// add it as dyldPath field in dyld_all_image_infos
// for simulator, dyld_sim is in image list, need host dyld added
#if TARGET_IPHONE_SIMULATOR
// get path of host dyld from table of syscall vectors in host dyld
void* addressInDyld = gSyscallHelpers;
#else
// get path of dyld itself
void* addressInDyld = (void*)&__dso_handle;
#endif
char dyldPathBuffer[MAXPATHLEN+1];
int len = proc_regionfilename(getpid(), (uint64_t)(long)addressInDyld, dyldPathBuffer, MAXPATHLEN);
if ( len > 0 ) {
dyldPathBuffer[len] = '\0'; // proc_regionfilename() does not zero terminate returned string
if ( strcmp(dyldPathBuffer, gProcessInfo->dyldPath) != 0 )
gProcessInfo->dyldPath = strdup(dyldPathBuffer);
}

// 加载插入的动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// record count of inserted libraries so that a flat search will look at
// inserted libraries, then main, then others.
sInsertedDylibCount = sAllImages.size()-1;

// 链接主程序
gLinkContext.linkingMainExecutable = true;
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}

// 链接插入的动态库
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing();
}
}

// <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
for (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image->inSharedCache() )
continue;
image->registerInterposing();
}
#if SUPPORT_ACCELERATE_TABLES
if ( (sAllCacheImagesProxy != NULL) && ImageLoader::haveInterposingTuples() ) {
// Accelerator tables cannot be used with implicit interposing, so relaunch with accelerator tables disabled
ImageLoader::clearInterposingTuples();
// unmap all loaded dylibs (but not main executable)
for (long i=1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image == sMainExecutable )
continue;
if ( image == sAllCacheImagesProxy )
continue;
image->setCanUnload();
ImageLoader::deleteImage(image);
}
// note: we don't need to worry about inserted images because if DYLD_INSERT_LIBRARIES was set we would not be using the accelerator table
sAllImages.clear();
sImageRoots.clear();
sImageFilesNeedingTermination.clear();
sImageFilesNeedingDOFUnregistration.clear();
sAddImageCallbacks.clear();
sRemoveImageCallbacks.clear();
sDisableAcceleratorTables = true;
sAllCacheImagesProxy = NULL;
sMappedRangesStart = NULL;
mainExcutableAlreadyRebased = true;
gLinkContext.linkingMainExecutable = false;
resetAllImages();
goto reloadAllImages;
}
#endif

// apply interposing to initial set of images
for(int i=0; i < sImageRoots.size(); ++i) {
sImageRoots[i]->applyInterposing(gLinkContext);
}
gLinkContext.linkingMainExecutable = false;

// <rdar://problem/12186933> do weak binding only after all inserted images linked
// 弱符号绑定
sMainExecutable->weakBind(gLinkContext);


CRSetCrashLogMessage("dyld: launch, running initializers");
// 初始化
initializeMainExecutable();

// notify any montoring proccesses that this process is about to enter main()
dyld3::kdebug_trace_dyld_signpost(DBG_DYLD_SIGNPOST_START_MAIN_DYLD2, 0, 0);
notifyMonitoringDyldMain();

// 寻找主程序入口点
result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getMain();
*startGlue = 0;
}
}
catch(const char* message) {
syncAllImages();
halt(message);
}
catch(...) {
dyld::log("dyld: launch failed\n");
}

CRSetCrashLogMessage(NULL);

if (sSkipMain) {
dyld3::kdebug_trace_dyld_signpost(DBG_DYLD_SIGNPOST_START_MAIN, 0, 0);
result = (uintptr_t)&fake_main;
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
}

return result;
}

  主要的步骤如下:

  • 设置上下文运行环境
  • 加载共享缓存库
  • 初始化主程序
  • 加载插入的动态库
  • 链接主程序
  • 链接插入的动态库
  • 初始化主程序
  • 寻找主程序入口点
  • 进入主程序入口点

加载共享缓存库:mapSharedCache

  我们需要知道,像每个app自带的动态库,比如libobj或者libdispatch,都是被映射到在一个共享区,每个app都是从这里读取动态库的内容。这样就可以大大节省了内存空间。

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
static void mapSharedCache()
{
dyld3::SharedCacheOptions opts;
opts.cacheDirOverride = sSharedCacheOverrideDir;
opts.forcePrivate = (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);
opts.useHaswell = sHaswell;
opts.verbose = gLinkContext.verboseMapping;
// 加载动态库缓存
loadDyldCache(opts, &sSharedCacheLoadInfo);

// update global state
if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
dyld::gProcessInfo->processDetachedFromSharedRegion = opts.forcePrivate;
dyld::gProcessInfo->sharedCacheSlide = sSharedCacheLoadInfo.slide;
dyld::gProcessInfo->sharedCacheBaseAddress = (unsigned long)sSharedCacheLoadInfo.loadAddress;
sSharedCacheLoadInfo.loadAddress->getUUID(dyld::gProcessInfo->sharedCacheUUID);
dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_SHARED_CACHE_A, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, {{ 0, 0 }}, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
}
}

bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
results->loadAddress = 0;
results->slide = 0;
results->cachedDylibsGroup = nullptr;
results->errorMessage = nullptr;

if ( options.forcePrivate ) {
// mmap cache into this process only
return mapCachePrivate(options, results);
}
else {
// 已经映射到了共享区域了,直接将它在共享内存中的内存地址映射到进程的内存地址空间
if ( reuseExistingCache(options, results) )
return (results->errorMessage != nullptr);

// 如果是第一个程序刚刚启动,共享区其实没内容的,需要将库映射到共享区
return mapCacheSystemWide(options, results);
}
}

初始化主程序:instantiateFromLoadedImage

  主要工作就是创建一个装在主程序的映像加载器(ImageLoader)。主要流程就三步:

  • 检查主程序运行的CPU架构与当前设备的CPU架构是否匹配
  • 实例化一个ImageLoader
  • 把ImageLoader添加到一个管理表中
1
2
3
4
5
6
7
8
9
10
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}

throw "main executable not a known format";
}

  主要看下instantiateMainExecutable的实现

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
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
// 判断主程序是否压缩的,现在基本的程序都是压缩的
// 判断方式通过段类型为LC_DYLD_INFO和LC_DYLD_INFO_ONLY的信息
// switch (cmd->cmd) {
// case LC_DYLD_INFO:
// case LC_DYLD_INFO_ONLY:
// if ( cmd->cmdsize != sizeof(dyld_info_command) )
// throw "malformed mach-o image: LC_DYLD_INFO size wrong";
// dyldInfoCmd = (struct dyld_info_command*)cmd;
// *compressed = true;
// break;
// .......
// }
// ......
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// 根据load commands的内容来实例化具体类,这里返回一个ImageLoaderMachOCompressed对象
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path,
}

加载插入的动态库:loadInsertedDylib

  循环遍历DYLD_INSERT_LIBRARIES环境变量中指定的动态库列表

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
static void loadInsertedDylib(const char* path)
{
ImageLoader* image = NULL;
unsigned cacheIndex;
try {
LoadContext context;
.......
image = load(path, context, cacheIndex);
}
......
}

//
ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex)
{
......
ImageLoader* image = loadPhase0(path, orgPath, context, cacheIndex, NULL);
......
}

// 从DYLD_ROOT_PATH路径进行查找
static ImageLoader* loadPhase0(const char* path, const char* orgPath, const LoadContext& context, unsigned& cacheIndex, std::vector<const char*>* exceptions)
{
#if SUPPORT_ROOT_PATH
// handle DYLD_ROOT_PATH which forces absolute paths to use a new root
if ( (gLinkContext.rootPaths != NULL) && (path[0] == '/') ) {
for(const char* const* rootPath = gLinkContext.rootPaths ; *rootPath != NULL; ++rootPath) {
char newPath[strlen(*rootPath) + strlen(path)+2];
strcpy(newPath, *rootPath);
strcat(newPath, path);
ImageLoader* image = loadPhase1(newPath, orgPath, context, cacheIndex, exceptions);
if ( image != NULL )
return image;
}
}
#endif

// try raw path
return loadPhase1(path, orgPath, context, cacheIndex, exceptions);
}

// loadPhase1从LD_LIBRARY_PATH路径进行查找
// loadPhase2从executable_path路径进行查找
// loadPhase3 ~ loadPhase4类似的路径查找
static ImageLoader* loadPhase5(const char* path, const char* orgPath, const LoadContext& context, unsigned& cacheIndex, std::vector<const char*>* exceptions)
{
for (std::vector<DylibOverride>::iterator it = sDylibOverrides.begin(); it != sDylibOverrides.end(); ++it) {
if ( strcmp(it->installName, path) == 0 ) {
path = it->override;
break;
}
}

if ( exceptions != NULL )
// 尝试打开,如果在共享区,直接返回该动态库对应的加载器(ImageLoader),如果不是则在硬盘区,则尝试打开,如果能打开调用loadPhase6将动态库映射到一个加载器
return loadPhase5load(path, orgPath, context, cacheIndex, exceptions);
else
// 检查是否已经存在,如果已经存在直接返回
return loadPhase5check(path, orgPath, context);
}

static ImageLoader* loadPhase6(int fd, const struct stat& stat_buf, const char* path, const LoadContext& context)
{
......
if ( isCompatibleMachO(firstPages, path) ) {

// 检查MACH-0类型,只有MH_BUNDLE, MH_DYLIB, 和一些MH_EXECUTE类型才可以被动态加载
const mach_header* mh = (mach_header*)firstPages;
switch ( mh->filetype ) {
case MH_EXECUTE:
case MH_DYLIB:
case MH_BUNDLE:
break;
default:
throw "mach-o, but wrong filetype";
}
......
// 初始化一个映像加载器
ImageLoader* image = ImageLoaderMachO::instantiateFromFile(path, fd, firstPages, headerAndLoadCommandsSize, fileOffset, fileLength, stat_buf, gLinkContext);

// 添加到全局管理
return checkandAddImage(image, context);
}
......
}

链接主程序:link

  这步主要加载所有的动态库,符号绑定等。

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
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{

// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);

uint64_t t0 = mach_absolute_time();
// 递归加载所有动态库
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);

// we only do the loading step for preflights
if ( preflightOnly )
return;

uint64_t t1 = mach_absolute_time();
context.clearAllDepths();
// 刷新库依赖的层级。层级越深,depth越大
this->recursiveUpdateDepth(context.imageCount());

uint64_t t2 = mach_absolute_time();
// 递归修正自己和加载动态库的基地址
this->recursiveRebase(context);
context.notifyBatch(dyld_image_state_rebased, false);

uint64_t t3 = mach_absolute_time();
// 对no-lazy符号进行绑定,修正那些指向其他二进制文件所包含的符号的指针
// lazy在运行时绑定。
this->recursiveBind(context, forceLazysBound, neverUnload);

uint64_t t4 = mach_absolute_time();
// 弱符号绑定
if ( !context.linkingMainExecutable )
this->weakBind(context);
uint64_t t5 = mach_absolute_time();

context.notifyBatch(dyld_image_state_bound, false);
uint64_t t6 = mach_absolute_time();

std::vector<DOFInfo> dofs;
// 注册程序的DOF节区,供dtrace使用
this->recursiveGetDOFSections(context, dofs);
context.registerDOFs(dofs);
uint64_t t7 = mach_absolute_time();

// interpose any dynamically loaded images
if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
this->recursiveApplyInterposing(context);
}

// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);

fgTotalLoadLibrariesTime += t1 - t0;
fgTotalRebaseTime += t3 - t2;
fgTotalBindTime += t4 - t3;
fgTotalWeakBindTime += t5 - t4;
fgTotalDOF += t7 - t6;

// done with initial dylib loads
fgNextPIEDylibAddress = 0;
}

  继续看Mach-O格式图,我们可以看到text段下有__stubs和__stb_helper,以及data段下有__nl_symbol_ptr和__la_symbol_ptr。

Mach-O示意图

  __nl_symbol_ptr和__la_symbol_ptr 分别表示non lazy binding指针表和lazy binding指针表,这两个指针表分别保存的是字符串标对应的函数地址。

  我们通过一个例子来了解__stubs、__stb_helper和__nl_symbol_ptr、__la_symbol_ptr 之间的关系。测试代码如下

1
2
3
4
5
int main(int argc, char * argv[]) {
printf("测试1");
printf("测试2");
return 0;
}

  在第一个printf打下断点,进入汇编模式进行查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
testData`main:
0x1000ca618 <+0>: sub sp, sp, #0x30 ; =0x30
0x1000ca61c <+4>: stp x29, x30, [sp, #0x20]
0x1000ca620 <+8>: add x29, sp, #0x20 ; =0x20
0x1000ca624 <+12>: stur wzr, [x29, #-0x4]
0x1000ca628 <+16>: stur w0, [x29, #-0x8]
0x1000ca62c <+20>: str x1, [sp, #0x10]
-> 0x1000ca630 <+24>: adrp x0, 4
0x1000ca634 <+28>: add x0, x0, #0x871 ; =0x871
; 跳入__stubs区
0x1000ca638 <+32>: bl 0x1000cc67c ; symbol stub for: printf

// 查看0x1000cc67c处内存内容是什么
// 的确是跳入stubs区
(lldb) image lookup --address 0x1000cc67c
Address: testData[0x000000010000867c] (testData.__TEXT.__stubs + 540)
Summary: testData`symbol stub for: printf

  给0x1000cc67c下断点,继续看进入stub做什么了

1
2
3
4
5
6
7
8
9
10
testData`printf:
-> 0x1000cc67c <+0>: nop
; 跳入0x00000001000cc934,即进入stub_helper
0x1000cc680 <+4>: ldr x16, #0x3b70 ; (void *)0x0000000100080934
0x1000cc684 <+8>: br x16

// 查看0x00000001000cc934内容,发现进入了stub_helper
(lldb) image lookup --address 0x0000000100080934
Address: testData[0x0000000100008934] (testData.__TEXT.__stub_helper + 588)
Summary:

  0x0000000100080934 - 0x0000000000078000 = 0x0000000100008934 ,而这个0x0000000100008934在Mach-O的位置,就是__la_symbol_ptr 内指向printf位置的地址

Mach-O示意图

  继续给0x0000000100080934 下断点,查看后面指令

1
2
3
4
5
6
->  0x1000cc934: ldr    w16, 0x1000cc93c
0x1000cc938: b 0x1000cc6e8

(lldb) image lookup --address 0x1001046e8
Address: testData[0x00000001000086e8] (testData.__TEXT.__stub_helper + 0)
Summary:

  0x1000cc6e8处下断点

1
2
3
4
5
6
7
8
9
0x1001046e8: adr    x17, #0x3998              ; (void *)0x00000001200da038: initialPoolContent + 2856
0x1001046ec: nop
0x1001046f0: stp x16, x17, [sp, #-0x10]!
0x1001046f4: nop
;跳入dyld_stub_binder函数
0x1001046f8: ldr x16, #0x3980 ; (void *)0x00000001944e915c: dyld_stub_binder
0x1001046fc: br x16
0x100104700: ldr w16, 0x100104708
0x100104704: b 0x1001046e8

  dyld_stub_binder函数是个汇编函数,它函数地址其实是从__nl_symbol_ptr取到的,在ARM中__nl_symbol_ptr就是__got。

Mach-O示意图

  总结,第一次访问printf符号的时候先去stub,stub告诉从__la_symbol_ptr查找,__la_symbol_ptr表示还没有printf符号真实函数地址,需要动态绑定,于是去__nl_symbol_ptr查找dyld_stub_binder函数的地址,进行查找真实的printf地址。找到后调用printf函数,并把这个地址保存进__la_symbol_ptr。下次调用printf函数的时候在__la_symbol_ptr就能得到真实地址进行跳转。

初始化主程序:initializeMainExecutable

  初始化主程序和以及其相关的模块,比如动态库。这时候就会执行以前文章说的libobjc库

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
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;

// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}

// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);

// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}

void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
uint64_t t1 = mach_absolute_time();
mach_port_t thisThread = mach_thread_self();
ImageLoader::UninitedUpwards up;
up.count = 1;
up.images[0] = this;
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized, false);
mach_port_deallocate(mach_task_self(), thisThread);
uint64_t t2 = mach_absolute_time();
fgTotalInitTime += (t2 - t1);
}

void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// 递归初始化所有的映像加载器内的数据,Load方法也在这里被初始化
for (uintptr_t i=0; i < images.count; ++i) {
images.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}

寻找主程序入口点:getThreadPC

  在LC_MAIN段,程序入口点的保存位置是程序的头的起始位置 + 段记录的偏移地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void* ImageLoaderMachO::getThreadPC() const
{
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
if ( cmd->cmd == LC_MAIN ) {
entry_point_command* mainCmd = (entry_point_command*)cmd;
void* entry = (void*)(mainCmd->entryoff + (char*)fMachOData);
// <rdar://problem/8543820&9228031> verify entry point is in image
if ( this->containsAddress(entry) )
return entry;
else
throw "LC_MAIN entryoff is out of range";
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
return NULL;
}

iOS的内存管理

发表于 2018-05-26 | 分类于 iOS

0x01 Tagged Pointer

  当我们定义一个变量的时候,比如NSNumber *a = @123,内存中的分布如下

变量内存示意图

  这样就造成了资源浪费,定义一个变量,保存简单的123却用了16个字节,造成了内存碎片;而且为了维护它,还需要额外的开销用引用计数管理它。所以在64系统中,苹果为了解决这样的问题,提出了Tagged Pointer概念。

  Tagged Pointer直接将数据保存到指针中,其指针格式变为tag + data的形式,一般用于NSNumber、NSString、NSDate。我们以NSString为例,看下什么是Tagged Pointer。

  首先定义变量如下

1
2
3
4
5
NSString *a = [NSString stringWithFormat:@"%@", @"123"];
NSString *b = [NSString stringWithFormat:@"%@", @"1234567890123456"];

// 不能下面这样定义,这样定义的@"123"是常量
NSString *a = @"123";

  我们看到变量a的类型变为NSTaggedPointerString

NSTaggedPointerString示意图

  我们再打印下内存地址看下,很明显变量a的地址由33 + 32 + 31 + 字符串长度组成,其中3表示字符串类型的tag,后面的3、2、1就是保存的值。而变量b的地址很明显是个堆地址。

1
2
3
4
(lldb) p a
(NSTaggedPointerString *) $0 = 0xa000000003332313 @"123"
(lldb) p b
(__NSCFString *) $1 = 0x00000001d004b250 @"1234567890123456"

  那么,下面的代码会有什么问题?

1
2
3
4
5
6
7
8
9
10
11
@property (nonatomic, copy) NSString *name;

- (void)viewDidLoad {
[super viewDidLoad];

for (NSInteger i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.name = [NSString stringWithFormat:@"%@", @"123456789012"];
});
}
}

  我们会看到闪退,这是因为多线程环境下,调用setter方法的时候,可能多个线程同时进入到if语句内,导致同一个对象在引用计数只有1的情况下,release方法被重复调用,从而造成闪退。

1
2
3
4
5
6
- (void)setName:(NSString *)name {
if(_name != name) {
[_name release];
_name = [name retain];
}
}

  那么同样的,下面的代码会闪退吗?

1
2
3
4
5
6
7
8
9
10
11
@property (nonatomic, copy) NSString *name;

- (void)viewDidLoad {
[super viewDidLoad];

for (NSInteger i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.name = [NSString stringWithFormat:@"%@", @"123"];
});
}
}

  答案是不会,因为self.name此时是Tagged Pointer,不参与引用计数管理,其retainCount可以认为是个无限值,所以任由你release,能闪退算它输。

  我们可以通过CFGetRetainCount计算retainCount来证实下。

1
2
3
4
5
6
7
8
9
for (NSInteger i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.name = [NSString stringWithFormat:@"%@", @"123"];
CFIndex idx = CFGetRetainCount((__bridge CFTypeRef)(self.name));
});
}

// 打印的值
(CFIndex) idx = 9223372036854775807

  正是因为这样的特性,同样我们在下面的代码,打印变量a的值还是123,而不是nil。

1
2
3
4
5
__weak NSString *a = nil;
@autoreleasepool {
a = [NSString stringWithFormat:@"%@", @"123"];
}
NSLog(@"%@", a);

  那么,我们看下系统是怎么判断是不是Tagged Pointer。

1
2
3
4
static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) 
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

  对于宏_OBJC_TAG_MASK定义又如下

1
2
3
4
5
6
7
8
9
10
11
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL

// 对于OBJC_MSB_TAGGED_POINTERS定义如下
#if TARGET_OS_OSX && __x86_64__
# define OBJC_MSB_TAGGED_POINTERS 0 // macOS
#else
# define OBJC_MSB_TAGGED_POINTERS 1 // iOS或者模拟器
#endif

  所以我们可以看到,当时macOS系统的时候,就拿地址最后一位是不是1来判断是不是taggedPoint;而对于iOS来说,则判断最高位是不是为1,就拿之前例子里的0xa000000003332313为例,0xa表示为1010,最高位显然为1,所以是这个是TaggedPointer。

0x02 weak

  首先,看下如下测试代码

1
2
3
4
5
int main(int argc, char * argv[]) {
__weak NSObject *obj = [NSObject new];

return 0;
}

  接着,我们看下其汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
testData`main:
0x102e1d808 <+0>: sub sp, sp, #0x40 ; =0x40
0x102e1d80c <+4>: stp x29, x30, [sp, #0x30]
0x102e1d810 <+8>: add x29, sp, #0x30 ; =0x30
0x102e1d814 <+12>: adrp x8, 9
0x102e1d818 <+16>: add x8, x8, #0xf8 ; =0xf8
0x102e1d81c <+20>: adrp x9, 8
0x102e1d820 <+24>: add x9, x9, #0xf38 ; =0xf38
0x102e1d824 <+28>: adrp x10, 7
0x102e1d828 <+32>: ldr x10, [x10, #0x68]
0x102e1d82c <+36>: add x11, sp, #0x18 ; =0x18
0x102e1d830 <+40>: stur wzr, [x29, #-0x4]
0x102e1d834 <+44>: stur w0, [x29, #-0x8]
0x102e1d838 <+48>: stur x1, [x29, #-0x10]
-> 0x102e1d83c <+52>: ldr x0, [x8]
0x102e1d840 <+56>: ldr x1, [x9]
0x102e1d844 <+60>: str x11, [sp, #0x10]
0x102e1d848 <+64>: blr x10
0x102e1d84c <+68>: ldr x8, [sp, #0x10]
0x102e1d850 <+72>: str x0, [sp, #0x8]
0x102e1d854 <+76>: mov x0, x8
0x102e1d858 <+80>: ldr x1, [sp, #0x8]
0x102e1d85c <+84>: bl 0x102e20e68 ; symbol stub for: objc_initWeak
0x102e1d860 <+88>: ldr x1, [sp, #0x8]
0x102e1d864 <+92>: mov x0, x1
0x102e1d868 <+96>: bl 0x102e20eb0 ; symbol stub for: objc_release
0x102e1d86c <+100>: stur wzr, [x29, #-0x4]
0x102e1d870 <+104>: ldr x0, [sp, #0x10]
0x102e1d874 <+108>: bl 0x102e20e14 ; symbol stub for: objc_destroyWeak
0x102e1d878 <+112>: ldur w0, [x29, #-0x4]
0x102e1d87c <+116>: ldp x29, x30, [sp, #0x30]
0x102e1d880 <+120>: add sp, sp, #0x40 ; =0x40
0x102e1d884 <+124>: ret

  可以发现,声明__weak后其内部实现过程中调用了objc_initWeak和objc_destroyWeak。但是如果测试例子再修改如下:

1
2
3
4
5
6
int main(int argc, char * argv[]) {
__weak NSObject *obj = [NSObject new];
obj = [NSObject new];

return 0;
}

  汇编代码变化如下,发现再给__weak声明的变量重新赋值的时候调用的是objc_storeWeak

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
testData`main:
0x102cc5778 <+0>: sub sp, sp, #0x70 ; =0x70
0x102cc577c <+4>: stp x29, x30, [sp, #0x60]
0x102cc5780 <+8>: add x29, sp, #0x60 ; =0x60
0x102cc5784 <+12>: adrp x8, 9
0x102cc5788 <+16>: add x8, x8, #0xf8 ; =0xf8
0x102cc578c <+20>: adrp x9, 8
0x102cc5790 <+24>: add x9, x9, #0xf38 ; =0xf38
0x102cc5794 <+28>: adrp x10, 7
0x102cc5798 <+32>: ldr x10, [x10, #0x68]
0x102cc579c <+36>: sub x11, x29, #0x18 ; =0x18
0x102cc57a0 <+40>: stur wzr, [x29, #-0x4]
0x102cc57a4 <+44>: stur w0, [x29, #-0x8]
0x102cc57a8 <+48>: stur x1, [x29, #-0x10]
-> 0x102cc57ac <+52>: ldr x0, [x8]
0x102cc57b0 <+56>: ldr x1, [x9]
0x102cc57b4 <+60>: str x11, [sp, #0x30]
0x102cc57b8 <+64>: str x10, [sp, #0x28]
0x102cc57bc <+68>: str x8, [sp, #0x20]
0x102cc57c0 <+72>: str x9, [sp, #0x18]
0x102cc57c4 <+76>: blr x10
0x102cc57c8 <+80>: ldr x8, [sp, #0x30]
0x102cc57cc <+84>: str x0, [sp, #0x10]
0x102cc57d0 <+88>: mov x0, x8
0x102cc57d4 <+92>: ldr x1, [sp, #0x10]
0x102cc57d8 <+96>: bl 0x102cc8e44 ; symbol stub for: objc_initWeak
0x102cc57dc <+100>: ldr x1, [sp, #0x10]
0x102cc57e0 <+104>: mov x0, x1
0x102cc57e4 <+108>: bl 0x102cc8e8c ; symbol stub for: objc_release
0x102cc57e8 <+112>: ldr x8, [sp, #0x20]
0x102cc57ec <+116>: ldr x0, [x8]
0x102cc57f0 <+120>: ldr x9, [sp, #0x18]
0x102cc57f4 <+124>: ldr x1, [x9]
0x102cc57f8 <+128>: ldr x10, [sp, #0x28]
0x102cc57fc <+132>: blr x10
0x102cc5800 <+136>: str x0, [sp, #0x8]
0x102cc5804 <+140>: b 0x102cc5808 ; <+144> at main.m
0x102cc5808 <+144>: sub x8, x29, #0x18 ; =0x18
0x102cc580c <+148>: mov x0, x8
0x102cc5810 <+152>: ldr x1, [sp, #0x8]
0x102cc5814 <+156>: str x8, [sp]
0x102cc5818 <+160>: bl 0x102cc8ee0 ; symbol stub for: objc_storeWeak
0x102cc581c <+164>: ldr x1, [sp, #0x8]
0x102cc5820 <+168>: mov x0, x1
0x102cc5824 <+172>: bl 0x102cc8e8c ; symbol stub for: objc_release
0x102cc5828 <+176>: stur wzr, [x29, #-0x4]
0x102cc582c <+180>: ldr x0, [sp]
0x102cc5830 <+184>: bl 0x102cc8df0 ; symbol stub for: objc_destroyWeak
0x102cc5834 <+188>: ldur w0, [x29, #-0x4]
0x102cc5838 <+192>: ldp x29, x30, [sp, #0x60]
0x102cc583c <+196>: add sp, sp, #0x70 ; =0x70
0x102cc5840 <+200>: ret
0x102cc5844 <+204>: sub x8, x29, #0x18 ; =0x18
0x102cc5848 <+208>: mov x9, x1
0x102cc584c <+212>: stur x0, [x29, #-0x20]
0x102cc5850 <+216>: stur w9, [x29, #-0x24]
0x102cc5854 <+220>: mov x0, x8
0x102cc5858 <+224>: bl 0x102cc8df0 ; symbol stub for: objc_destroyWeak
0x102cc585c <+228>: ldur x0, [x29, #-0x20]
0x102cc5860 <+232>: bl 0x102cc8c94 ; symbol stub for: _Unwind_Resume

  查看源码比较下objc_initWeak和objc_storeWeak的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}

return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}

id objc_storeWeak(id *location, id newObj)
{
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}

  虽然都是统一调用storeWeak函数,两者之间只有一个区分,就是objc_initWeak是没有旧值的,而objc_storeWeak是有旧值的,即DontHaveOld和DoHaveOld的区别。

  最后会调用objc_destroyWeak函数进行销毁所有指向对象的弱引用对象。销毁的时候,告诉storeWeak函数没有新值。

1
2
3
4
5
void objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}

storeWeak

  我们直接来到源码,看下其实现

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

template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);

Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;

retry:
// 如果是旧值,拿到旧值的那个哈希表
if (haveOld) {
// *location 赋值给oldObj
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
// 如果是新值,拿到新值的那个哈希表
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}

SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
// 如果是旧值。之前*location赋值给过oldObj,如果不一致,说明有别的线程进来改过了,则重新尝试。
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}

// 如果是新值则需要判断有没有调用过initialized方法,如果没有这里进行调用
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));

previouslyInitializedClass = cls;

goto retry;
}
}

// 清理旧值,解除注册
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}

// 注册新值
if (haveNew) {
newObj = (objc_object *)
// newObject为所赋的值。比如例子中的[NSObject new], location为例子中的NSObject *obj
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);

// 设置isa的weakly_referenced位为true
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}

*location = (id)newObj;
}

SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

return (id)newObj;
}

SlideTable

  定义如下

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
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; // 引用的数量
weak_table_t weak_table; // 弱引用表,保存着所有的关联着对象的弱引用地址

... ...
}

struct weak_table_t {
weak_entry_t *weak_entries; // 上面说的保存的地址就是保存在这里
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};

struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers; // 关联着对象的弱引用地址保存在这里
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};

bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}

weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}

weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};

  具体解释是这样的,在我们的例子中__weak NSObject *obj = [NSObject new];,[NSObject new]新构造了一个对象,这个对象有个SlideTable维护弱引用表,表里保存的就是NSObject *obj变量的地址。

weak_unregister_no_lock

  源码先行

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
void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
id *referrer_id)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;

weak_entry_t *entry;

if (!referent) return;

if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 解除引用关联,referer为引用对象的地址,即例子中的NSObject *obj变量的地址
remove_referrer(entry, referrer);
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0) {
empty = false;
}
else {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i]) {
empty = false;
break;
}
}
}
// 如果弱引用对象没有关联的对象了,表也可以删了
if (empty) {
weak_entry_remove(weak_table, entry);
}
}
}

  具体如何解除引用过程如下,

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 void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
......

size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
// 入参old_referrer在我们的例子中就是NSObject *obj变量的地址。然后查找表中的每个引用数据,如果找到的数据跟NSObject *obj变量的地址相等,就将其置为nil
while (entry->referrers[index] != old_referrer) {
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
hash_displacement++;
if (hash_displacement > entry->max_hash_displacement) {
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
}
entry->referrers[index] = nil;
entry->num_refs--;
}

weak_register_no_lock

  还是先看源码

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
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;

if (!referent || referent->isTaggedPointer()) return referent_id;

// ensure that the referenced object is viable
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}

if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}


weak_entry_t *entry;
// 结合例子来说,就是找到[NSObject new]新创建出对象的weak_entry_t,然后把NSObject *obj变量的地址加入到这个表中,这样做的目的就是,当[NSObject new]的对象销毁后,只要遍历这个对象里的弱引用表,就可以把所有指向这块内存的变量地址都可以置nil。
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}

return referent_id;
}

object_dispose

  当一个对象销毁时,会调用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
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
id object_dispose(id obj)
{
if (!obj) return nil;

objc_destructInstance(obj);
free(obj);

return nil;
}

void *objc_destructInstance(id obj)
{
if (obj) {
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// 是否c++,是的话调用析构函数
if (cxx) object_cxxDestruct(obj);
// 是否有关联属性,是的话移出关联属性
if (assoc) _object_remove_assocations(obj);
// 清理并销毁对象
obj->clearDeallocating();
}

return obj;
}

inline void objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// 如果是弱引用对象
clearDeallocating_slow();
}

assert(!sidetable_present());
}

void objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));

SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
table.unlock();
}

  最终如果是弱引用对象会来到weak_clear_no_lock函数,里面遍历所有指向此对象的对象地址,然后都置为nil。

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
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
objc_object *referent = (objc_object *)referent_id;

weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
return;
}

weak_referrer_t *referrers;
size_t count;

if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}

for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}

weak_entry_remove(weak_table, entry);
}

0x03 Copy

NSString

  首先,看下NSString相关的例子

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char * argv[]) {
NSString *a = [NSString stringWithFormat:@"1234567890"];
NSString *b = [a copy];
NSString *c = [a mutableCopy];

NSLog(@"%p -- %p -- %p", a, b, c);

return 0;
}

// 打印出的结果
0x1d02202c0 -- 0x1d02202c0 -- 0x1d0445cd0

  我们发现对于不可变的NSString,copy出来的是不可变对象,并且是浅拷贝,因为地址相同;mutableCopy出来的是可变对象,而且是深拷贝,地址都不同了。

NSMutableString

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char * argv[]) {
NSMutableString *a = [[NSMutableString alloc] initWithString:@"1234567890"];
NSString *b = [a copy];
NSMutableString *c = [a mutableCopy];

NSLog(@"%p -- %p -- %p", a, b, c);

return 0;
}

// 打印出的结果
0x1d425d5e0 -- 0x1d422bfa0 -- 0x1d425d5b0

  对于可变的NSMutableString,copy出来的是不可变对象,并且是深拷贝;mutableCopy出来的是可变对象,但也是深拷贝。

  NSArray、NSMutableArray、NSDictionay、NSMutableDictionary其实也类似,具体例子不表述了,总结规律如下:

  • 不可变对象的copy出来的是不可变对象,且是浅拷贝;mutableCopy的是可变对象,且是深拷贝。
  • 可变对象的copy出来的是不可变对象,但是深拷贝;mutableCopy的是可变对象,且也是深拷贝

0x04 NSProxy

  在定时器相关的初始化方法中,如果使用的是下面这样的初始化方法,那么由于控制器self本身强引用定时器,而target又会把self传入定时器内部,而在定时器内部又会对self做强引用,这样就造成了循环引用。

1
2
3
self.timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerFire) userInfo:nil repeats:YES];

self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkFire)];

  解决方法,使用NSProxy,通过这个中间介打破强引用关系,然后再利用消息转发,将方法的实现还是转发回原来的target上。

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
@interface MyProxy : NSProxy

@property (nonatomic, weak) id target;

+ (id)proxyWithTarget:(id)target;

@end

@implementation MyProxy

+ (id)proxyWithTarget:(id)target {
MyProxy *proxy = [MyProxy alloc];
proxy.target = target;
return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}

@end

// 使用自定义的Proxy
self.timer = [NSTimer timerWithTimeInterval:1.f target:[MyProxy proxyWithTarget:self] selector:@selector(timerFire) userInfo:nil repeats:YES];

self.displayLink = [CADisplayLink displayLinkWithTarget:[MyProxy proxyWithTarget:self] selector:@selector(displayLinkFire)];

  那么为什么不是继承NSObject呢,这是因为这样就会走一遍消息转发流程,具体在runtime篇已经讲过,效率没有直接使用NSProxy高。

  当然,这样的使用场景也不止在定时器上,别的地方遇到类似的互相强引用,也可以使用这样的方法解决。

0x05 autoreleasepool

  我们的示例代码如下

1
2
3
4
5
6
7
8
int main(int argc, char * argv[]) {
@autoreleasepool {
for (NSInteger i = 0; i < 100; i++) {
NSObject *obj = [NSObject new];
}
}

return 0;

  转换汇编后,我们可以看到@autoreleasepool实际会被转换为objc_autoreleasePoolPush和objc_autoreleasePoolPop两个函数,而@autoreleasepool中间的代码则被包在这两个函数中间。

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
testData`main:
0x10019d7d0 <+0>: sub sp, sp, #0x70 ; =0x70
0x10019d7d4 <+4>: stp x29, x30, [sp, #0x60]
0x10019d7d8 <+8>: add x29, sp, #0x60 ; =0x60
0x10019d7dc <+12>: mov x8, #0x0
0x10019d7e0 <+16>: adrp x9, 9
0x10019d7e4 <+20>: add x9, x9, #0xf0 ; =0xf0
0x10019d7e8 <+24>: adrp x10, 8
0x10019d7ec <+28>: add x10, x10, #0xf30 ; =0xf30
0x10019d7f0 <+32>: adrp x11, 7
0x10019d7f4 <+36>: ldr x11, [x11, #0x68]
0x10019d7f8 <+40>: stur wzr, [x29, #-0x4]
0x10019d7fc <+44>: stur w0, [x29, #-0x8]
0x10019d800 <+48>: stur x1, [x29, #-0x10]
-> 0x10019d804 <+52>: stur x8, [x29, #-0x28]
0x10019d808 <+56>: str x10, [sp, #0x30]
0x10019d80c <+60>: str x11, [sp, #0x28]
0x10019d810 <+64>: str x9, [sp, #0x20]
0x10019d814 <+68>: bl 0x1001a0e14 ; symbol stub for: objc_autoreleasePoolPush
0x10019d818 <+72>: stur xzr, [x29, #-0x18]
0x10019d81c <+76>: str x0, [sp, #0x18]
0x10019d820 <+80>: mov x8, #0x64
0x10019d824 <+84>: ldur x9, [x29, #-0x18]
0x10019d828 <+88>: cmp x9, x8
0x10019d82c <+92>: cset w10, lt
0x10019d830 <+96>: tbnz w10, #0x0, 0x10019d838 ; <+104> at main.m
0x10019d834 <+100>: b 0x10019d87c ; <+172> at main.m
0x10019d838 <+104>: sub x0, x29, #0x20 ; =0x20
0x10019d83c <+108>: ldr x8, [sp, #0x20]
0x10019d840 <+112>: ldr x9, [x8]
0x10019d844 <+116>: ldr x10, [sp, #0x30]
0x10019d848 <+120>: ldr x1, [x10]
0x10019d84c <+124>: str x0, [sp, #0x10]
0x10019d850 <+128>: mov x0, x9
0x10019d854 <+132>: ldr x9, [sp, #0x28]
0x10019d858 <+136>: blr x9
0x10019d85c <+140>: stur x0, [x29, #-0x20]
0x10019d860 <+144>: ldr x0, [sp, #0x10]
0x10019d864 <+148>: ldur x1, [x29, #-0x28]
0x10019d868 <+152>: bl 0x1001a0f04 ; symbol stub for: objc_storeStrong
0x10019d86c <+156>: ldur x8, [x29, #-0x18]
0x10019d870 <+160>: add x8, x8, #0x1 ; =0x1
0x10019d874 <+164>: stur x8, [x29, #-0x18]
0x10019d878 <+168>: b 0x10019d820 ; <+80> at main.m
0x10019d87c <+172>: mov w0, #0x0
0x10019d880 <+176>: ldr x1, [sp, #0x18]
0x10019d884 <+180>: str w0, [sp, #0xc]
0x10019d888 <+184>: mov x0, x1
0x10019d88c <+188>: bl 0x1001a0e08 ; symbol stub for: objc_autoreleasePoolPop
0x10019d890 <+192>: ldr w0, [sp, #0xc]
0x10019d894 <+196>: ldp x29, x30, [sp, #0x60]
0x10019d898 <+200>: add sp, sp, #0x70 ; =0x70
0x10019d89c <+204>: ret

  而这两个函数的实现如下

1
2
3
4
5
6
7
8
9
void *objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}

  继续往下前,需要了解AutoreleasePoolPage的结构,定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AutoreleasePoolPage 
{
... ...
static size_t const SIZE = PAGE_MAX_SIZE;
magic_t const magic;
id *next; // 指向能存放“autorelease对象的地址”的地址
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;

... ...
}

  首先,看到这个结构中有parent和child成员,第一反应这个结构是双向链表。这个结构的大小为PAGE_MAX_SIZE,这是一个宏,其定义如下

1
2
3
#define PAGE_MAX_SIZE           PAGE_SIZE
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096

  一个AutoreleasePoolPage的大小为4096字节,除了用来存放其成员变量所占用的空间外,剩余的空间都是用来存放autorelease对象的地址。

  继续之前的源码分析,首先看下push方法的实现

1
2
3
4
5
6
7
8
9
10
static inline void *push() {
id *dest;
if (DebugPoolAllocation) {
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}

  push方法里面接着调用了autoreleaseFast,并且传入了一个POOL_BOUNDARY对象,从字面意思理解这个对象是自动释放池的边界,那么它具体指的是什么?我们看到它的宏定义如下,其实就是个nil

1
#   define POOL_BOUNDARY nil

  再继续往下看下源代码,首先看看有没有已经创建了的自动释放池,并且如果没满的话调用AutoreleasePoolPage的add方法,如果满了的话就调用autoreleaseFullPage,最后如果没有创建过自动释放池就新建一个,则调用autoreleaseNoPage。

1
2
3
4
5
6
7
8
9
10
static inline id *autoreleaseFast(id obj) {
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}

  首先看下add方法,直接将POOL_BOUNDARY加入进自动释放池中,并返回POOL_BOUNDARY的地址。

1
2
3
4
5
6
7
8
id *add(id obj) {
assert(!full());
unprotect();
id *ret = next;
*next++ = obj;
protect();
return ret;
}

  接着看autoreleaseFullPage,前面说过自动释放池是双链表形式存在的,所以先遍历表,找到空的链表,然后把对象加入这个池子中;但是如果没子链表,那么就新建一个。

1
2
3
4
5
6
7
8
9
10
11
12
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);

do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());

setHotPage(page);
return page->add(obj);
}

  最后,看下autoreleaseNoPage,也是简单明了的代码,直接新建一个自动释放池,并将其设置为hot,因为自动释放池肯定有很多个,当前活跃的可添加自动释放对象的自动释放池为hot,否则为cold。

1
2
3
4
5
6
7
8
9
10
id *autoreleaseNoPage(id obj)
{
... ...
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);

... ...

return page->add(obj);
}

  看完push方法源码,我们可以总结,当首次调用AutoreleasePoolPage的push方法时候,会将一个POOL_BOUNDARY入栈,并且会返回其存放的内存地址,这个地址也就是能存放自动释放对象的空间的首地址。所以,后面的autorelease对象的地址存放在这个POOL_BOUNDARY所在地址的后面。

AutoreleasePoolPage内存示意图

  如果4096的空间都被用完了,就会新创建一个AutoreleasePoolPage对象。

  看完push方法,接着看下pop方法,核心方法就是releaseUntil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline void pop(void *token) {
AutoreleasePoolPage *page;
id *stop;

... ...

page = pageForPointer(token);
stop = (id *)token;

... ...

// 对自动释放池内的对象调用release方法,直到遇到POOL_BOUNDARY
page->releaseUntil(stop);

// 释放自动释放池
page->kill();
}

  接着看releaseUntil方法,遍历池子,在找到POOL_BOUNDARY之前的对象,都调用release方法进行释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void releaseUntil(id *stop) {

while (this->next != stop) {
... ...

page->unprotect();
// 之前说过,next指向下一个可存放自动释放对象的地址,所以往前就是已经存放过的那些自动释放对象了
id obj = *--page->next;

··· ···

if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}

setHotPage(this);
}

  总结,最后调用pop方法的时候,会把这个POOL_BOUNDARY所在地址作为参数传进去。然后从整个链表中所存放的最后一个autorelease对象的地址开始,依次往前调用对象的release方法,直到遇到前面传进来的POOL_BOUNDARY所在地址。

  如果是嵌套autorelease,因为没遇到一次@autorelease{}都会加入一个POOL_BOUNDARY,所以pop的时候,遇到POOL_BOUNDARY就停止了。所以对于对象来说,在最里面嵌套里的对象最先被释放掉。

  我们举个例子,会调用到私有函数_objc_autoreleasePoolPrint来帮助我们完成这个例子,然后我们需要将工程配置成MRC。

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
extern void _objc_autoreleasePoolPrint();

int main(int argc, char * argv[]) {

@autoreleasepool {
Animal *a1 = [[Animal new] autorelease];
Animal *a2 = [[Animal new] autorelease];

@autoreleasepool {
Animal *a3 = [[Animal new] autorelease];
_objc_autoreleasePoolPrint(); // 打印自动释放池
}

NSLog(@"第二个释放池结束");
}

NSLog(@"第一个释放池结束");

return 0;
}

// 运行结果
objc[53031]: ##############
objc[53031]: AUTORELEASE POOLS for thread 0x112b03380
objc[53031]: 5 releases pending.
objc[53031]: [0x7fdff9002000] ................ PAGE (hot) (cold)
objc[53031]: [0x7fdff9002038] ################ POOL 0x7fdff9002038 // 第一个AutoReleasePool的POOL_BOUNDARY
objc[53031]: [0x7fdff9002040] 0x60c00001dca0 Animal
objc[53031]: [0x7fdff9002048] 0x60c00001dc90 Animal
objc[53031]: [0x7fdff9002050] ################ POOL 0x7fdff9002050 // 第二个AutoReleasePool的POOL_BOUNDARY
objc[53031]: [0x7fdff9002058] 0x60c00001dc80 Animal
objc[53031]: ##############
2018-07-20 19:49:15.576802+0800 testData[53031:3013783] -[Animal dealloc] // 第二个AutoReleasePool内的对象被释放
2018-07-20 19:49:15.577441+0800 testData[53031:3013783] 第二个释放池结束
2018-07-20 19:49:15.577533+0800 testData[53031:3013783] -[Animal dealloc] // 第一个AutoReleasePool内的对象被释放
2018-07-20 19:49:15.577645+0800 testData[53031:3013783] -[Animal dealloc]
2018-07-20 19:49:15.577768+0800 testData[53031:3013783] 第一个释放池结束

  对于释放时机,在@autorelease{}是出了大括号就会进行释放。

  那么,我们知道主线程中默认会自动开启一个RunLoop,所以在这种情况下,如果只是调用autorelease方法,释放时机是根据RunLoop的,比如像这样的情况。

1
2
3
4
5
- (void)viewDidLoad {
[super viewDidLoad];

Animal *a1 = [[Animal new] autorelease];
}

  在主线程中,RunLoop注册了2个Observer,第一个,监听kCFRunLoopEntry,调用objc_autoreleasePoolPush();第二个,监听了kCFRunLoopBeforeWaiting和kCFRunLoopBeforeExit,前者会调用objc_autoreleasePoolPop()和objc_autoreleasePoolPush(),后者会调用objc_autoreleasePoolPop()。

深入分析Runloop

发表于 2018-05-24 | 分类于 iOS

  一般写的程序都是像下面这样的,代码一行行执行,到return的时候代码都已经执行完毕,程序退出。

1
2
3
4
int main(int argc, char * argv[]) {
/* 代码段 */
return 0;
}

  但实际的应用中,我们的软件不可能像这样线性的执行,执行完我们的软件就退出,所以就需要一种技术,让我们的软件始终保持运行状态。在iOS系统中,这项技术就是Runloop。

0x01 主要的结构信息

1. CFRunloop

  Runloop都会对应一个线程。而且Runloop可以有多种模式,但是当前使用的只能是一种模式。我们的CFRunloop结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; // 对应的线程
uint32_t _winthread;
CFMutableSetRef _commonModes; // 保存的commonMode
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode; // 当前模式
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

2. CFRunLoopMode

  Mode管理着各种事件,我们的source0、source1、observer、timer都归mode管理。每个模式下又包含多个source0,source1,observer和timer。不同mode下的source0、source1、observer、timer都是隔离开的。

  如果Mode里面没有source0,source1,observer和timer,Runloop会立马退出。

  模式的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
// 需要处理的几种事件
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

CFMutableDictionaryRef _portToV1SourceMap; // mach_port_t对应的CFRunLoopSourceRef
__CFPortSet _portSet; // 所有要监听的mach_port_t
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};

3. CFRunLoopSource

  source分为source0和source1。

1
2
3
4
5
6
7
8
9
10
11
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};

  source0一般都是应用的内部事件,比如触摸事件、CFSocket等。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;

  source1一般与mach_port通信,所以接收的是内核态的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
mach_port_t (*getPort)(void *info); // 相比source0多了mach_port_t,即端口,用于线程之间通信
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
void * (*getPort)(void *info);
void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

4. CFRunLoopObserver

  观察runloop的各种状态。

1
2
3
4
5
6
7
8
9
10
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};

  可被观察的状态有如下几种

1
2
3
4
5
6
7
8
9
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 进入runloop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入睡眠
kCFRunLoopAfterWaiting = (1UL << 6), // 唤醒后
kCFRunLoopExit = (1UL << 7), // 退出runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

5. CFRunLoopTimer

  可以在设定的时间到达后触发回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

0x02 获取线程

1. CFRunLoopGetMain

  内部调用了_CFRunLoopGet0函数,传入的参数是主线程。

1
2
3
4
5
6
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}

2. CFRunLoopGetCurrent

  跟CFRunLoopGetMain函数一样,调用的同样是_CFRunLoopGet0函数,只是参数不同,这里的入参是当前所在线程。

1
2
3
4
5
6
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}

3. CFRunLoopGet0

  既然CFRunLoopGetMain和CFRunLoopGetCurrent都调用了_CFRunLoopGet0函数,我们看下这个函数的实现。

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
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 如果线程为空,默认视作主线程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
// __CFRunLoops是个CFMutableDictionaryRef类型的全局变量,用来保存RunLoop。这里首先判断有没有这个全局变量,如果没有就新创建一个这个全局变量,并同时创建一个主线程对应的runloop。
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 创建一个主线程对应的runloop。
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
// 交换dict和__CFRunLoops,也就是这里开始__CFRunLoops就是刚刚创建的那个全局变量
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 根据线程取其对应的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果这个线程没有对应的runloop,就新建立一个runloop对象
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
// 二次确认,是否的确没有该线程对应的runloop
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
// 如果的确没有对应的runloop,就保存进全局变量中
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
// 注册一个回调,当线程销毁时一同销毁对应的runloop
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}

  获取线程流程总结如下:

  • 有个全局变量保存着各个线程与各个runloop对象的关系,该变量初始化的时候会同时创建一个主线程对应的runloop对象。
  • 子线程的runloop默认是获取的时候才开始创建。所以多线程环境中,只有主线程的runloop是一开始就创建出来的,其他线程被创建的时候并不会一起创建一个runloop对象。
  • runloop对象的生命周期和线程的生命周期同步。

0x03 创建RunLoop

  上面我们的RunLoop是在获取的时候被创建的,创建源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
CFRunLoopRef loop = NULL;
CFRunLoopModeRef rlm;
uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
if (NULL == loop) {
return NULL;
}
(void)__CFRunLoopPushPerRunData(loop);
__CFRunLoopLockInit(&loop->_lock);
loop->_wakeUpPort = __CFPortAllocate();
if (CFPORT_NULL == loop->_wakeUpPort) HALT;
__CFRunLoopSetIgnoreWakeUps(loop);
loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
loop->_commonModeItems = NULL;
loop->_currentMode = NULL;
loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
loop->_blocks_head = NULL;
loop->_blocks_tail = NULL;
loop->_counterpart = NULL;
loop->_pthread = t;
loop->_winthread = 0;
rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
return loop;
}

static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create) {
CHECK_FOR_FORK();
CFRunLoopModeRef rlm;
struct __CFRunLoopMode srlm;
memset(&srlm, 0, sizeof(srlm));
_CFRuntimeSetInstanceTypeIDAndIsa(&srlm, __kCFRunLoopModeTypeID);
srlm._name = modeName;
rlm = (CFRunLoopModeRef)CFSetGetValue(rl->_modes, &srlm);
if (NULL != rlm) {
__CFRunLoopModeLock(rlm);
return rlm;
}
if (!create) {
return NULL;
}
rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL);
if (NULL == rlm) {
return NULL;
}
__CFRunLoopLockInit(&rlm->_lock);
rlm->_name = CFStringCreateCopy(kCFAllocatorSystemDefault, modeName);
rlm->_stopped = false;
rlm->_portToV1SourceMap = NULL;
rlm->_sources0 = NULL;
rlm->_sources1 = NULL;
rlm->_observers = NULL;
rlm->_timers = NULL;
rlm->_observerMask = 0;
rlm->_portSet = __CFPortSetAllocate();
rlm->_timerSoftDeadline = UINT64_MAX;
rlm->_timerHardDeadline = UINT64_MAX;

kern_return_t ret = KERN_SUCCESS;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
rlm->_timerFired = false;
// 新建一个runloop的队列,用来处理timer相关事件
rlm->_queue = _dispatch_runloop_root_queue_create_4CF("Run Loop Mode Queue", 0);
mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (queuePort == MACH_PORT_NULL) CRASH("*** Unable to create run loop mode queue port. (%d) ***", -1);
rlm->_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, rlm->_queue);

__block Boolean *timerFiredPointer = &(rlm->_timerFired);
dispatch_source_set_event_handler(rlm->_timerSource, ^{
*timerFiredPointer = true;
});

// Set timer to far out there. The unique leeway makes this timer easy to spot in debug output.
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 321);
dispatch_resume(rlm->_timerSource);

ret = __CFPortSetInsert(queuePort, rlm->_portSet);
if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);

#endif
#if USE_MK_TIMER_TOO
rlm->_timerPort = mk_timer_create();
ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif

ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret);

CFSetAddValue(rl->_modes, rlm);
CFRelease(rlm);
__CFRunLoopModeLock(rlm); /* return mode locked */
return rlm;
}

  构建一个mode的时候会同时创建一个GCD Queue,用来处理时间相关的任务。

0x04 运行RunLoop

  runloop内部其实就是一个do…while()循环。

1
2
3
4
5
6
7
void CFRunLoopRun(void) {	/* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

  调用的是CFRunLoopRunSpecific函数,用kCFRunLoopDefaultMode模式进行启动

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
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
// 根据模式名字,找到对应模式
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 如果没有找到这个模式或者这个模式里是空的(source0\source1\timers都为空),就退出runloop循环
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
// 取出runloop当前的模式
CFRunLoopModeRef previousMode = rl->_currentMode;
// 将runloop的模式改为传进来的默认模式
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;

// 通知observers要进入runloop了
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 主要工作内容还是在__CFRunLoopRun
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知observers要退出runloop了
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}

  看下最重要的__CFRunLoopRun函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
// 获取当前mach的时间,纳秒级精度
uint64_t startTSR = mach_absolute_time();

// 如果runloop暂停或者mode暂停,那么就退出循环
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}

// 如果是主线程并且是默认mode,将gcd获取主线程的端口赋值给dispatchPort,以便和主线程通信
mach_port_name_t dispatchPort = MACH_PORT_NULL;
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();

#if USE_DISPATCH_SOURCE_FOR_TIMERS
// 将gcd中根队列的端口赋值给modeQueuePort
mach_port_name_t modeQueuePort = MACH_PORT_NULL;
if (rlm->_queue) {
// cgd根队列管理着所有的子队列,获取根队列的端口,方便与其通信,来处理后面传递进队列的任务
modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (!modeQueuePort) {
CRASH("Unable to get port for run loop mode queue (%d)", -1);
}
}
#endif

// 看入参的超时时间,如果是在可取范围内的,就设置一个定时器,如果时间到了就跳到__CFRunLoopTimeout处理超时后要做的事。
dispatch_source_t timeout_timer = NULL;
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
if (seconds <= 0.0) { // instant timeout
seconds = 0.0;
timeout_context->termTSR = 0ULL;
} else if (seconds <= TIMER_INTERVAL_LIMIT) {
dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_retain(timeout_timer);
timeout_context->ds = timeout_timer;
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
dispatch_resume(timeout_timer);
} else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}

Boolean didDispatchPortLastTime = true;
int32_t retVal = 0;
do {
......
uint8_t msg_buffer[3 * 1024]; // 消息缓存池
mach_msg_header_t *msg = NULL;
mach_port_t livePort = MACH_PORT_NULL;
......
__CFPortSet waitSet = rlm->_portSet; // 需要监听的port

__CFRunLoopUnsetIgnoreWakeUps(rl);
// 通知observers即将处理timer事件
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知observers即将处理source0事件
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 执行当前runloop链表内的每个block任务,即CFRunLoopPerformBlock加入的block任务
__CFRunLoopDoBlocks(rl, rlm);
// 处理source0事件
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
// 如果有再次处理block任务
__CFRunLoopDoBlocks(rl, rlm);
}

Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 如果有source1,处理source1事件
// 循环中第一次的循环didDispatchPortLastTime为YES,所以这个分支在第一次循环的时候不会进入
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}

didDispatchPortLastTime = false;
// 通知observers即将休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 设置为休眠状态
__CFRunLoopSetSleeping(rl);

__CFPortSetInsert(dispatchPort, waitSet);

__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);

CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();

// 睡眠中,有个循环接收端口消息
do {
if (kCFUseCollectableAllocator) {
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;

// 内部调用mach_msg函数接收消息,以便接收到消息立刻唤醒runloop,如果没有消息,就让线程休眠
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 如果收到来自runloop queue队列的事件,里面一般都是timer相关事件
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
// 按个取出任务执行
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
if (rlm->_timerFired) {

rlm->_timerFired = false;
break;
} else {
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
}
} else {
// Go ahead and leave the inner loop.
break;
}
} while (1);

__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);

rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));

__CFPortSetRemove(dispatchPort, waitSet);

__CFRunLoopSetIgnoreWakeUps(rl);

// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
// 通知observers即将醒来
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

// 处理收到的消息
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);

if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
#if USE_DISPATCH_SOURCE_FOR_TIMERS
// 被timer相关唤醒,处理timer
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
#if USE_MK_TIMER_TOO
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
else if (livePort == dispatchPort) { // 如果是主线程任务,处理主线程任务。
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
// 内部是_dispatch_main_queue_callback_4CF
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
} else {
CFRUNLOOP_WAKEUP_FOR_SOURCE();

voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);

// Despite the name, this works for windows handles as well
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
// 有source1事件处理,就处理source1事件
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply = NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
}

// Restore the previous voucher
_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);

}
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
// 处理加入到loop的block
__CFRunLoopDoBlocks(rl, rlm);


if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) { // 如果runloop被停止
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) { // 如果mode被停止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { // 如果模式里是空的
retVal = kCFRunLoopRunFinished;
}

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
voucher_mach_msg_revert(voucherState);
os_release(voucherCopy);
#endif

} while (0 == retVal);

if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
} else {
free(timeout_context);
}

return retVal;
}

  启动runloop主要流程

  1. 通知observers即将进入runloop
  2. 通知observers即将处理timers事件
  3. 通知observers即将处理source事件
    • 处理blocks
    • 处理source0
  4. 通知observers即将休眠(内部有个循环接收消息)
  5. 通知observers结束休眠,并回到第2步
  6. 处理消息
    • 处理timer事件
    • 处理主线程任务
    • 处理blocks
    • 回到第2步
  7. 通知observers即将退出runloop

0x05 使用

NSTimer

  在RunLoop中,关于定时器经常遇到的问题就是滑动的时候定时器就不走动了。解决方案也是很熟悉,把timer加入NSRunLoopCommonModes 或者同时加入到NSDefaultRunLoopMode 和UITrackingRunLoopMode 。

1
2
3
4
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
或者
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:UITrackingRunLoopMode];

  其中NSRunLoopCommonModes其实是一个mode集合,里面包括所有的被标记为common的mode,比如NSDefaultRunLoopMode、UITrackingRunLoopMode。

  RunLoop同一时刻下只能跑在一个mode上,所以Timer不走动就是因为如果只加入到一种mode下,切换到别的mode,比如滑动页面就会切换到UITrackingRunLoopMode,Timer就因为在该模式下没有注册,所以不会响应Timer事件。

线程保活

  比如子线程内请求一个网络数据,但是等待完成需要等一会,这就会导致得到数据的时候子线程就被回收掉了。要想子线程一直存在,我们可以利用RunLoop,在子线程写下如下代码就可以使得线程保活。

1
2
3
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];

  但是这样又会有一个问题,我们看下对于run方法的描述

1
2
3
Puts the receiver into a permanent loop, during which time it processes data from all attached input sources.
If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.

  简单来说就是run方法内部是个无限循环,无限调用runMode:beforeDate:方法,并且即时删除mode下所有的source和timer也不能停止。如果创建多个线程,并都通过run启用RunLoop,就会造成内存泄露的问题,如果想要其变得可控,官方也给了建议,自己手动写了while,并且通过一个全局变量来控制什么时候结束这个循环。

1
2
3
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

  停止RunLoop的伪代码可以写成这样的

1
2
3
4
- (void)stop {
self.shouldKeepRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
}

  我们总结一下启动RunLoop的几种方式

  • run

    无限循环,终止不掉

  • runUntilDate:

    内部同样重复调用runMode:beforeDate:,但是时间到了就结束,不再调用;或者通过CFRunLoopStop结束

  • runMode:beforeDate:

    只调用一次;或者通过CFRunLoopStop结束

AutoreleasePool

  我们知道[NSRunLoop currentRunLoop]用来获取当前RunLoop,如果没有就会创建一个RunLoop,我们看下其内部汇编代码,发现会自动开启AutoreleasePool。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Foundation`+[NSRunLoop(NSRunLoop) currentRunLoop]:
0x1853c7ea4 <+0>: stp x20, x19, [sp, #-0x20]!
0x1853c7ea8 <+4>: stp x29, x30, [sp, #0x10]
0x1853c7eac <+8>: add x29, sp, #0x10 ; =0x10
0x1853c7eb0 <+12>: mov x0, #0x0
0x1853c7eb4 <+16>: bl 0x185416114 ; NSPushAutoreleasePool
0x1853c7eb8 <+20>: mov x19, x0
0x1853c7ebc <+24>: bl 0x18495545c ; CFRunLoopGetCurrent
0x1853c7ec0 <+28>: bl 0x184958a74 ; _CFRunLoopGet2
0x1853c7ec4 <+32>: mov x20, x0
0x1853c7ec8 <+36>: mov x0, x19
0x1853c7ecc <+40>: bl 0x185416118 ; NSPopAutoreleasePool
0x1853c7ed0 <+44>: mov x0, x20
0x1853c7ed4 <+48>: ldp x29, x30, [sp, #0x10]
0x1853c7ed8 <+52>: ldp x20, x19, [sp], #0x20
0x1853c7edc <+56>: ret

深入分析Runtime

发表于 2018-05-21 | 分类于 iOS

    Objective-C作为一门动态语言,其核心就是Runtime。本篇将从源码着手,分析Runtime的几个关键地方,使得我们可以更好的理解Runtime的运行机制。我们所有的类都继承于NSObject,所以我们先把结构列出来

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
// 每个OC对象都有一个isa成员
@interface NSObject <NSObject> {
Class isa ;
}

// isa又是一个obc_class的结构体
typedef struct objc_class *Class;

struct objc_class : objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;

class_rw_t *data() {
return bits.data();
}
......
};

// objc_class又继承objc_object
struct objc_object {
private:
isa_t isa;
......
};

struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
......
};

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};

    所以NSObject的结构可以大致看成下面这样的:

1
2
3
4
5
6
7
8
9
10
11
@interface NSObject <NSObject> {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;

class_rw_t *data() {
return bits.data();
}
....
}

0x01 isa

  在ARM64之前,isa是直接指向类对象或元类对象的地址,但是在ARM64的时候,isa包含了更多的信息。isa_t是一个共用体(union),结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
union isa_t 
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits; // unsigned long

# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};

  作为共用体,所有成员共享一块区域。uintptr_t表示的是unsigned long,大小是8个字节,其作为共用体里最大的成员,所以这个共用体也占据8个字节的大小,可能这里有些人会觉得疑惑,明明struct里有9个uintptr_t成员,怎么会共用体才8个字节。要解释这个问题,我们需要知道位域这个概念。

  位域,是指信息在存储时,并不需要占用一个完整的字节,我们可以指定其大小。这样做的目的可以使得我们可以节省空间。举个例子,假设我们养一个宠物,给它设定三个属性,分别为吃饭了没,喝水了没,洗澡了没。

1
2
3
@property (nonatomic, assign) BOOL eat;
@property (nonatomic, assign) BOOL drink;
@property (nonatomic, assign) BOOL sleep;

  我们知道,BOOL占用一个字节,三个属性占用了三个字节。对于这三个属性的值非真即假,用三个字节我觉得很浪费,那么如何使用位域更节省的使用空间呢?对于一个属性非真即假,我们用二进制就可以来满足,1个字节又有8位,所以三个属性的结果只需要3位即可满足。比如睡觉在最后一位,喝水倒数第二位,睡觉倒数第三位,假如满足吃饭的条件,值就是0b0000 0001。如果满足吃饭也满足喝水,值就是0b0000 0011,以此类推。

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
@interface Animal : NSObject

- (void)setEat:(BOOL)eat;
- (void)setDrink:(BOOL)drink;
- (void)setSleep:(BOOL)sleep;

-(BOOL)eat;
-(BOOL)drink;
-(BOOL)sleep;

@end

@implementation Animal {
union {
char bits; // 共用体占据1个字节

struct {
bool eat : 1; // 指定占据1位
bool drink : 1;
bool sleep : 1;
};
}_action;
}


- (void)setEat:(BOOL)eat {
if (eat) {
_action.bits |= (1 << 0); // 1 << 0即0000 0001;或上1将对应位设置为1
}else{
_action.bits &= ~(1 << 0); // ~(1 << 0)即1111 1110;与上,将对应为设置为0
}
}

- (void)setDrink:(BOOL)drink {
if (drink) {
_action.bits |= (1 << 1); // 1 << 1,左移1位,即0000 0010;或上1将对应位设置为1
}else{
_action.bits &= ~(1 << 1); // ~(1 << 1)即1111 1101;与上,将对应为设置为0
}
}

- (void)setSleep:(BOOL)sleep {
if (sleep) {
_action.bits |= (1 << 2); // 1 << 2即0000 0100;或上1将对应位设置为1
}else{
_action.bits &= ~(1 << 2); // ~(1 << 2)即1111 1011;与上,将对应为设置为0
}
}

- (BOOL)eat {
return !!(_action.bits & (1 << 0)); // 与上,来进行取值
}

- (BOOL)drink {
return !!(_action.bits & (1 << 1));
}

- (BOOL)sleep {
return !!(_action.bits & (1 << 2));
}

@end

// 测试代码
int main(int argc, char * argv[]) {
Animal *a = [Animal new];
NSLog(@"%@", [NSString stringWithFormat:@"宠物%@,%@,%@", [a eat] ? @"有吃饭":@"没有吃饭", [a drink] ? @"有喝水":@"没有喝水", [a sleep] ? @"有睡觉":@"没有睡觉"]);
[a setEat:YES];
[a setDrink:NO];
[a setSleep:YES];
NSLog(@"%@", [NSString stringWithFormat:@"宠物%@,%@,%@", [a eat] ? @"有吃饭":@"没有吃饭", [a drink] ? @"有喝水":@"没有喝水", [a sleep] ? @"有睡觉":@"没有睡觉"]);
}

// 运行结果:
2018-05-21 10:03:24.118022+0800 testData[67088:104712921] 宠物没有吃饭,没有喝水,没有睡觉
2018-05-21 10:03:24.119529+0800 testData[67088:104712921] 宠物有吃饭,没有喝水,有睡觉

  回到isa_t共用体,我们详细解释下里面每一位的意思:

  • nonpointer,占据1位

    0 : 直接执行类地址或者元类地址

    1:包含更多信息

  • has_assoc,占据1位

    0:没有关联对象

    1:有关联对象

  • has_cxx_dtor,占据1位

    0:没有C++类的析构函数

    1:有C++类的析构函数

  • shiftcls, 占据33位

    类地址或元类地址,具体值的算法= isa的地址 & 0x0000000ffffffff8

  • magic,占据1位

    0:对象没有完成初始化

    1:对象完成初始化

  • weakly_referenced,占位1位

    0:没有被弱引用指向

    1:有被弱引用指向

  • deallocating,占据1位

    0:没有正在被释放

    1:正在被释放

  • has_sidetable_rc,占据1位

    0:引用计数器可以被保存在isa里

    1:引用计数器太大,不能保存在isa里

  • extra_rc,占据19位

    存储引用计数 - 1的值

  首先,证明在ARM64上isa已经不是直接指向类对象或元类对象,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char * argv[]) {
Animal *a = [Animal new];
Class cls = object_getClass(a);
}

// 打印a->isa的值
(lldb) p/x a->isa
(Class) $1 = 0x000001a1025496d5
// 打印cls的值
(lldb) p/x cls
(Class) $0 = 0x00000001025496d0

  我们发现的确isa的地址跟类地址的值是不一致的,要通过isa拿到真实的类地址需要与上0x0000000ffffffff8

1
2
p/x 0x000001a1025496d5 & 0x0000000ffffffff8
(long) $2 = 0x00000001025496d0 // 即cls的地址值

  将0x0000000ffffffff8转换成二进制值

1
2
p/t 0x0000000ffffffff8
(long) $3 = 0b0000000000000000000000000000111111111111111111111111111111111000

  与上面说的shiftcls一致,即倒数第四位开始的33位。我们知道如果要取值,就是与1进行取与操作

1
2
3
0x000001a1025496d  = 0b0000000000000000000000011010000100000010010101001001011011010101
0x0000000ffffffff8 = 0b0000000000000000000000000000111111111111111111111111111111111000
& = 0b0000000000000000000000000000000100000010010101001001011011010000

  接着,我们设置一下关联对象,证明has_assoc是否会变为1

1
2
3
4
5
6
7
设置前:0b0000000000000000000000011010000100000010010101001001011011010101  // 倒数第二位为0

// 测试代码
Animal *a = [Animal new];
objc_setAssociatedObject(a, @selector(age), @100, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

设置后:0b0000000000000000000000011010000100000000010010011001011011101111 // 倒数第二位为1

  其他位就不一一做实验了。

0x02 方法查找流程

isa和superClass关系示意图

  在之前的Category文章里,其实已经提到这个,所有的调用流程如上图所示。我们只需要记住,实例对象只保存成员变量的值,实例对象的方法(比如- (void)eat;)保存在其类对象的方法表里,以及协议信息和成员变量信息(名字,大小等)也是保存在类对象对应的协议表和成员变量表中。而类方法(比如+ (void)eat),则保存在元类(meta-class)的的方法表中。

  所以下文的表述中,如果说的是类的方法表,说明找的是实例方法;如果说的是元类的方法表,说明找的是类方法。

0x03 objc_msgSend

  objc_msgSend是通过汇编代码实现的,Objective-C中调用方法实际都是走的objc_msgSend函数,所以这个函数是会被十分频繁的调用,以汇编实现将提升效率。

  我们首先写下测试代码:

1
2
Animal *a = [Animal new];
[a eat];

  符号断点在eat方法上,看下objc_msgSend汇编代码,所有方法调用都会带两个参数,一个是self,即调用者,第二个是_cmd,即调用方法,分别保存在x0和x1寄存器里。

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
libobjc.A.dylib`objc_msgSend:
; x0即调用者Animal对象,#0x0即nil,也就是Animal对象是不是为0
-> 0x183c5c900 <+0>: cmp x0, #0x0
; 如果是nil,就跳转到0x183c5c96c地址处继续执行
0x183c5c904 <+4>: b.le 0x183c5c96c
; 相当于a->isa的地址保存到x13寄存器。x0表示Animal实例对象a的地址,[x0]表示a->isa的地址
0x183c5c908 <+8>: ldr x13, [x0]
; 这个上面提过,isa地址 与上 #0xffffffff8得到类对象地址,并将类对象的地址保存到x16寄存器
0x183c5c90c <+12>: and x16, x13, #0xffffffff8
; 在类对象地址偏移16个字节的位置,读取16个字节,分别放进x10寄存器和x11寄存器,详细看下面注解①
0x183c5c910 <+16>: ldp x10, x11, [x16, #0x10]
; w11保存的是cache_t结构体的mask成员,也就是将_cmd的低32位与mask做与运算。目的是取出方法的在散列表中的位置,保存到x12寄存器的低32位
0x183c5c914 <+20>: and w12, w1, w11
; x10保存着散列表的首地址,x12保存着方法的位置(假设为index),lsl #4表示左移4位,相当于乘以16,而且散列表每个成员(bucket_t)也正好占用16个字节,所以方法的实际地址 = 首地址 + index * 16。结果保存到x12寄存器
0x183c5c918 <+24>: add x12, x10, x12, lsl #4
;取出x12地址开始的16个字节,前8个字节保存到x9寄存器,后8个字节保存到x17寄存器。我们知道x12保存的是我们所调用的方法在散列表中的地址,其结构是bucket_t结构体,所以方法名key保存在x9寄存器,方法地址imp保存在x17寄存器。
0x183c5c91c <+28>: ldp x9, x17, [x12]
; 比较_cmd名字和散列表中所找到的方法的名字是否一致
0x183c5c920 <+32>: cmp x9, x1
; 如果不一致,就跳转到0x183c5c92c地址
0x183c5c924 <+36>: b.ne 0x183c5c92c ; <+44>
; 如果名字一致,就直接跳转到方法的实现地址。
0x183c5c928 <+40>: br x17
; 之前如果方法和找到的缓存方法的名字不一样,就来到了这里,如果x9是0,那就执行_objc_msgSend_uncached函数
0x183c5c92c <+44>: cbz x9, 0x183c5cc00 ; _objc_msgSend_uncached
; x10保存是散列表首地址,x12保存的是当前back_t成员的地址
0x183c5c930 <+48>: cmp x12, x10
; 如果x12跟x10值一样,说明散列表已经遍历完了, 跳转到0x183c5c940
0x183c5c934 <+52>: b.eq 0x183c5c940 ; <+64>
; 如果x12跟x10值不一致,说明没遍历结束,[x12, #-0x10]!往前移动16个字节,即来到前一个bucket_t成员。
0x183c5c938 <+56>: ldp x9, x17, [x12, #-0x10]!
; 跳转到上面的cmp x9, x1,继续比较方法名字是否一致
0x183c5c93c <+60>: b 0x183c5c920 ; <+32>
; 散列表遍历完成后,会来到这里。w11是掩码mask,将掩码左移4位,即左移16个字节,我们知道掩码保存的是散列表的长度减1。x12此时是散列表首地址,两者相加再次保存进x12寄存器,也就是x12保存的就是散列表最后一个成员的地址。
0x183c5c940 <+64>: add x12, x12, w11, uxtw #4
; 同<+28>
0x183c5c944 <+68>: ldp x9, x17, [x12]
; 比较名字
0x183c5c948 <+72>: cmp x9, x1
; 不一致就执行_objc_msgSend_uncached
0x183c5c94c <+76>: b.ne 0x183c5c954 ; <+84>
; 一致就调用方法,接下来就跟前面差不多了,后面不做解释
0x183c5c950 <+80>: br x17
0x183c5c954 <+84>: cbz x9, 0x183c5cc00 ; _objc_msgSend_uncached
0x183c5c958 <+88>: cmp x12, x10
; 如果又回到了表头,则跳转到0x183c5c968处
0x183c5c95c <+92>: b.eq 0x183c5c968 ; <+104>
0x183c5c960 <+96>: ldp x9, x17, [x12, #-0x10]!
0x183c5c964 <+100>: b 0x183c5c948 ; <+72>
; 即执行_objc_msgSend_uncached函数
0x183c5c968 <+104>: b 0x183c5cc00 ; _objc_msgSend_uncached
0x183c5c96c <+108>: b.eq 0x183c5c9a4 ; <+164>
0x183c5c970 <+112>: mov x10, #-0x1000000000000000
0x183c5c974 <+116>: cmp x0, x10
0x183c5c978 <+120>: b.hs 0x183c5c990 ; <+144>
0x183c5c97c <+124>: adrp x10, 209389
0x183c5c980 <+128>: add x10, x10, #0x270 ; =0x270
0x183c5c984 <+132>: lsr x11, x0, #60
0x183c5c988 <+136>: ldr x16, [x10, x11, lsl #3]
0x183c5c98c <+140>: b 0x183c5c910 ; <+16>
0x183c5c990 <+144>: adrp x10, 209389
0x183c5c994 <+148>: add x10, x10, #0x2f0 ; =0x2f0
0x183c5c998 <+152>: ubfx x11, x0, #52, #8
0x183c5c99c <+156>: ldr x16, [x10, x11, lsl #3]
0x183c5c9a0 <+160>: b 0x183c5c910 ; <+16>
0x183c5c9a4 <+164>: mov x1, #0x0
0x183c5c9a8 <+168>: movi d0, #0000000000000000
0x183c5c9ac <+172>: movi d1, #0000000000000000
0x183c5c9b0 <+176>: movi d2, #0000000000000000
0x183c5c9b4 <+180>: movi d3, #0000000000000000
0x183c5c9b8 <+184>: ret
0x183c5c9bc <+188>: nop

  注解① 回到NSObject结构,我们可以发现偏移16个字节就是cache_t成员的位置。

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
@interface NSObject <NSObject> {
isa_t isa; // 8个字节
Class superclass; // class其实也就是isa_t,也是8个字节
cache_t cache;
class_data_bits_t bits;

class_rw_t *data() {
return bits.data();
}
......
}

// cache_t结构体
struct cache_t {
struct bucket_t *_buckets; // 指针8个字节,指向一个散列表
mask_t _mask; // 掩码。 mask_t 即uint32_t,是4个字节。表示的是散列表长度 - 1,类似于数组,数组长度为n,但是取值从0开始的,所以需要n-1。
mask_t _occupied; // 4个字节。表示的缓存方法的个数
......
};

// bucket_t结构体
struct bucket_t {
cache_key_t _key; // 方法名,cache_key_t即unsigned long,8个字节
IMP _imp; // 方法的实现地址,指针8个字节
......
}; // bucket_t占用16个字节

  所以,我们可以发现cache成员位置开始取16个字节,其实就是把散列表bucket_t的首地址保存到x10寄存器;我们知道一个寄存器占用8个字节,所以mask掩码保存到x11寄存器的低32位,occupied保存到x11寄存器的高32位。

  上面的流程总结如下:

  • 判断对象是否为空
  • 取到isa地址
  • 根据isa地址拿到类对象的地址
  • 来到缓存成员cache_t的地址,为的是开始遍历缓存散列表
  • 计算出我们调用方法在散列表的位置index
  • 根据散列表首地址和散列表成员长度计算出方法在散列表的具体地址:方法地址 = 首地址 + 成员长度 * index
  • 比较我们调用的方法名字和散列表中找到的方法的名字是否一致,如果方法名字为空(说明肯定没被缓存过),那么就跳转到_objc_msgSend_uncached函数,一般第一次调用方法都会执行到这里的时候就跳转走了。
  • 如果调用的方法有名字(说明被缓存过了),且一致的话就直接调用函数执行地址。
  • 不一致的话,往前找前一个成员,也是进行名字比较。依次类推,直到来到首地址。
  • 如果来到首地址,这时候还是没找到跟我们调用方法名字一致的散列表成员,就扩大搜索范围,来到散列表尾部再往前一个个遍历,查找与调用方法名字一致的散列表成员。
  • 第二次来到表头还是没找到,就调用_objc_msgSend_uncached函数。

  前面说到,如果缓存里找不到缓存,就会调用_objc_msgSend_uncached,我们看下源码

1
2
3
4
5
6
7
8
9
10
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search

MethodTableLookup
br x17

END_ENTRY __objc_msgSend_uncached

  源码里有一个MethodTableLookup的宏

1
2
3
4
5
.macro MethodTableLookup
......
bl __class_lookupMethodAndLoadCache3
......
.endmacro

  里面调用c函数__class_lookupMethodAndLoadCache3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
// 因为缓存里没找到,所以入参cache为NO
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

// 查找主要在这个函数里面
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;

runtimeLock.assertUnlocked();

// 如果cache为YES,显然这里是NO
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}

// 加锁
runtimeLock.read();

// 如果类还没被构造好,即class_rw_t结构体还没被初始化
if (!cls->isRealized()) {
runtimeLock.unlockRead();
runtimeLock.write();

realizeClass(cls);

runtimeLock.unlockWrite();
runtimeLock.read();
}

// 如果类还是没初始化,一般第一次来到某个函数,就会执行一次也是唯一一次的initialize方法。
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}


retry:
runtimeLock.assertReading();


// 再次尝试在本类缓存中查找,如果找到了就结束本函数
imp = cache_getImp(cls, sel);
if (imp) goto done;

// 还是没在缓存中找到,就来到类的方法表中进行查找,如果找到了,在调用者缓存中保存,然后结束本函数
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}

// 尝试到父类的缓存表中查找,如果父类缓存表中没有找到,就去父类的方法表查找
// 这是一个for循环,所以会遍历直到基类,即NSObject结束
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}

//   cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}

// 方法表中如果找到了,在缓存中保存,然后结束本函数
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}


// 都没有找到,进入动态方法解析流程:首先看看是否元类,是元类调用resolveClassMethod方法和resolveInstanceMethod方法,如果不是元类调用resolveInstanceMethod方法
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
triedResolver = YES;
goto retry;
}


// 如果是resolveClassMethod方法或resolveInstanceMethod方法也没被调用,直接调用转发机制。
// 即先调用forwardingTargetForSelector,如果这个方法返回nil,继续调用methodSignatureForSelector,如果返回不为空继续调用forwardInvocation;如果还是为空,调用doesNotRecognizeSelector,则闪退报错
// _objc_msgForward_impcache会在下文做解释
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

done:
runtimeLock.unlockRead();

return imp;
}


void _class_resolveMethod(Class cls, SEL sel, id inst)
{
// 判断不是元类,调用resolveInstanceMethod方法
if (! cls->isMetaClass()) {
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// 如果是元类,首先调用resolveClassMethod方法,再次从类或者方法列表中查找,如果还是没有,再次调用resolveInstanceMethod方法
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}

  _objc_msgForward_impcache又是一个汇编函数

1
b      0x183c5cda0               ; _objc_msgForward

  内部就是调用_objc_msgForward函数,里面就是跳转到CoreFoundation里的CF_forwarding_prep_0

1
2
3
4
5
6
7
    0x183c5cda0 <+0>:  adrp   x17, 209389
0x183c5cda4 <+4>: ldr x17, [x17, #0x168]
-> 0x183c5cda8 <+8>: br x17

; 读取x17寄存器的值,即_CF_forwarding_prep_0函数
(lldb) register read x17
x17 = 0x000000018497a3c0 CoreFoundation`_CF_forwarding_prep_0

  _CF_forwarding_prep_0继续会调用___forwarding___函数,

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
CoreFoundation`_CF_forwarding_prep_0:
; 保存fp和lr寄存器
-> 0x18497a3c0 <+0>: stp x29, x30, [sp, #-0x10]!
0x18497a3c4 <+4>: mov x29, sp
0x18497a3c8 <+8>: sub sp, sp, #0xd0 ; =0xd0
0x18497a3cc <+12>: str q7, [sp, #0xc0]
0x18497a3d0 <+16>: str q6, [sp, #0xb0]
0x18497a3d4 <+20>: str q5, [sp, #0xa0]
0x18497a3d8 <+24>: str q4, [sp, #0x90]
0x18497a3dc <+28>: str q3, [sp, #0x80]
0x18497a3e0 <+32>: str q2, [sp, #0x70]
0x18497a3e4 <+36>: str q1, [sp, #0x60]
0x18497a3e8 <+40>: str q0, [sp, #0x50]
0x18497a3ec <+44>: str x8, [sp, #0x40]
0x18497a3f0 <+48>: str x7, [sp, #0x38]
0x18497a3f4 <+52>: str x6, [sp, #0x30]
0x18497a3f8 <+56>: str x5, [sp, #0x28]
0x18497a3fc <+60>: str x4, [sp, #0x20]
0x18497a400 <+64>: str x3, [sp, #0x18]
0x18497a404 <+68>: str x2, [sp, #0x10]
; sp偏移8位开始的8位,存放方法,即_cmd
0x18497a408 <+72>: str x1, [sp, #0x8]
; sp开始的8位保存对象,即self
0x18497a40c <+76>: str x0, [sp]
; sp保存到x0
0x18497a410 <+80>: mov x0, sp
0x18497a414 <+84>: mov x1, #0x0
; 跳转到___forwarding___函数
0x18497a418 <+88>: bl 0x184a94064 ; ___forwarding___

  里面进行转发流程,如果转发流程中的方法都没被实现,那么调用调用doesNotRecognizeSelector方法,程序就此闪退。汇编还是比较好理解,所以直接阅读一部分汇编代码,如果有兴趣阅读全部的汇编代码,只需要自己下符号断点,自己观察即可:

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
CoreFoundation`___forwarding___:
0x184a94064 <+0>: stp x26, x25, [sp, #-0x50]!
0x184a94068 <+4>: stp x24, x23, [sp, #0x10]
0x184a9406c <+8>: stp x22, x21, [sp, #0x20]
0x184a94070 <+12>: stp x20, x19, [sp, #0x30]
0x184a94074 <+16>: stp x29, x30, [sp, #0x40]
0x184a94078 <+20>: add x29, sp, #0x40 ; =0x40
0x184a9407c <+24>: sub sp, sp, #0x10 ; =0x10
0x184a94080 <+28>: mov x24, x1
; 在_CF_forwarding_prep_0函数里,sp保存到了x0里面,这里又将地址赋值给了x20
0x184a94084 <+32>: mov x20, x0
0x184a94088 <+36>: adrp x8, 174263
0x184a9408c <+40>: ldr x8, [x8, #0x770]
0x184a94090 <+44>: ldr x8, [x8]
0x184a94094 <+48>: stur x8, [x29, #-0x48]
; 从x20的地址开始取16位字节分别存到x19和x22,在在_CF_forwarding_prep_0函数里sp开始的16个字节分别存了self和_cmd,所以x19就是self,x22就是_cmd
0x184a94098 <+52>: ldp x19, x22, [x20]
-> 0x184a9409c <+56>: tbz x19, #0x3f, 0x184a940b8 ; <+84>
0x184a940a0 <+60>: ubfx x8, x19, #60, #3
0x184a940a4 <+64>: cmp x8, #0x7 ; =0x7
0x184a940a8 <+68>: ubfx x9, x19, #52, #8
0x184a940ac <+72>: add x9, x9, #0x8 ; =0x8
0x184a940b0 <+76>: csel x8, x8, x9, ne
0x184a940b4 <+80>: cbz w8, 0x184a9441c ; <+952>
;将self移到x0寄存器
0x184a940b8 <+84>: mov x0, x19
;调用object_getClass函数,获取Class对象,返回结果保存在x0
0x184a940bc <+88>: bl 0x183c466c4 ; object_getClass
; 将返回结果保存在x21
0x184a940c0 <+92>: mov x21, x0
; 根据class对象得到字符串
0x184a940c4 <+96>: bl 0x183c51810 ; class_getName
0x184a940c8 <+100>: mov x23, x0
0x184a940cc <+104>: adrp x8, 199366
; 得到SEL对象forwardingTargetForSelector:并保存到x25
0x184a940d0 <+108>: ldr x25, [x8, #0x340]
; 将class对象保存到x0
-> 0x184a940d4 <+112>: mov x0, x21
; 将sel对象保存到x1
0x184a940d8 <+116>: mov x1, x25
; 调用class_respondsToSelector方法,是否实现了得到SEL对象forwardingTargetForSelector:方法
0x184a940dc <+120>: bl 0x183c47550 ; class_respondsToSelector
0x184a940e0 <+124>: cbz w0, 0x184a94100 ; <+156>
0x184a940e4 <+128>: mov x0, x19
0x184a940e8 <+132>: mov x1, x25
-> 0x184a940ec <+136>: mov x2, x22
; 如果实现了就调用得到SEL对象forwardingTargetForSelector:方法
0x184a940f0 <+140>: bl 0x183c5c900 ; objc_msgSend
; 后面流程大致差不多,如果没实现得到SEL对象forwardingTargetForSelector:方法,就看下有没有methodSignatureForSelector方法,如果实现了methodSignatureForSelector方法,看下有没有调用forwardInvocation:....

  这部分流程总结如下:

  • 再次从缓存查找一次。
  • 如果缓存还是没找到,去类对象的方法表里查找方法,如果找到,就保存到缓存,并执行这个方法。
  • 如果类对象方法表里也没找到,就先去父类的缓存表里找,如果缓存表也没找到,就取找父类的方法表,如果找到,同样缓存方法到缓存,如果还是没找到,继续往上一层父类查找。
  • 以此类推,直到找到基类,即NSObject类的方法表。
  • 到了基类还是没找到,那么就先判断自己是不是元类,不是元类的话调用resolveInstanceMethod方法;是元类的话,先调用resolveClassMethod方法,接着去类的方法表一层一层查找有没有实现这个实例方法,如果也没找到就调用resolveInstanceMethod方法。
  • 如果resolveInstanceMethod方法或者resolveClassMethod方法也没被调用,开启转发流程。
  • 先调用forwardingTargetForSelector,如果这个方法返回nil,继续调用methodSignatureForSelector,如果返回不为空继续调用forwardInvocation;如果还是为空,调用doesNotRecognizeSelector,则闪退报错

0x04 Runtime的初始化

  这部分内容在Category文章里粗略讲述过一遍,这里将会带着源码详细走下初始化流程

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

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

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

  load_images函数其实就是调用类的+load方法,这里不重复介绍,Category文章里讲的很详细了。

1
2
3
4
5
6
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
rwlock_writer_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}

  我们直接看如何加载Mach-O信息,通过map_images函数获取所有的类相关的数据。

1
2
3
4
5
6
void  map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
// 之前做的操作,就是读取Mach-O的信息
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}

  Mach-O信息加载进来后,就要从数据段里读取相应的内容,并将构建成我们具体使用的类,我们的类,协议,分类等 都在数据段里,如下图。

数据段信息示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t i;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
TimeLogger ts(PrintImageTimes);

runtimeLock.assertWriting();

#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++

if (!doneOnce) {
doneOnce = YES;

if (DisableTaggedPointers) {
disableTaggedPointers();
}

if (PrintConnecting) {
_objc_inform("CLASS: found %d classes during launch", totalClasses);
}

// namedClasses
// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);

ts.log("IMAGE TIMES: first time tasks");
}


// 获取类的信息
for (EACH_HEADER) {
if (! mustReadClasses(hi)) {
// Image is sufficiently optimized that we need not call readClass()
continue;
}

bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->isPreoptimized();
// 从数据段的__objc_classlist里查找有哪些类,项目里有多少类,这里就有多少
classref_t *classlist = _getObjc2ClassList(hi, &count);
for (i = 0; i < count; i++) {
// 得到类
Class cls = (Class)classlist[i];
// 以类名有key,类为value注册进一个类映射表
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

if (newCls != cls && newCls) {
// 跟objc_getFutureClass有关,这里可以忽略
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}

ts.log("IMAGE TIMES: discover classes");

// 修复类列表和懒加载类列表还没映射的情况
if (!noClassesRemapped()) {
for (EACH_HEADER) {
// 获取被引用的类,只有项目里被import的才会在这里出现
// 所以_getObjc2ClassList得到的总数 - _getObjc2ClassRefs得到的总数 = 项目中没有被用到的类,可以做优化参考。
Class *classrefs = _getObjc2ClassRefs(hi, &count);
// 在类映射表重新映射
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
// 重新映射被引用的父类
classrefs = _getObjc2SuperRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
}
}

ts.log("IMAGE TIMES: remap classes");

// 获取被引用的方法列表
static size_t UnfixedSelectors;
sel_lock();
for (EACH_HEADER) {
if (hi->isPreoptimized()) continue;

bool isBundle = hi->isBundle();
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
// 在方法映射表中注册
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
sel_unlock();

ts.log("IMAGE TIMES: fix up selector references");

// 读取协议列表
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();

protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
// 在协议映射表中注册
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}

ts.log("IMAGE TIMES: discover protocols");

// 获取被引用的协议,并重新映射
for (EACH_HEADER) {
protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
for (i = 0; i < count; i++) {
remapProtocolRef(&protolist[i]);
}
}

ts.log("IMAGE TIMES: fix up @protocol references");

// 实现+load的类列表
for (EACH_HEADER) {
classref_t *classlist =
_getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
// 构造类信息,只有被构造好的类才可以被使用
realizeClass(cls);
}
}

ts.log("IMAGE TIMES: realize non-lazy classes");

// 获取分类列表
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();

for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
// 如果类映射表里没有分类对应的类,报错
if (!cls) {
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}

// 处理分类
// 首先,注册分类到其目标类
// 其次,重建类的方法表、协议表和成员变量表
bool classExists = NO;
// 实例信息
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
// 将分类表插入到分类映射表里,供后面取出使用加入到类的方法表、协议表等
// 因为此时类的结构还没被构造完成,就算要加也加不进去,所以先保存下
addUnattachedCategoryForClass(cat, cls, hi);
// 如果类已经被构造好,比如有实现+load方法的类会被先构造好,那么这里就需要重建类的结构,把类的方法、协议等重新加入进类的方法表、协议表里,并且刷新
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
// 类信息
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}

ts.log("IMAGE TIMES: discover categories");

// 勾建所有类的信息,以便其可以被使用
if (DebugNonFragileIvars) {
realizeAllClasses();
}
#undef EACH_HEADER
}

// 动态链接所有的类
static void realizeAllClasses(void)
{
runtimeLock.assertWriting();

header_info *hi;
for (hi = FirstHeader; hi; hi = hi->getNext()) {
realizeAllClassesInImage(hi);
}
}

static void realizeAllClassesInImage(header_info *hi)
{
runtimeLock.assertWriting();

size_t count, i;
classref_t *classlist;

if (hi->areAllClassesRealized()) return;

classlist = _getObjc2ClassList(hi, &count);
// 动态链接类,其类处于可用状态
for (i = 0; i < count; i++) {
realizeClass(remapClass(classlist[i]));
}

hi->setAllClassesRealized(YES);
}

static Class realizeClass(Class cls)
{
runtimeLock.assertWriting();

const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;

if (!cls) return nil;
if (cls->isRealized()) return cls;
assert(cls == remapClass(cls));


// 获取RO信息
ro = (const class_ro_t *)cls->data();
// 之前类的信息是只读的,这时候构建成可读可写的
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);

isMeta = ro->flags & RO_META;

rw->version = isMeta ? 7 : 0;


// 父类和元类也需要构建
supercls = realizeClass(remapClass(cls->superclass));
metacls = realizeClass(remapClass(cls->ISA()));

// 更新被重新映射的父类和元类
cls->superclass = supercls;
cls->initClassIsa(metacls);

// 跳转实例变量的偏移和布局
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);

cls->setInstanceSize(ro->instanceSize);

if (ro->flags & RO_HAS_CXX_STRUCTORS) {
cls->setHasCxxDtor();
if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
cls->setHasCxxCtor();
}
}

// 链接这个类到父类的父子关系表中
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}

// 将分类数据,比如方法表、协议表加载进类的方法表、协议表中。是前插操作,具体分类的文章里有说够
methodizeClass(cls);

return cls;
}

static void methodizeClass(Class cls)
{
runtimeLock.assertWriting();

bool isMeta = cls->isMetaClass();
auto rw = cls->data();
auto ro = rw->ro;

// Methodizing for the first time
if (PrintConnecting) {
_objc_inform("CLASS: methodizing class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}

// 附加方法
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
rw->methods.attachLists(&list, 1);
}
// 附加属性
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rw->properties.attachLists(&proplist, 1);
}
// 附加协议
protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rw->protocols.attachLists(&protolist, 1);
}

// Root classes get bonus method implementations if they don't have
// them already. These apply before category replacements.
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
}

// 附加分类信息
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);

if (PrintConnecting) {
if (cats) {
for (uint32_t i = 0; i < cats->count; i++) {
_objc_inform("CLASS: attached category %c%s(%s)",
isMeta ? '+' : '-',
cls->nameForLogging(), cats->list[i].cat->name);
}
}
}

if (cats) free(cats);
}

  通过上面的源代码,可以知道,我们的类的都是从Mach-O得到的,一开始还没被动态链接,所以这个类是不能被使用的,整个初始化流程可以总结如下:

  • 通过Mach-O数据区里的__objc_classlist段,获取所有类,并注册进一个类映射表,通过这个表以后可以根据类名可以很快的得到这个类。
  • 读取数据区__objc_selrefs段,获取所有被引用的方法,并注册进一个方法映射表
  • 读取数据区__objc_protolist段,获取所有协议,并注册进一个协议映射表
  • 读取数据区__objc_nlclslist段,获取所有实现了+(void)load的类,并提前链接好类,使得这个类已经准备好被使用
  • 读取数据区__objc_catlist段,获取所有分类,并注册进一个分类映射表
  • 链接所有类,使得所有类做好被使用的准备。其中包括开辟class_rw_t区,使得类的数据可以即可读又可以写,并且排列好父子关系链,最后将分类的信息前插进类的各个信息表中。

0x05 @property分析

  @property定义的属性,我们知道在不是分类的情况下,会自动生成getter、setter和成员变量。其实这一切都是在编译器完成的,虽然这块不涉及runtime,但是前面我们知道类构建的时候会基于class_ro_t结构重新生成一个class_rw_t结构。而class_ro_t保存的其实就是编译期类的信息,其中包括属性拆解后的信息。

  定义测试代码如下

1
2
3
4
5
6
7
8
@interface Animal : NSObject

@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;

- (void)eat;

@end

  clang转成c++代码看下

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
struct _class_ro_t {
unsigned int flags;
unsigned int instanceStart;
unsigned int instanceSize;
const unsigned char *ivarLayout;
const char *name;
const struct _method_list_t *baseMethods;
const struct _objc_protocol_list *baseProtocols;
const struct _ivar_list_t *ivars;
const unsigned char *weakIvarLayout;
const struct _prop_list_t *properties;
};

struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};

// 成员变量表
static struct /*_ivar_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count;
struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_ivar_t),
2,
// 属性age和name变成了_age和_name的成员变量
{{(unsigned long int *)&OBJC_IVAR_$_Animal$_age, "_age", "i", 2, 4},
{(unsigned long int *)&OBJC_IVAR_$_Animal$_name, "_name", "@\"NSString\"", 3, 8}}
};

// 方法表
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[4];
} _OBJC_$_INSTANCE_METHODS_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
4,
// 生成对应的setter和get方法
{{(struct objc_selector *)"age", "i16@0:8", (void *)_I_Animal_age},
{(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_Animal_setAge_},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Animal_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Animal_setName_}}
};
// 属性表
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"age","Ti,N,V_age"},
{"name","T@\"NSString\",C,N,V_name"}}
};

// 初始化_class_ro_t结构
static struct _class_ro_t _OBJC_CLASS_RO_$_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = {
0, __OFFSETOFIVAR__(struct Animal, _age), sizeof(struct Animal_IMPL),
0,
"Animal",
(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Animal,
0,
(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Animal,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Animal,
};

  通过转换的代码可以很清楚看到,属性的确是在编译期被拆解到class_ro_t结构的各个表中。我们看下成员变量表的初始化

1
2
3
4
5
6
7
8
9
10
static struct /*_ivar_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count;
struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_ivar_t),
2, // 表内元素个数
{{(unsigned long int *)&OBJC_IVAR_$_Animal$_age, "_age", "i", 2, 4},
{(unsigned long int *)&OBJC_IVAR_$_Animal$_name, "_name", "@\"NSString\"", 3, 8}}
};

  ivar_list装的是ivar_t,其结构如下,拿上面第一个ivar_t结构举例:

1
2
3
4
5
6
7
8
9
10
11
12
struct ivar_t {
int32_t *offset; // 成员的偏移地址,对应&OBJC_IVAR_$_Animal$_age
const char *name; // 名字,对应"_age"
const char *type; // 类型,对应"i"
uint32_t alignment_raw; //内存对齐, 对应2
uint32_t size; // 对应4

uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};

  OBJC_IVAR_$_Animal$_age表示成员的偏移地址,这样做的好处是只要拿到实例的地址,再加上偏移地址就能直接访问成员变量,快速的进行取值和赋值。

1
extern "C" unsigned long int OBJC_IVAR_$_Animal$_age __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct Animal, _age);

  其他方法表和属性表初始化也类似,这里不做复述,但是我们看到在方法表中还有这样的代码

1
{(struct objc_selector *)"age", "i16@0:8", (void *)_I_Animal_age},

  i16@0:8是什么样的格式,这其实是@encode指令,可以将具体的类型表示成字符串编码。

encode示意图

  那么i16@0:8是什么意思呢?

i:表示返回int类型

16:表示整个方法占据16个字节

@:表示第一个参数,即对象,方法中默认带两个参数self和_cmd,这里就是指self

0:表示其从偏移位置0开始的8个字节,之前说过这个方法占据16个字节,self占据其中8个字节

::表示第二个参数,是一个方法,即_cmd

8:表示偏移位置8开始的8个字节,即方法占据16个字节的后8个字节。

  v20@0:8i16再来一个setter方法

v:表示返回void

20:表示整个方法占据20字节

@:表示第一个参数对象,即self,占据8个字节

0:表示对象从偏移值0开始占据8个字节

:表示第二个参数,是一个方法,即_cmd

8:表示方法是从偏移值8开始占据8个字节

i:表示第三个参数,int类型的参数

16:表示第三个参数从偏移值16开始占据4个字节

  所有参数加起来的确是20个字节。继续回到这个方法表中,_I_Animal_age就是函数的实际执行地址

1
{{(struct objc_selector *)"age", "i16@0:8", (void *)_I_Animal_age},

 这里也验证了默认带的两个参数:self、cmd。OBJC_IVAR_$_Animal$_age前面说过表示的是成员变量的偏移地址。,通过self+偏移地址直接拿到值

1
2
3
static int _I_Animal_age(Animal * self, SEL _cmd) { 
return (*(int *)((char *)self + OBJC_IVAR_$_Animal$_age));
}

  不同于getter,setter的执行函数内部是这样的,拿到成员变量偏移地址后赋值

1
2
3
static void _I_Animal_setAge_(Animal * self, SEL _cmd, int age) { 
(*(int *)((char *)self + OBJC_IVAR_$_Animal$_age)) = age;
}

  如果我自己实现了setter方法,代码又会如何变化

1
2
3
4
5
6
7
- (void)setAge:(int)age {

}

// 转换代码
static void _I_Animal_setAge_(Animal * self, SEL _cmd, int age) {
}

  setter方法就变成了自己定义的方法了,如果里面不写_age = age,那不会对成员变量_age进行赋值。


  在回来说下属性表,之前说过对方法的定义上,苹果有一套自己的@encode指令,属性同样也有这样的@endcode指令

1
2
3
4
5
6
7
8
9
10
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"age","Ti,N,V_age"},
{"name","T@\"NSString\",C,N,V_name"}}
};

  其参照表如下

encode示意图

  Ti,N,V_age的理解如下

T:类型,后面紧跟着的就是对应类型

i:int类型

N:nonatomic

V_age:对应的变量名是_age

  T@\"NSString\",C,N,V_name,对于这样的可以理解为如下,对于我们定义属性时的@property(nonatomic, copy) NSString *name是不是一一对应了。

T@\”NSString\”:类型是个OC对象,对象名是NSString

C:copy

N:nonatomic

V_name:对应的变量名是_name


  我们已经知道属性在编译期会自动被拆成成员变量、setter、getter以及相关属性信息放到各个对应的表中。那么我们在运行时想要创建一个属性,下面这样做可以吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (id)init{
if (self = [super init]) {
class_addIvar([self class], "_name", sizeof(NSString *), log2(sizeof(NSString*)), @encode(NSString *));
objc_property_attribute_t attrs[] = { { "T", "@\"NSString\"" }, { "C", "" }, { "V", "_name" } };
class_addProperty([self class], "name", attrs, 3);
class_addMethod([self class], @selector(name), (IMP)nameGetter, "@@:");
class_addMethod([self class], @selector(setName:), (IMP)nameSetter, "v@:@");
}
return self;
}

NSString *nameGetter(id self, SEL _cmd) {
Ivar ivar = class_getInstanceVariable([self class], "_name");
return object_getIvar(self, ivar);
}

void nameSetter(id self, SEL _cmd, NSString *name) {
Ivar ivar = class_getInstanceVariable([self class], "_name");
id oldName = object_getIvar(self, ivar);
if (oldName != name) object_setIvar(self, ivar, [name copy]);
}

  答案是不可以,问题出在class_addIvar函数里,所在添加ivar的类有一个是RW_CONSTRUCTING的标签。

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
BOOL  class_addIvar(Class cls, const char *name, size_t size, 
uint8_t alignment, const char *type)
{
if (!cls) return NO;

if (!type) type = "";
if (name && 0 == strcmp(name, "")) name = nil;

rwlock_writer_t lock(runtimeLock);

assert(cls->isRealized());

if (cls->isMetaClass()) {
return NO;
}

// 来到这里返回NO
if (!(cls->data()->flags & RW_CONSTRUCTING)) {
return NO;
}

if ((name && getIvar(cls, name)) || size > UINT32_MAX) {
return NO;
}
}

  那么在哪里设置了这个标签,objc_allocateClassPair函数里调用的objc_initializeClassPair_internal函数里设置了这个标签,而objc_allocateClassPair则是我们动态生成一个类的时候用到的函数。

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
Class objc_allocateClassPair(Class superclass, const char *name, 
size_t extraBytes)
{
Class cls, meta;

rwlock_writer_t lock(runtimeLock);

if (getClass(name) || !verifySuperclass(superclass, true/*rootOK*/)) {
return nil;
}

// Allocate new classes.
cls = alloc_class_for_subclass(superclass, extraBytes);
meta = alloc_class_for_subclass(superclass, extraBytes);

// fixme mangle the name if it looks swift-y?
objc_initializeClassPair_internal(superclass, name, cls, meta);

return cls;
}

static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{
......

// Set basic info

cls->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
meta->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
......
}

  所以要想使用class_addIvar必须是动态生成的一个类,还不是一个在编译期就存在的类。

一个特殊的例子

  下面的例子中,程序是否可以正常运行,如果可以正常运行,那么得出的结果会是什么?

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
@interface Animal : NSObject

@property (nonatomic, copy) NSString *name;

- (void)print;

@end

@implementation Animal

- (void)print {
NSLog(@"动物的名字叫:%@", self.name);
}

@end

// 调用
int main(int argc, char * argv[]) {
NSString *a = @"占位数据,测试中....";

Class cls = [Animal class];
void *obj = &cls;
[(__bridge id)obj print];

return 0;
}

  运行程序后,我们可以看到print方法顺利被调用,但是结果有点出乎预料

1
动物的名字叫:占位数据,测试中....

  上面已经说过,我们方法的调用首先是取isa地址,取到isa地址后,isa地址与上 #0xffffffff8得到类对象地址。而此例中,我们获取到类对象后,然后&cls也取到了Animal类对象地址。从这里开始流程上就跟Animal实例对象调用print方法一致了,所以可以顺利调用print方法。

  为了我验证说的,我们首先实例化一个Aniaml对象

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
Animal *animal = [Animal new];

// 打印animal的地址
(lldb) p animal
(Animal *) $0 = 0x00000001d0007f00

// 这个地址保存的第一个位置就是isa,看过class结构都清楚isa是第一个成员
// isa的地址为0x000001a50000e59d
(lldb) x/4g 0x170000c80
0x1d0007f00: 0x000001a104b8659d 0x0000000000000000
0x1d0007f10: 0xbadd7e6de908bead 0x0000000000000000


// 与上& 0xffffffff8得到类对象地址
(lldb) p/x 0x000001a104b8659d & 0xffffffff8
(long) $1 = 0x0000000104b86598 // 记住这个类地址

// 我们打断点读取obj的地址为
(lldb) po obj
<Animal: 0x16b287978>

// obj保存的是cls指针的地址,实际上obj就是指向cls的。要想重新拿回cls对象的地址,需要看看这个地址保存的内容是什么
// 我们的类对象地址再次出现了
(lldb) x/4g 0x16b287978
0x16b287978: 0x0000000104b86598 0x00000001d0007f00
0x16b287988: 0x0000000104b84330 0x000000016b2879e0

  所以[(__bridge id)obj print];的时候,汇编代码中[x0]取的就是0x0000000104b86598,0x0000000104b86598 & 0xffffffff8还是0x0000000104b86598,剩下的步骤就跟[animal print]流程一样的,最后调用成功。

  我们的函数栈关系如下:

函数栈示意图

​ 那么,为什么打印结果会那么奇怪呢?我们先看下正常实例调用流程是怎么样的

函数栈示意图

  我们变量a指向 Animal实例对象所在的内存地址。调用self.name的时候,对象内存中第二个8个字节保存的就是变量_name的值,第一个8个字节肯定是isa。

  那么同样的,在我们这个例子里,我们的obj指向的是cls的地址,读取_name就是读取cls地址开始的第二个8个字节。

函数栈示意图

  我们证明下我们之前说的,断点打在print方法内,打印obj指向的内存的内容,0x000000010007e5a8是第一个8个字节,指的是isa指针;0x000000010007c340是第二个8个字节,就是我们字符串的内容,self.name读取的就是这块字符串。

1
2
3
4
5
6
7
8
(lldb) po self
<Animal: 0x16fd8f970>

(lldb) x/4g 0x16fd8f970
0x16fd8f970: 0x000000010007e5a8 0x000000010007c340
0x16fd8f980: 0x000000016fd8f9d8 0x0000000000000001
(lldb) po 0x000000010007c340
占位数据,测试中....

0x06 Super

  super在我们项目中用的熟的不能再熟了,比如调用父类的方法。这里我们先举个例子,有个继承自NSObject的类,下面的打印猜猜会是什么样的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface Animal()

@end

@implementation Animal

- (void)print {
NSLog(@"self class is %@\n", [self class]);
NSLog(@"self superclass is %@\n", [self superclass]);
NSLog(@"super class is %@\n", [super class]);
NSLog(@"super superclass is %@\n", [super superclass]);
}

@end

  这个细节很多人很容易忽视掉,我们发现[self class]、[self superclass]和[super class]、[super superclass]打印结果其实是一样的。

1
2
3
4
2018-05-22 20:06:17.169031+0800 testData[59785:156959813] self class is Animal
2018-05-22 20:06:17.169237+0800 testData[59785:156959813] self superclass is NSObject
2018-05-22 20:06:17.169353+0800 testData[59785:156959813] super class is Animal
2018-05-22 20:06:17.169484+0800 testData[59785:156959813] super superclass is NSObject

  那要解开这个原因,我们还是得先将代码转换一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void _I_Animal_print(Animal * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_Animal_cb9872_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_Animal_cb9872_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass")));

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_Animal_cb9872_mi_2, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Animal"))}, sel_registerName("class")));

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_Animal_cb9872_mi_3, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Animal"))}, sel_registerName("superclass")));
}

// 有个__rw_objc_super结构
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};

  调用super的时候消息接收者实际了一个__rw_objc_super对象,赋值的时候__rw_objc_super对象的object实际是self,superClass实际是class_getSuperclass(objc_getClass(“Animal”)),即NSObject。

1
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Animal"))}, sel_registerName("class"))

  消息发送通过objc_msgSendSuper,但是真机调试中我们发现,实际调用的是objc_msgSendSuper2

1
0x100799638 <+64>:  bl     0x100799f64               ; symbol stub for: objc_msgSendSuper2

  同样objc_msgSendSuper2是一个汇编函数,我们需要看下其实现过程,这里其实只要看一行就够了

1
2
3
libobjc.A.dylib`objc_msgSendSuper2:
; x0即__rw_objc_super对象,取出16位分别给x0和x16寄存器,我们知道__rw_objc_super装了两个对象一个object和superClass,所以x0寄存器里装的就是self
0x183c5cb00 <+0>: ldp x0, x16, [x0]

  根据函数调用约定,x0装的是消息接收者,所以实际消息接收者是self。所以[self class]和[super class],实际消息接收者都是self,那么这两个的区别就是[self class]从自己的方法表开始查找方法,而[super class]是直接从父类的方法表开始查找方法。

0x07 isKindOfClass & isMemberOfClass

  有如下测试代码,其结果如何?

1
2
3
4
5
6
7
8
9
10
Animal *animal = [Animal new];

BOOL a = [animal isKindOfClass:[animal class]];
BOOL b = [animal isMemberOfClass:[animal class]];
BOOL c = [animal isKindOfClass:[NSObject class]];
BOOL d = [animal isMemberOfClass:[NSObject class]];
BOOL e = [Animal isKindOfClass:[Animal class]];
BOOL f = [Animal isMemberOfClass:[Animal class]];
BOOL g = [Animal isKindOfClass:[NSObject class]];
BOOL h = [Animal isMemberOfClass:[NSObject class]];

  不卖关子,结果如下

1
2
3
4
5
6
7
8
① a = YES;
② b = YES;
③ c = YES;
④ d = NO;
⑤ e = NO;
⑥ f = NO;
⑦ g = YES;
⑧ h = NO;

  分析为什么结果之前,我们先看下源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}

// object_getClass如果传入类对象,得到元类对象
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

  源代码可以看出

1
2
3
4
5
6
7
实例方法:
isKindOfClass: 从自己类对象开始遍历父类对象,如果跟cls相等,就返回YES
isMemberOfClass:直接判断自己类对象与cls是否相等

类方法:
isKindOfClass: 从自己元类对象开始遍历父类元类对象,如果跟cls相等,就返回YES
isMemberOfClass:直接判断自己元类对象与cls是否相等

  根据上面得出的结论,我们的例子可以分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// animal是实例对象,其类对象是Animal,[animal class]即Animal,所以返回YES
BOOL a = [animal isKindOfClass:[animal class]];
// animal是实例对象,其类对象是Animal,[animal class]即Animal,所以返回YES
BOOL b = [animal isMemberOfClass:[animal class]];
// animal是实例对象,其类对象是Animal,遍历的时候回找到NSObject,所以返回YES
BOOL c = [animal isKindOfClass:[NSObject class]];
// animal是实例对象,其类对象是Animal,[NSObject class]即NSObject,两者不相等,所以返回NO
BOOL d = [animal isMemberOfClass:[NSObject class]];
// Animal是类对象,内部是比较元类对象,而[Animal class]是类对象,遍历只是找此元类对象的父类,两者肯定不相等,所以返回NO
BOOL e = [Animal isKindOfClass:[Animal class]];
// Animal是类对象,内部是比较元类对象,而[Animal class]是类对象,两者比较肯定不相等,所以返回NO
BOOL f = [Animal isMemberOfClass:[Animal class]];
// Animal是类对象,内部是比较元类对象,而[NSObject class]是类对象,遍历的时候查找元类对象的父类,然而又因为NSObject是基类,我们知道元类查找父类的时候,当找到元类的基类也没找到的话,会指向类对象,所以Animal元类对象查找super的时候找到NSObject元类对象的时候,发现还是不匹配,会继续匹配NSObject的类对象,而这时[NSObject class]就是NSObject类对象,所以相等,返回YES。
BOOL g = [Animal isKindOfClass:[NSObject class]];
// 因为没有遍历,元类对象跟类对象肯定不相等,所以返回NO
BOOL h = [Animal isMemberOfClass:[NSObject class]];

0x08 方法交换

  首先看下方法的结构信息,包含了函数实现的具体地址IMP。

1
2
3
4
5
6
struct method_t {
SEL name;
const char *types;
IMP imp;
......
};

  所以其实很好猜了,交换imp地址就能完成方法交换操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;

rwlock_writer_t lock(runtimeLock);
// 交换
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
// 缓存刷新下
flushCaches(nil);

updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}

0x09 Hook Block

  我们知道可以通过method_exchangeImplementations可以hook我们OC方法。那么如果我想要hook block又该如何实现呢?

  之前Block篇里我们知道block结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __xxxx_block_desc_0 {
size_t reserved;
size_t Block_size;
// 下面两个是可选的,是捕获对象的时候会被加入到这个结构
void (*copy)(struct __xxxx_block_impl_0*, struct __xxx_block_impl_0*);
void (*dispose)(struct __xxxx_block_impl_0*);
}

struct __xxx_block_impl_0 {
struct __block_impl impl;
struct __xxx_block_desc_0* Desc;
}

  通过官方的ABI,我们看到对其完整定义如下,signature描述了方法的参数类型和返回值类型等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Block_literal_1 {
void *isa;
int flags; // 标志位
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 {
unsigned long int reserved;
unsigned long int size;
// optional helper functions
void (*copy_helper)(void *dst, void *src);
void (*dispose_helper)(void *src);

const char *signature;
} *descriptor;
};

  所以我们可以自己定义一个这样的结构体,然后将block对象强转为这样的结构体,这样我们就可以拿到block对象的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typedef struct _blockDescription {
size_t reserved;
size_t block_size;
}*BlockDescription;

typedef struct _blockMemoryDescription {
void (*copy)(void *dst, void *src);
void (*dispose)(void *blockImpl);
}*BlockMemoryDescription;

typedef struct _blockSignDescription {
char *signature;
}*BlockSignDescription;

typedef struct _hBlock{
void *isa;
int flags;
int reserved;
void *FuncPtr;
BlockDescription description;
}HBlock, *PHBlock;

// 对应block对象的flag成员
enum {
BLOCK_HAS_COPY_DISPOSE = (1 << 25),
BLOCK_HAS_CTOR = (1 << 26), // helpers have C++ code
BLOCK_IS_GLOBAL = (1 << 28),
BLOCK_HAS_STRET = (1 << 29), // IFF BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30),
};

  我们知道进入消息转发流程后,会调用NSBlock的methodSignatureForSelector和hookForwardInvocation方法,NSBlock是官方的对象所以我们不能进行修改,所以需要替换methodSignatureForSelector和hookForwardInvocation。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define EXCHANGE_METHOD(originalM, replaceM) \
{ Class class = [NSObject class]; \
Class swizzledClass = [HookBlock class];\
SEL originalSelector = @selector(originalM);\
SEL swizzledSelector = @selector(replaceM); \
Method originalMethod = class_getInstanceMethod(class, originalSelector); \
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector); \
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); \
if (success) { \
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); \
} else { \
method_exchangeImplementations(originalMethod, swizzledMethod);\
}\
}

- (void)hookBlock:(id)block {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
EXCHANGE_METHOD(methodSignatureForSelector:, hookMethodSignatureForSelector:)
EXCHANGE_METHOD(forwardInvocation:, hookForwardInvocation:)
});
// 强转为自定义的block结构
PHBlock hBlock = (__bridge PHBlock)(block);
}

替换原来的block实现

  现在有这样一个需求,我拦截block后,需要替换其原本实现,我们原本定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, char * argv[]) {

void(^testBlock)(void) = ^{
NSLog(@"很高兴认识你");
};

HookBlock *hook = [HookBlock new];
[hook hookBlock:testBlock];

testBlock();

return 0;
}

  如果正常调用,将会打印很高兴认识你,我们现在想要改变其打印为很讨厌认识你。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)hookBlock:(id)block {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
EXCHANGE_METHOD(methodSignatureForSelector:, hookMethodSignatureForSelector:)
EXCHANGE_METHOD(forwardInvocation:, hookForwardInvocation:)
});
PHBlock hBlock = (__bridge PHBlock)(block);

Method replaceMethod = class_getInstanceMethod([self class], @selector(replaceBlock));
IMP replaceMethodIMP = method_getImplementation(replaceMethod);
hBlock->FuncPtr = replaceMethodIMP;
}

- (void)replaceBlock {
NSLog(@"很讨厌认识你");
}

  运行后,可以看到成功替换了实现

1
2018-05-23 20:55:31.041306+0800 testData[29327:25955322] 很讨厌认识你

打印block的参数

  我们再次改变需求,这次我们想要先打印下block的参数,然后再调用其实现方法。

  先看下初始化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char * argv[]) {

void(^testBlock)(int a, int b) = ^(int a, int b){
NSString *result = [NSString stringWithFormat:@"%d + %d = %d", a, b, a + b];
NSLog(@"%@", result);
};

HookBlock *hook = [HookBlock new];
[hook hookBlock:testBlock];

testBlock(1, 2);

return 0;
}

  我们首先拦截block,然后进入消息转发流程,根据NSMethodSignature可以获得参数类型,根据NSInvocation可以获得参数值,所以我们的实现如下

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
- (void)hookBlock:(id)block {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
EXCHANGE_METHOD(methodSignatureForSelector:, hookMethodSignatureForSelector:)
EXCHANGE_METHOD(forwardInvocation:, hookForwardInvocation:)
});
PHBlock hBlock = (__bridge PHBlock)(block);

// 保存原来实现函数地址
objc_setAssociatedObject(block, "originImp", [NSValue valueWithPointer:hBlock->FuncPtr], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 指向进入消息转发流程的函数地址
hBlock->FuncPtr = _objc_msgForward;
}

- (NSMethodSignature *)hookMethodSignatureForSelector:(SEL)aSelector {
PHBlock block = (__bridge PHBlock)self;

uint8_t *desc = (uint8_t *)block->description;
desc += sizeof(struct _blockDescription);

if(block->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct _blockMemoryDescription);
}

BlockSignDescription signDesc = (BlockSignDescription)desc;
const char * signature = signDesc->signature;
return [NSMethodSignature signatureWithObjCTypes:signature];
}

- (void)hookForwardInvocation:(NSInvocation *)anInvocation {
PHBlock block = (__bridge PHBlock)self;

NSMethodSignature *methodSignature = [anInvocation methodSignature];
NSInteger numberOfArguments = [methodSignature numberOfArguments];

for (NSInteger i = 1; i < numberOfArguments; i++) {
const char *argTypeName = [methodSignature getArgumentTypeAtIndex:i];

switch (argTypeName[0]) {
case '@': {
__unsafe_unretained id type;
[anInvocation getArgument:&type atIndex:i];
NSLog(@"%@", [NSString stringWithFormat:@"第%@个参数值为%@", @(i), type]);
}break;

#define DATA_TYPE_ARG(_typeSymbol, _type) \
case _typeSymbol: { \
_type type; \
[anInvocation getArgument:&type atIndex:i]; \
NSString *result = [NSString stringWithFormat:@"第%@个参数值为%@", @(i), @(type)]; \
NSLog(@"%@", result); \
}break;


DATA_TYPE_ARG('i', int);
DATA_TYPE_ARG('I', unsigned int);
DATA_TYPE_ARG('l', long);
DATA_TYPE_ARG('L', unsigned long);
DATA_TYPE_ARG('q', long long);
DATA_TYPE_ARG('Q', unsigned long long);
DATA_TYPE_ARG('f', float);
DATA_TYPE_ARG('d', double);
DATA_TYPE_ARG('B', BOOL);
DATA_TYPE_ARG('c', char);
DATA_TYPE_ARG('C', unsigned char);
}
}

// 重新调用自己的实现
NSValue *pointerValue = objc_getAssociatedObject(anInvocation.target, "originImp");
block->FuncPtr = pointerValue.pointerValue;
[anInvocation invoke];
}

浅谈ARM64汇编

发表于 2018-05-15 | 分类于 逆向

0x01 精简指令集RISC(Reduced Instruction Set Computer)

  ARM是RISC的代表,RISC具有以下几个特点:

  • 简单的指令集:只提供很有限的操作
  • 等长指令集:执行指令速度快且性能稳定,可将一条指令分割成若干个进程或线程,交由多个处理器同时执行。
  • Load/Store架构:CPU并不会对内存中的数据进行操作,所有的计算都要求在寄存器中完成,而寄存器和内存的通信由Load/Store指令完成。
  • 更多的寄存器:基于RISC的处理器具有更多的通用寄存器可以使用,且每个寄存器都可以进行数据存储或寻址。
  • 效率更高:RISC指令集能够非常有效地适合于采用流水线、超流水线和超标量技术,从而实现指令集并行操作,提高处理器的性能。

0x02 寄存器

1. 简介

  寄存器是CPU的一个组成部分,里面存放着指令、数据和地址等供CPU计算使用,速度比内存快。寄存器分为通用寄存器和专用寄存器。

  • 通用寄存器

    • 提供了31个64位通用寄存器,x0 ~ x30。

    • 可以通过w0 ~ w30来访问这31个64位寄存器的低32位,写入时会将高32位清零。

      注:后面文章中wn其实也可以代表xn,它们之间只是位数不同。

  • 浮点寄存器

    • v0 ~ v31,一共有32个浮点寄存器,每个寄存器大小是128位。分别可以用的方式来访问不同的位数。可以通过Bn、Hn、Sn、Dn、Qn来访问不同的位数。

      Bn:8位

      Hn:16位

      Sn:32位

      Dn:64位

      Qn:128位

  • 特殊寄存器

    • 程序计数器

      • pc,保存着当前CPU执行指令的地址。不能用作算数指令的源或目的地以及用作加载或存储指令。
    • 堆栈指针

      • sp,即x31,指向堆栈的顶部。sp不能被大多数指令引用, 但一些算术指令,例如ADD指令,可以读写当前的堆栈指针来调整函数中的堆栈指针。每个异常级别都有一个专用的SP寄存器。
      • fp,即x29,帧指针,指向当前frame的栈底,也就是高地址。
    • 链接寄存器

      • lr,即x30,存储着函数的返回地址。
    • 程序状态寄存器

      在汇编中通过状态寄存器来控制分支的执行。

      • cpsr:与其他寄存器不太一样,其他寄存器用来存储数据的,但是这个寄存器是,按位起作用的,每一位都有专门的含义。
      • spsr:当发生异常时,cpsr会存入spsr直到异常恢复再复制回cpsr。

      状态寄存器示意图1

      状态寄存器示意图2

2. 模式与异常等级

2.1 模式

  • 用户模式(USR): ARM处理器正常程序执行状态。
  • 快速中断模式(FIQ): 高速数据传输或通道处理。
  • 外部中断模式(IRQ): 通用的中断处理。
  • 管理模式(supervisor): 操作系统使用的保护模式。
  • 数据访问终止模式(abort): 当数据或指令预取终止时进入该模式,可用于虚拟存储及存储保护。
  • 系统模式(system): 运行具有特权的操作系统任务。
  • 未定义指令终止模式(UND):当未定义的指令执行时进入该模式。
  • hyp:用于虚拟化扩展。
  • monitor:用于Security扩展。

2.2 异常等级

  • EL0:非特权用户模式
  • EL1:操作系统内核
  • EL2:Hypervisor,虚拟扩展
  • EL3:Secure Monitor,安全扩展。实现EL0和EL1的Secure和Non-Secure之间的切换,可起到物理屏障安全隔离作用。

2.3 模式与异常等级之间的关系

  • user模式:只能在EL0执行

  • monitor模式:只能在Secure的EL3执行

  • hyp模式:只能在Non-Secure的EL2执行,虚拟机。

  • system,supervisor,abort,undefined,IRQ,FIQ模式:依赖于Secure模式

    状态寄存器示意图1

0x02 指令

  我们首先工程里创建一个.h文件和一个.s文件

创建的文件示意图

  接着,在头文件声明一个test方法

头文件示意图

  汇编文件里,声明一个global,表示_test那块指令是可以暴露出给外面调用的

汇编文件示意图

  准备工作结束后,以后使用的时候就加入asm.h头文件就可以调用相关函数。接着,我们开始认识一些常用的指令

mov

  mov指令的格式为:mov{条件}{s} 目的寄存器,源操作数

  mov指令可完成从另一个寄存器、被移位的寄存器或将一个立即数加载到目的寄存器。其中s选项决定指令的操作是否影响CPSR中条件标志位的值,当没有s时指令不更新CPSR中条件标志位的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
.text
.global _test

_test:
mov w1, 8
mov w2, w1
ret

// 调试结果
(lldb) register read/d w1
w1 = 8
(lldb) register read/d w2
w2 = 8

mvn

  mvn指令的格式为:mvn{条件}{s} 目的寄存器,源操作数

  mvn指令可完成从另一个寄存器被移位的寄存器或将一个立即数加载到目的寄存器。与mov指令不同之处是在传送之前按位被取反了,即把一个被取反的值传送到目的寄存器中。 其中s选项决定指令的操作是否影响CPSR中条件标志位的值,当没有s时指令不更新CPSR中条件标志位的值。

1
2
3
4
5
6
7
8
9
10
11
.text
.global _test

_test:
mov w1, 0
mvn w2, w1
ret

// 调试结果
(lldb) register read/d w2
w2 = -1

add

  add指令的格式为:add{条件}{s} 目的寄存器,操作数1,操作数2

  add指令用于把两个操作数相加,并将结果存放到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数

1
2
3
4
5
6
7
8
9
10
11
12
.text
.global _test

_test:
mov w1, 2
mov w2, 4
add w0, w1, w2
ret

// 调试结果
(lldb) register read/d w0
w0 = 6

adc

  adc指令的格式为:adc{条件}{s} 目的寄存器,操作数1,操作数2

  adc指令用于把两个操作数相加,再加上CPSR中的C条件标志位的值,并将结果存放到目的寄存器中。它使用一个进位标志位,这样就可以做比64位大的数的加法,注意不要忘记设置s后缀来更改进位标志。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。

sub

  sub指令的格式为:sub{条件}{s} 目的寄存器,操作数1,操作数2

  sub指令用于把操作数1减去操作数2,并将结果存放到目的寄存器。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。该指令可用于有符号数或无符号数的减法运算。

1
2
3
4
5
6
7
8
9
10
11
12
.text
.global _test

_test:
mov w1, 4
mov w2, 2
sub w0, w1, w2
ret

// 调试结果
(lldb) register read/d w0
w0 = 2

sbc

  sbc指令的格式为:sbc{条件}{s} 目的寄存器,操作数1,操作数2

  sbc指令用于把操作数1减去操作数2,再减去CPSR中的C条件标志位的反码,并将结果存放到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。该指令使用进位标志来表示借位,这样就可以做大于32位的减法,注意不要忘记设置s后缀来更改进位标志。该指令可用于有符号数或无符号数的减法运算。

mul

  mul指令的格式为:mul{条件}{s} 目的寄存器,操作数1,操作数2

  mul指令完成将操作数1与操作数2的乘法运算,并把结果放置到目的寄存器中,同时可以根据运算结果设置CPSR中相应的条件标志位。其中,操作数1和操作数2均为64位的有符号或无符号数。

1
2
3
4
5
6
7
8
9
10
11
12
.text
.global _test

_test:
mov w1, 4
mov w2, 2
mul w0, w1, w2
ret

// 调试结果
(lldb) register read/d w0
w0 = 8

and

  and指令的格式为:and{条件}{s} 目的寄存器,操作数1,操作数2

  and指令用于在两个操作数上进行逻辑与运算,并把结果放置到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。该指令常用于屏蔽操作数1的某些位。

1
2
3
4
5
6
7
8
9
10
11
.text
.global _test

_test:
mov w1, 4
and w0, w1, 3 ; 保持w1的0,1位,其余位清零
ret

// 调试结果
(lldb) register read/d w0
w0 = 0

orr

  orr指令的格式为:orr{条件}{s} 目的寄存器,操作数1,操作数2

  orr指令用于在两个操作数上进行逻辑或运算,并把结果放置到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。该指令常用于设置操作1的某些位。

1
2
3
4
5
6
7
8
9
10
11
.text
.global _test

_test:
mov w1, 4
orr w0, w1, 3 ; 设置w1的0,1位,其余位保持不变
ret

// 调试结果
(lldb) register read/d w0
w0 = 7

eor

  eor指令的格式为:eor{条件}{s} 目的寄存器,操作数1,操作数2

  eor指令用于在两个操作数上进行逻辑异或运算,并把结果放置到目的寄存器中操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。该指令常用于反转操作数1的某些位。

1
2
3
4
5
6
7
8
9
10
11
.text
.global _test

_test:
mov w1, 4
eor w0, w1, 3 ; 反转w1的0,1位,其余位保持不变
ret

// 调试结果
(lldb) register read/d w0
w0 = 7

tst

  tst指令的格式为:tst{条件} 操作数1,操作数2

  tst指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位与运算,并根据运算结果更新CPSR中条件标志位的值。操作数1是要测试的数据,而操作数2是一个位掩码,该指令一般用来检测是否设置了特定的位。

1
2
3
4
5
6
7
8
9
10
11
12
13
.text
.global _test

_test:
mov w1, 0
tst w1, 1
ret

// 调试结果
(lldb) register read/t cpsr
cpsr = 0b01100000000000000000000000000000 ;执行tst命令之前
(lldb) register read/t cpsr
cpsr = 0b01000000000000000000000000000000 ;执行tst命令之后

str

  str指令的格式为:tst{条件} 源寄存器,<存储器地址>

  str指令用于从源寄存器中将一个64位或32位(看使用rn还是wn)的字节数据传送到存储器中。

  str的示例与ldr一起说明。

ldr

  ldr指令的格式为:ldr{条件} 目的寄存器,<存储器地址>

  ldr指令用于从存储器中将一个64位或32位(看使用rn还是wn)的字节数据传送到目的寄存器中。当程序计数器pc作为目的寄存器时,指令从存储器中读取的字节数据被当做目的地址,从而可以实现程序流程的跳转。

1
2
3
4
5
6
7
8
9
10
11
12
.text
.global _test

_test:
mov w1, 4
str w1, [sp, 8]
ldr w0, [sp, 8]
ret

// 调试结果
(lldb) register read/d w0
w0 = 4

b

  b指令的格式为:b{条件} 目标地址

  b指令是最简单的跳转指令。一个遇到一个b指令,ARM处理器将立即跳转到给定的目标地址,从那里继续执行。

1
2
3
4
5
6
7
8
9
10
11
.text
.global _test

_test:
b label
mov w2, 5

label:
mov w0, 3
mov w1, 4
ret

bl

  b指令的格式为:bl{条件} 目标地址

  同样是跳转指令,但是在跳转之前,会在lr寄存器中保存pc的当前内容,因此,可以通过将lr的内容重新加载到pc中,来返回到跳转指令之后的那个指令处执行。

0x03 栈

一个简单汇编代码

  首先,我们看下最简单的汇编是什么样的,我们定义两个局部变量:

1
2
3
4
5
int main(int argc, char * argv[]) {
int a = 2;
int b = 3;
return a + b;
}

  我们Xcode的Debug -> Debug Workflow -> Always Show Disassembly,打开始终显示汇编,这样断点的时候就直接以汇编代码呈现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
main:
0x102eba73c <+0>: sub sp, sp, #0x20 ; =0x20
0x102eba740 <+4>: orr w8, wzr, #0x3
0x102eba744 <+8>: orr w9, wzr, #0x2
0x102eba748 <+12>: str wzr, [sp, #0x1c]
0x102eba74c <+16>: str w0, [sp, #0x18]
0x102eba750 <+20>: str x1, [sp, #0x10]
0x102eba754 <+24>: str w9, [sp, #0xc]
0x102eba758 <+28>: str w8, [sp, #0x8]
-> 0x102eba75c <+32>: ldr w8, [sp, #0xc]
0x102eba760 <+36>: ldr w9, [sp, #0x8]
0x102eba764 <+40>: add w0, w8, w9
0x102eba768 <+44>: add sp, sp, #0x20 ; =0x20
0x102eba76c <+48>: ret

  当然我们也可以直接用命令进行转换:

xcrun -sdk iphoneos clang -S -arch arm64 main.m

  我们需要知道iOS是小端模式,所以汇编中的栈是由高地址向低地址传递数据,栈底是高地址,栈顶是低地址。在编译器生成汇编时,首先会计算需要的栈空间大小,并利用sp指针向低地址开辟相应的空间,在上面的汇编代码中,需要开辟0x20(即32)字节的栈空间。即,刚开始的栈如下图:

初始化的栈示意图

  这个例子中,我们会一行一行阅读汇编指令

  1. sub sp, sp, #0x20 ; sp指针往低地址移动32字节,sp指针指向了新地址

移动32字节的栈示意图

  1. orr w8, wzr, #0x3 ; 3与零寄存器取或,得到的值存到w8寄存器,这里就是把3放到w8寄存器
  2. orr w9, wzr, #0x2 ; 2与零寄存器取或,得到的值存到w9寄存器,这里就是把2放到w9寄存器
  3. str wzr, [sp, #0x1c] ; 从sp地址+28个字节为基地址开始的4个字节内的数据用零寄存器内的数据填充
  4. str w0, [sp, #0x18] ; 从sp地址+24个字节为基地址开始的4个字节内的数据用w0寄存器内的数据填充
  5. str x1, [sp, #0x10] ; 从sp地址+16个字节为基地址开始的8个字节内的数据用x1寄存器内的数据填充
  6. str w9, [sp, #0xc] ; 从sp地址+12个字节为基地址开始的4个字节内的数据用w9寄存器内的值(即2)填充
  7. str w8, [sp, #0x8] ; 从sp地址+8个字节为基地址开始的4个字节内的数据用w8寄存器内的值(即3)填充
  8. ldr w8, [sp, #0xc] ; 把从sp地址+12个字节为基地址开始的4个字节的数据放到w8寄存器
  9. ldr w9, [sp, #0x8] ; 把从sp地址+8个字节为基地址开始的4个字节的数据放到w9寄存器
  10. add w0, w8, w9 ; 把w8和w9寄存器内的值相加,保存到w0寄存器,即返回操作,return的值保存在w0寄存器。
  11. add sp, sp, #0x20 ; sp指针往高地址移动32个字节,即回到最初的位置

数据入栈的栈示意图

  这里还需要注意的是,我们的函数返回结果是保存在寄存器w0(如果是64位则是x0)中的,但也不是也只有这一个寄存器可以用作保存返回结果,w0~w7一共8个寄存器都是可以使用的。而且为什么需要开启32字节的栈空间,明明我们用不到那么多,那是因为ARM规定sp必须16字节对齐。

堆栈平衡

  前面的示例代码中,或许你对于代码开始时的sub sp, sp, #0x20和结束时的add sp, sp, #0x20感到好奇,其实这样做就是维持栈平衡,特别最后的add sp, sp, #0x20表示的就退栈,前面开辟了多少空间,函数调用结束的时候就需要恢复成函数调用前的样子。如果每次只有开辟空间,没有退栈操作,那么很快我们的栈将会被使用完毕。

  这也就解释了,函数内的为什么变量都存在栈区,并且为什么函数调用结束后,函数内的变量就会被释放掉。

函数的参数传递和调用约定

  那么在汇编代码中,参数是如何进行传递的?我们通过下面这段示例来了解下:

1
2
3
4
5
6
7
int add(int a, int b) {
return a + b;
}

int main(int argc, char * argv[]) {
return add(2, 3);
}

  将add函数转换成汇编代码:

1
2
3
4
5
6
7
8
9
add:
0x100c8e718 <+0>: sub sp, sp, #0x10 ; =0x10
0x100c8e71c <+4>: str w0, [sp, #0xc]
0x100c8e720 <+8>: str w1, [sp, #0x8]
-> 0x100c8e724 <+12>: ldr w0, [sp, #0xc]
0x100c8e728 <+16>: ldr w1, [sp, #0x8]
0x100c8e72c <+20>: add w0, w0, w1
0x100c8e730 <+24>: add sp, sp, #0x10 ; =0x10
0x100c8e734 <+28>: ret

  我们在add函数内部打下断点,通过lldb的register指令读取下x0和x1寄存器的值:

1
2
3
4
(lldb) register read/d w0
w0 = 2
(lldb) register read/d w1
w1 = 3

  可以发现,函数参数是通过寄存器进行传递的,那么是不是如果函数参数很多很多,所有寄存器都可以用来保存函数参数?我们把示例改下:

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
int add(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m) {
return a + b;
}

int main(int argc, char * argv[]) {

return add(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
}

// add函数对应汇编如下:
add:
0x10496e694 <+0>: sub sp, sp, #0x40 ; =0x40
0x10496e698 <+4>: ldr w8, [sp, #0x50]
0x10496e69c <+8>: ldr w9, [sp, #0x4c]
0x10496e6a0 <+12>: ldr w10, [sp, #0x48]
0x10496e6a4 <+16>: ldr w11, [sp, #0x44]
0x10496e6a8 <+20>: ldr w12, [sp, #0x40]
0x10496e6ac <+24>: str w0, [sp, #0x3c]
0x10496e6b0 <+28>: str w1, [sp, #0x38]
0x10496e6b4 <+32>: str w2, [sp, #0x34]
0x10496e6b8 <+36>: str w3, [sp, #0x30]
0x10496e6bc <+40>: str w4, [sp, #0x2c]
0x10496e6c0 <+44>: str w5, [sp, #0x28]
0x10496e6c4 <+48>: str w6, [sp, #0x24]
0x10496e6c8 <+52>: str w7, [sp, #0x20]
-> 0x10496e6cc <+56>: ldr w0, [sp, #0x3c]
0x10496e6d0 <+60>: ldr w1, [sp, #0x38]
0x10496e6d4 <+64>: add w0, w0, w1
0x10496e6d8 <+68>: str w12, [sp, #0x1c]
0x10496e6dc <+72>: str w9, [sp, #0x18]
0x10496e6e0 <+76>: str w10, [sp, #0x14]
0x10496e6e4 <+80>: str w11, [sp, #0x10]
0x10496e6e8 <+84>: str w8, [sp, #0xc]
0x10496e6ec <+88>: add sp, sp, #0x40 ; =0x40
0x10496e6f0 <+92>: ret

  事实上,能用作保存函数参数的寄存器只有8个分别是w0~w7,其他都是保存到栈上了,加上前面已经提到过的函数返回结果保存在w0(当然你也可以写在w0~w7任意一个)。这种调用约定也称为ATPCS。

函数跳转

  回到之前的例子:

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

int main(int argc, char * argv[]) {
return add(2, 3);
}

main:
// 开辟32字节空间
0x10280e738 <+0>: sub sp, sp, #0x20
// 保存现场:x29表示fp寄存器,x30表示lr寄存器。从sp地址往高地址偏移16个字节的地址开始,用16个字节保存fp和lr寄存器。前8个字节保存fp寄存器,后8个字节保存lr寄存器。
0x10280e73c <+4>: stp x29, x30, [sp, #0x10]
// fp寄存器移动到从sp地址往高地址偏移16个字节的地址
0x10280e740 <+8>: add x29, sp, #0x10 ; =0x10
0x10280e744 <+12>: orr w8, wzr, #0x2
0x10280e748 <+16>: orr w9, wzr, #0x3
0x10280e74c <+20>: stur wzr, [x29, #-0x4]
0x10280e750 <+24>: str w0, [sp, #0x8]
0x10280e754 <+28>: str x1, [sp]
// x8和x9寄存器内的值保存到x0和x1寄存器上,之前说过函数参数是通过前八位寄存器传递的
0x10280e758 <+32>: mov x0, x8
0x10280e75c <+36>: mov x1, x9
0x10280e760 <+40>: bl 0x10280e718 ; add at main.m:12
// 恢复现场:从sp地址往高地址偏移16个字节的地址开始的16个字节里面的数据,前8个字节保存到fp寄存器,后8个字节保存到lr寄存器
0x10280e764 <+44>: ldp x29, x30, [sp, #0x10]
// 退栈
0x10280e768 <+48>: add sp, sp, #0x20 ; =0x20
0x10280e76c <+52>: ret

add:
0x10280e718 <+0>: sub sp, sp, #0x10 ; =0x10
0x10280e71c <+4>: str w0, [sp, #0xc]
0x10280e720 <+8>: str w1, [sp, #0x8]
-> 0x10280e724 <+12>: ldr w0, [sp, #0xc]
0x10280e728 <+16>: ldr w1, [sp, #0x8]
0x10280e72c <+20>: add w0, w0, w1
0x10280e730 <+24>: add sp, sp, #0x10 ; =0x10
0x10280e734 <+28>: ret

  上面的代码流程大致是下面这样的:

数据入栈的栈示意图

  代码中,一开始做了保存现场的操作。首先讲下为什么要保存fp寄存器里的值?因为fp寄存器可能正在被之前一个函数用着,如果不保存,对调用本函数的函数其栈区就乱了,因为我们要知道的是sp与fp之间的空间就是函数的栈空间,所以必须保存已保证栈的正确性。

  其次,为什么需要保存lr寄存器里的值,我们知道lr保存着函数返回地址,有了它,函数才能知道我结束后该跳转到哪里,假设我们不保存会有什么后面,但我们执行bl 0x10280e718这条指令的时候,bl指令会将bl下一条指令的地址保存进lr寄存器里,所以在这个例子里,bl执行完毕后会来到0x10280e764这个位置继续往后执行,如果后面没做恢复操作,在执行到ret指令后,会又来到0x10280e764这个位置一直死循环。所以只有之前保存过lr寄存器值,然后再后面又恢复数据给lr寄存器,ret指令才能正确跳转。

  需要提一个概念,叶子函数和非叶子函数

  • 叶子函数:函数内部不再调用其他函数的函数
  • 非叶子函数:函数内部还在调用其他函数的函数

深入分析block

发表于 2018-05-13 | 分类于 iOS

0x01 Block结构

   我们通过命令将Objective-C代码转换为C++代码,看下底层block的结构

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

  测试代码如下:

1
2
3
4
5
6
7
8
int main(int argc, char * argv[]) {

^{
NSLog(@"Hello Block");
};

return 0;
}

  转换后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

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; // fp就是前面传进来的__main_block_func_0
Desc = desc; // desc就是前面传进来的__main_block_desc_0_DATA
}
};

// block里面的函数,被解析成一个独立的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_a5d38c_mi_0);
}

// block的描述信息,包括block大小
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)};

// main函数
int main(int argc, char * argv[]) {

// 结构体初始化并赋值
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

return 0;
}

  main函数中初始化了__main_block_impl_0结构体,通过与结构体同名的构造函数将函数实现(__main_block_func_0)地址和函数描述(__main_block_desc_0_DATA)地址传递进了__main_block_impl_0结构。

  我们的block函数被转换成了单独了一个函数__main_block_func_0并赋值给block结构体内FuncPtr成员,这样做我们想必也知道以后要是调用这个函数,直接调用结构体的FuncPtr成员即可。为了证明这点,我们将测试代码改成如下:

1
2
3
4
5
6
7
8
9
10
int main(int argc, char * argv[]) {

void(^helloBlock)(void) = ^{
NSLog(@"Hello Block");
};

helloBlock();

return 0;
}

  我们继续转换代码,这次主要看下main函数里面的实现:

1
2
3
4
5
6
7
8
9
int main(int argc, char * argv[]) {

void(*helloBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

// 使用结构体的FuncPtr成员来调用函数
((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

return 0;
}

  并且在前面的结构体内发现了isa,我们知道只要OC对象都会有一个isa指针,所以我们可以知道block本质上也是一个OC对象。

0x02 捕获变量

1. auto修饰词

  我们知道局部变量前面默认都是有个auto修饰符的,所以我们写下如下测试代码:

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
int main(int argc, char * argv[]) {

int age = 10;
void(^helloBlock)(void) = ^{
NSLog(@"小明今天%d岁", age);
};

helloBlock();

return 0;
}

// 转换代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age; // 结构体多了一个age成员
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age)/* 成员变量的age在这里被赋值,初始化的时候外部会传值 */ {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // 使用的是__main_block_impl_0成员age

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_942932_mi_0, age);
}

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


int main(int argc, char * argv[]) {

int age = 10;
// 初始化,并把10传进结构体
void(*helloBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

return 0;

}

  在转换代码里,结构体多了个名为age的成员变量,并且main函数里对block结构体初始化的时候就把10这个值传递进去了,取来用的时候直接调取结构体age这个成员变量。初始化完成后,结构体内的age成员和main函数里定义的age变量其实已经没啥关系了,所以这也是为什么,之后修改变量age这个值,block内读取age的值是不会变的。

2. static修饰词

  修改测试代码,加上static修饰词

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
int main(int argc, char * argv[]) {

static int age = 10;
void(^helloBlock)(void) = ^{
NSLog(@"小明今天%d岁", age);
};

helloBlock();

return 0;
}

// 转换后的代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *age; // 同样是新增了age成员,不同的是这次是个指针
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *age = __cself->age;

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_5c5d34_mi_0, (*age));
}

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


int main(int argc, char * argv[]) {

static int age = 10;
// 初始化的时候,也是直接将地址传递进去
void(*helloBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));

((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

return 0;

}

  这次,同样新增了一个age的成员变量,但是跟之前不同的是这次是一个指针成员,初始化的时候直接将变量的地址传递了进去,我们知道如果使用指针传递,我们可以随时随地的修改这个变量内的值,并且读取的时候也是被改变后的值,因为block内部直接访问的是变量的地址。所以,这里就跟前面不一样了,后面修改了值,调用block后,block内部读取变量的值是修改后的值。

3. 全局变量

  测试代码修改如下:

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
int age = 10;

int main(int argc, char * argv[]) {

void(^helloBlock)(void) = ^{
NSLog(@"小明今天%d岁", age);
};

helloBlock();

return 0;
}

// 转换后的代码:
int age = 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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_4e4a2d_mi_0, age);
}

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)};
int main(int argc, char * argv[]) {

void(*helloBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

return 0;



}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

  转换后的代码里,全局变量同样还是全局变量,并没有被吸收进结构体内。既然是也是全局变量,那么这个变量也是想怎么改就怎么改,block内部读取的值也是最新被赋值的值。

property属性

  这次将代码修改为如下,方便可以添加属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@interface ViewController ()

@property (nonatomic, assign) int age;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

void(^helloBlock)(void) = ^{
NSLog(@"小明今天%d岁", self.age);
};

helloBlock();
}
@end

// 转换后的代码
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
ViewController *self; // 多了一个控制器自己的成员
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
ViewController *self = __cself->self; // bound by copy
// 通过runtime读取成员变量的值,所以也可以保证被修改的值,后面block内部读取的时候也是读取最新的
NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_ViewController_bc83d3_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
}
static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __ViewController__viewDidLoad_block_dispose_0(struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0), __ViewController__viewDidLoad_block_copy_0, __ViewController__viewDidLoad_block_dispose_0};

// OC方法默认会传两个参数:self和SEL
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));

// 初始化Block的时候传进去了self
void(*helloBlock)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, 570425344));

((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);
}

  使用属性的时候,block结构体多了一个self的成员变量,初始化的时候将控制器实例传递了进去。读取值的时候通过消息发送机制获取最新的成员变量的值。

0x03 block类型

1. NSGlobalBlock

  block内部没有调用auto修饰符变量的block都是NSGlobalBlock类型。

  这里不使用clang转换代码,因为block都是运行时确定的,所以通过打断点确定block类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main(int argc, char * argv[]) {

void(^helloBlock)(void) = ^{
NSLog(@"Hello World");
};

helloBlock();

return 0;
}

// lldb指令打印:
(lldb) po [helloBlock class]
__NSGlobalBlock__

// 又或者使用static修饰符
int main(int argc, char * argv[]) {
static int age = 10;
void(^helloBlock)(void) = ^{
NSLog(@"小明今天%d岁", age);
};

helloBlock();

return 0;
}

// lldb指令打印:
(lldb) po [helloBlock class]
__NSGlobalBlock__

2. NSStackBlock

  首先,我们需要将Xcode里ARC改为MRC

ARC修改

  测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char * argv[]) {
int age = 10;
void(^helloBlock)(void) = ^{
NSLog(@"小明今天%d岁", age);
};

helloBlock();
}

// lldb指令打印:
(lldb) po [helloBlock class]
__NSStackBlock__

  也就是block内部使用了auto修饰符的都是 NSStackBlock类型。

3. NSMallocBlock

  NSStackBlock调用copy得到的就是NSMallocBlock类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char * argv[]) {
int age = 10;
void(^helloBlock)(void) = [^{
NSLog(@"小明今天%d岁", age);
} copy];

helloBlock();

return 0;
}

// lldb指令打印
(lldb) po [helloBlock class]
__NSMallocBlock__

0x04 Copy

  在ARC下,下列情况会自动将block从栈区复制到堆区。

  • block作为返回值
  • 赋值给strong强引用对象
  • 在Cocoa里作为方法的参数(包括GCD)。

  所以下面的情况,肯定不会自动复制到堆区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char * argv[]) {
int age = 10;
__weak void(^helloBlock)(void) = ^{
NSLog(@"小明今天%d岁", age);
};

helloBlock();

return 0;
}

// lldb指令
(lldb) po [helloBlock class]
__NSStackBlock__

  那么,如果对Block的三种类型进行copy操作会有什么效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// NSGlobalBlock
int main(int argc, char * argv[]) {
void(^helloBlock)(void) = [^{
NSLog(@"Hello World");
} copy];

helloBlock();

return 0;
}

// lldb指令
(lldb) po [helloBlock class]
__NSGlobalBlock__

// NSStackBlock上面已经说过
// NSMallocBlock还是NSMallocBlock

   所以三种block类型使用copy的结果如下:

  • NSGlobalBlock使用copy还是NSGlobalBlock
  • NSStackBlock使用copy,变为NSMallocBlock
  • NSMallocBlock使用copy还是NSMallocBlock,只是引用计数+1

  我们知道ARC下,会自动将block从栈区拷贝到堆区,同时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
int main(int argc, char * argv[]) {
Animal *a = [Animal new];
void(^helloBlock)(void) = ^{
NSLog(@"小明今天%d岁", a.age);
};

helloBlock();

return 0;
}

// 转换代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Animal *__strong a; // __strong 修饰,强引用
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Animal *__strong _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
Animal *__strong a = __cself->a; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_d37033_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)a, sel_registerName("age")));
}

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

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

// 描述里面多了copy函数和销毁函数
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};


int main(int argc, char * argv[]) {
Animal *a = ((Animal *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Animal"), sel_registerName("new"));
void(*helloBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, 570425344));

((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

return 0;

}

  copy函数内部会调用_Block_object_assign函数,这个函数会根据auto 对象的修饰符(strong,weak,unsafe_unretained)做出相应操作,形成强引用还是弱引用。这里先讲auto修饰的对象,当block被拷贝到堆区的时候,其内部也会调用__main_block_copy_0函数将内部的Animal *a对象也拷贝到堆区,同时引用计数+1,我们看下源码,_Block_object_assign函数第三个参数flag,这里是3,即BLOCK_FIELD_IS_OBJECT。其他的情况在__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
enum {

// 一个对象
BLOCK_FIELD_IS_OBJECT = 3,
// 一个block变量
BLOCK_FIELD_IS_BLOCK = 7,
// 被__block修饰的变量
BLOCK_FIELD_IS_BYREF = 8,
// 被__weak修饰的变量,只能被辅助copy函数使用
BLOCK_FIELD_IS_WEAK = 16,
// block辅助函数调用,仅赋值,内部实现不进行retain或copy
BLOCK_BYREF_CALLER = 128
};

// flag上层传了3,即BLOCK_FIELD_IS_OBJECT
void _Block_object_assign(void *destAddr, const void *object, const int flags) {
......
// 也就是来到这里
else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) {
// 持有对象
_Block_retain_object(object);
_Block_assign((void *)object, destAddr);
}
}

  我们可以看到其内部通过_Block_retain_object函数强引用了Animal *a对象。那么还有一种__weak修饰的变量,代码如下:

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
int main(int argc, char * argv[]) {
__weak Animal *a = [Animal new];
void(^helloBlock)(void) = ^{
NSLog(@"小明今天%d岁", a.age);
};

helloBlock();
return 0;
}

// 转换后的代码:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Animal *__weak a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Animal *__weak _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
Animal *__weak a = __cself->a; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_39c8fe_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)a, sel_registerName("age")));
}

  可以看到,Animal *a是被weak修饰的,所以block对a成员的持有是弱引用。所以我们需要记住,auto修饰的对象,如果不是weak修饰过的,那么block内部会对对象强引用。那么先看下面这段代码:

1
2
3
4
5
6
int main(int argc, char * argv[]) {
{
Animal *a = [Animal new];
} // 出了这个括号,对象会被销毁
return 0;
}

  对象会在出了对象后被销毁,那么如下代码会发生什么,出了括号后会被销毁吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, char * argv[]) {
void(^helloBlock)(void) = nil;
{
Animal *a = [Animal new];
helloBlock = ^{
NSLog(@"小明今天%d岁", a.age);
};
}

NSLog(@"");// 断点在这里,会发生什么

return 0;
}

  根据上面讲的,想必大家都知道这里Animal对象不会被销毁。因为auto修饰的对象,会被block强引用。

  还有一个,如果要从堆上移出会调用block内部的dispose函数,内部调用_Block_object_dispose函数,会自动释放引用的auto变量,作用类似于release,具体不详细表述了。

0x05 __block修饰符

  前面我们说过,通过static修饰的局部变量和全局变量被block捕获后,外部修改变量的值后,block内部读取的时候也是最新的值,但是实际需求中,我只是临时使用下这个变量,函数执行完毕后,这个变量销毁就可以了,不希望变量被放到全局区,所以这时候需要通过__block来修饰。

1. 修饰局部变量

  还是代码走起

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
int main(int argc, char * argv[]) {
__block int age = 10;
void(^helloBlock)(void) = nil;
{
helloBlock = ^{
NSLog(@"小明今天%d岁", age);
};
}

age = 20;

helloBlock();

return 0;
}

// 转换代码
// 变量被转换成这种结构体了
struct __Block_byref_age_0 {
void *__isa; // 有isa,可以理解为被转换后,在内部,该变量变成了一个OC对象
__Block_byref_age_0 *__forwarding; // 指向该实例自身的指针
int __flags;
int __size;
int age; // 原来的变量
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // block修饰的变量,变成了一个结构体
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_264df2_mi_0, (age->__forwarding->age));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

int main(int argc, char * argv[]) {
// 初始化__Block_byref_age_0,并把age地址传给forwarding,说明forwarding是指向自己的
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
// 初始化block函数
void(*helloBlock)(void) = __null;
{
helloBlock = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
}

// 重新赋值,已经变成给结构体赋值了
(age.__forwarding->age) = 20;
// 调用block函数
((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

return 0;

}

  这里就很有趣了,__block修饰的变量会被改成一个结构体,而且该结构含有isa成员,那么这个变量很明显被转换成一个OC对象了,原本的变量也被包含在这个对象内,而且对于这个成员的管理,block这个对象默认是强引用这个变量,这个是不同于没被__block修饰的变量。

1
2
3
Animal *a = [Animal new]; // block会对其强引用
__weak Animal *b = [Animal new]; // block会对其弱引用
__block Animal *c = [Animal new]; // block会对其强引用

  对__block修饰的变量的修改,也就是对这个对象的age成员的修改((age.__forwarding->age) = 20;)。同时这个对象还有一个forwarding指针,这个的作用就是指向自己,如果在栈区就指向在栈区的实例地址,如果在堆区就指向在堆区的实例地址,下面源代码里也会证明这一点:

forwarding指针示意图

  同时,也可以发现_Block_object_assign的第三个参数flag变为8了,即BLOCK_FIELD_IS_BYREF,继续看源码

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
void _Block_object_assign(void *destAddr, const void *object, const int flags) {
.......
else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF) {
_Block_byref_assign_copy(destAddr, object, flags);
}
......
}

static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
struct Block_byref **destp = (struct Block_byref **)dest;
struct Block_byref *src = (struct Block_byref *)arg;

// 如果没有被引用,说明还没拷贝到堆上。这里的操作就是将block修饰的变量(即已经转换成的__Block_byref_age_0结构体)拷贝到堆上
else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
// if its weak ask for an object (only matters under GC)
struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack
copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
if (isWeak) {
copy->isa = &_NSConcreteWeakBlockVariable; // mark isa field so it gets weak scanning
}
if (src->flags & BLOCK_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
copy->byref_keep = src->byref_keep;
copy->byref_destroy = src->byref_destroy;
(*src->byref_keep)(copy, src);
}
else {
// just bits. Blast 'em using _Block_memmove in case they're __strong
_Block_memmove(
(void *)&copy->byref_keep,
(void *)&src->byref_keep,
src->size - sizeof(struct Block_byref_header));
}
}
// __Block_byref_age_0已经拷贝到堆上了,只增加引用计数。因为可能多个block调用同一个对象
else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
// 将复制到堆上的变量地址赋值给__Block_byref_age_0的forwarding实例指针
// 我们可以看到赋值到堆上,被转换的对象结构体里的forwarding指针被指向堆上地址了
_Block_assign(src->forwarding, (void **)destp);
}

  我们__block修饰的变量不仅被转换成一个对象结构体,并且第一次使用的时候还会被拷贝到堆上。这里还需要注意一个地方,就是如果是同一个变量被多个block捕获,那么这个变量在堆上只存在一份地址,block对其只是引用计数加1,而不是说多个block捕获这个变量,这个变量就会被多次拷贝到堆上。

  最后,因为这个变量被拷贝到堆区了,所以需要将变量转换后的结构体里的forwarding指向这个堆区地址,由于只是拷贝操作,所以栈上存在一份,堆上也存在一份。因此,如果__block修饰的对象或变量在栈区,则forwarding执行栈区地址,如果被复制到的堆区,则栈上的forwarding指向堆区地址,被拷贝到堆上的对象的forwarding则指向自己在堆上的地址。

2. 修饰对象

  同样的如果__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
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
int main(int argc, char * argv[]) {
__block Animal *a = [Animal new];
a.age = 10;
void(^helloBlock)(void) = nil;
{
helloBlock = ^{
NSLog(@"小明今天%d岁", a.age);
};
}

a.age = 20;

helloBlock();

return 0;
}

// 转换代码
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Animal *__strong a;
};

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

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_62a055_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)(a->__forwarding->a), sel_registerName("age")));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

int main(int argc, char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 33554432, sizeof(__Block_byref_a_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((Animal *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Animal"), sel_registerName("new"))};
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)(a.__forwarding->a), sel_registerName("setAge:"), 10);
void(*helloBlock)(void) = __null;
{
helloBlock = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
}

((void (*)(id, SEL, int))(void *)objc_msgSend)((id)(a.__forwarding->a), sel_registerName("setAge:"), 20);

((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

return 0;
}

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
// 参数131表示BLOCK_BYREF_CALLER|BLOCK_FIELD_IS_OBJECT
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

  同样block修饰的对象也被转换成一个结构体,这个结构体不同之处就是多了两个函数__Block_byref_id_object_copy和__Block_byref_id_object_dispose,用来管理结构体的成员 Animal a的内存,之前因为是变量所以不需要管理,只需要管理其所在的结构体内存就可以了。其他跟之前也一样,对象被拷贝到堆区。不同于__main_block_copy_0和__main_block_dispose_0,__Block_byref_id_object_copy和__Block_byref_id_object_dispose是用来管理Block_byref_a_0结构体内的Animal a成员的声明周期,而__main_block_copy_0和__main_block_dispose_0管理的是__Block_byref_a_0的生命周期。

  我们看下多出两个函数的源码,里面同样调用了_Block_object_assign函数,只是flag变为了131,其实就是BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT,那么前面的40又是什么?首先dst指向的就是__Block_byref_a_0地址,回到这个结构体,发现+40其实就是成员Animal *a的地址

1
2
3
4
5
6
7
8
9
 struct __Block_byref_a_0 {
void *__isa; // 8
__Block_byref_a_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
Animal *__strong a;
};

  进入源码后就会执行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
void _Block_object_assign(void *destAddr, const void *object, const int flags) {
if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) {
if ((flags & BLOCK_FIELD_IS_WEAK) == BLOCK_FIELD_IS_WEAK) {
_Block_assign_weak(object, destAddr);
}
else {
// 来到这个分支,只是赋值,因为之前已经移动到堆区了
_Block_assign((void *)object, destAddr);
}
}
.....
}

  观察下面的测试代码:

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
typedef  void(^HelloBlock)(void);
@interface ViewController ()

@property (nonatomic, copy) HelloBlock block;

@end

@implementation ViewController

// ① 这个构造block的函数
- (void)contructBlock1 {
static int age = 10;
self.block = ^{
NSLog(@"小明今天%d岁", age);
};
}

// ② 另外一个构造block的函数
- (void)contructBlock2 {
__block int age = 10;
self.block = ^{
NSLog(@"小明今天%d岁", age);
};
}

- (void)viewDidLoad {
[super viewDidLoad];

[self contructBlock1];
// [self contructBlock2];
self.block();
}

  不卖关子,直接说结果,调用contructBlock1方法后再执行block,会闪退;而调用contructBlock2方法则是正常运行。这是因为static修饰的变量不会被block持有,离开作用域后再访问这个变量就会有问题;而__block修饰的变量,会被block持有,所以即使离开了作用域也没关系。

  总结:

  • 当在栈上的时候block对象不会对auto和block修饰的变量强引用

  • 当auto和block修饰的变量拷贝到堆上的时候,就会产生强引用

    _Block_object_assign((void)&dst->a, (void)src->a, 3);

    _Block_object_assign((void)&dst->a, (void)src->a, 8);

  • 当auto和block修饰的变量需要从堆上移出的时候

    _Block_object_dispose((void*)src->a, 3);

    _Block_object_dispose((void*)src->a, 8);

0x06 循环引用

  首先,创建一份会循环引用的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef  void(^HelloBlock)(void);
@interface ViewController ()

@property (nonatomic, assign) int age;
@property (nonatomic, copy) HelloBlock block;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.age = 10;
self.block = ^{
NSLog(@"小明今天%d岁", self.age);
};
self.age = 20;
self.block();
}

  这里因为用到了强引用和弱引用,设计runtime,所以clang转换命令改为如下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 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
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
// block强引用了self
ViewController *const __strong self;
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

// block函数
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
ViewController *const __strong self = __cself->self; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_ViewController_0193cf_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
}


static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __ViewController__viewDidLoad_block_dispose_0(struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0), __ViewController__viewDidLoad_block_copy_0, __ViewController__viewDidLoad_block_dispose_0};

// viewDidLoad方法
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));

((void (*)(id, SEL, int))(void *)objc_msgSend)((id)self, sel_registerName("setAge:"), 10);
((void (*)(id, SEL, HelloBlock))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, 570425344)));
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)self, sel_registerName("setAge:"), 20);
((HelloBlock (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("block"))();
}

  我们看到转换代码中,block结构体内是强引用了self。而self又本来就强引用block的,所以这就造成了循环引用。一般解决方法是加上__weak。

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
- (void)viewDidLoad {
[super viewDidLoad];

__weak ViewController *weakSelf = self;
self.block = ^{
NSLog(@"小明今天%d岁", weakSelf.age);
};
self.age = 20;
self.block();
}

// 转换代码
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
ViewController *__weak weakSelf; // 变为弱引用self了
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *__weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

// 但block函数里同样是弱引用,这就会造成一个问题。如果block执行的时候self被销毁了,那么这里就要出问题了,所以这也就是为什么,需要在block里面强引用回self。
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
ViewController *__weak weakSelf = __cself->weakSelf; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_ViewController_337367_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)weakSelf, sel_registerName("age")));
}

  加上__weak,block结构体弱引用self了,这就解决了循环引用问题了。

  同样,在MRC下,只要加上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
- (void)viewDidLoad {
[super viewDidLoad];

__block ViewController *vc = self;
self.block = ^{
NSLog(@"小明今天%d岁", vc.age);
};
self.age = 20;
self.block();
}

// 转换代码
struct __Block_byref_vc_0 {
void *__isa;
__Block_byref_vc_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
ViewController *__strong vc;
};

struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
__Block_byref_vc_0 *vc; // by ref
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_vc_0 *_vc, int flags=0) : vc(_vc->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
__Block_byref_vc_0 *vc = __cself->vc; // bound by ref

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_ViewController_87e968_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)(vc->__forwarding->vc), sel_registerName("age")));
}

  这是因为,self被转换为一个结构体并复制到堆上,并且由__Block_byref_id_object_copy和__Block_byref_id_object_dispose对这个self进行管理,需要注意的是MRC下__Block_byref_vc_0对ViewController *vc是弱引用,这不同于ARC。所以只要block执行完毕后,会调用__ViewController_viewDidLoad_block_dispose_0对__Block_byref_vc_0进行销毁,接着__Block_byref_vc_0会调用__Block_byref_id_object_dispose函数对self进行销毁,所以不存在循环引用的问题了。

探索iOS签名机制

发表于 2018-05-11 | 分类于 iOS

0x01 前言

   我们知道,数据如果是明文传递很容易被“窃听者”窃听,所以为了应对这种问题,我们通常会对数据进行加密。

简单加密过程示意图

  加密对数据的安全作用完全不可或缺。接下来我们将从简至繁,了解加密的过程。

0x02 对称加密

  在对称加密中,加密者和解密者都是使用同一个密钥。

对称加密过程示意图

  常见的对称加密有如下几种:

  • DES

      DES是一种将64位明文加密成64位密文的对称密码算法,密钥长度是56位。规格上来说,密钥长度是64位,但每隔7位会设置一个用于错误检查的位,因此密钥实际长度为56位。

      由于每次只能加密64位数据,所以遇到比较大的数据,需要对DES加密进行反复。比如说要加密128位数据,首先先加密前64位,再对后64位进行加密。如果是更大的数据,以此类推。

      由于目前已经可以短时间内被破解,所以不建议使用。

  • 3DES

      DES-EDE3,将DES重复3次所得到的一种密码算法,3次所使用的密钥都是不同的,也叫三重DES算法。缺点是处理速度不高,安全性也不足够高。

  • AES

      取代DES成为新标准的一种对称密码算法。AES的密码长度有128、192、256位三种,在2000年时,Rijindael算法作为AES的实现。目前,AES已经逐步取代DES、3DES成为首选的对称密码算法。加密后文件大小翻倍,同样的,解密的时间要慢于加密的时间。

  但是,在使用对称加密的时候会遇到密钥配送问题。我们说过对称加密使用的是同一个密钥进行加解密,那么加密者肯定需要将密钥发送给接收者,而这发送密钥的过程中,窃听者是可以窃取这个密钥的,这就是密钥配送问题。

  解决密钥配送问题,一般有以下几种方案:

  • 事先约定好密钥

  • 密钥分配中心

  • Diffie-Hellman密钥交换,这是一种密钥交换方法

  • 非对称加密

0x03 非对称加密

  非对称加密中,密钥分为加密密钥和解密密钥两种不同的密钥。非对称加密,也称公钥密码。

  一般加密密钥是公开的,因此该密钥也称公钥;由消息接收者自己保管的解密密钥不能公开,一次该密钥也称私钥。

  公钥和私钥是一一对应的,是不能单独生成的,一对公钥和密钥称为密钥对。由公钥加密的密文,必须使用与该公钥所对应的私钥才能解密;由私钥加密的密文,必须使用与该私钥所对应的公钥才能解密。

非对称加密过程示意图1

  为什么说非对称加密很好的解决了对称加密的密钥配送问题?这是因为由消息的接收者生成一对公钥和私钥,然后将公钥发给消息的发送者,最后消息的发送者进行加密,这样消息的接收者就可以用私钥进行解密,得到明文信息,如下图所示。

非对称加密过程示意图2

  目前使用自多的非对称加密是RSA。RSA原理可以参考原理1、原理2。

  当然,非对称加密也不是完美的,对于数据比较大的来说,其效率会比较低,其解密所需时间大大慢于加密时间,那有没有又安全而且效率又高的方法呢?

0x04 混合密码

  通过对称加密和非对称加密的结合,我们可以解决对称加密的密钥配送问题和非对称加密的解密效率慢的问题。https运用的其实就是这个混合加密技术。

  加密的过程如下:

  • 消息接收者生成一对公钥和私钥
  • 消息接收者将公钥发送给消息发送者
  • 消息发送者随机生成一个对称加密的密钥
  • 消息发送者用密钥加密消息
  • 消息发送者用公钥对密钥进行加密
  • 消息发送者将加密消息和加密了的密钥发送给消息接收者
  • 消息接收者用私钥解密加密了的密钥
  • 消息接收者用密钥解密消息

混合密码示意图

0x05 单向散列函数

  单向散列函数,可以根据消息内容计算出散列值,散列值的长度和消息的长度无关,无论消息是1bit、1M、1G,计算出的散列值都是固定长度的。

  也被称为消息摘要函数或哈希函数。输出的散列值,也被称为消息摘要或指纹。

单向散列函数示意图

  单向散列函数具有以下特点:

  • 根据任意长度的消息,计算出固定长度的散列值
  • 计算速度快
  • 数据哪怕只有一点点不同,计算出的散列值也都不一样
  • 具备单向性

  常见的单向散列函数:

  • MD4、MD5

    产生128bit的散列值,目前已经不安全

  • SHA-1

    产生160bit的散列值,目前已经不安全

  • SHA-2

    SHA-256、SHA-384、SHA-512,散列值长度分别是256bit、384bit、512bit

  • SHA-3

    最新标准

  采用单向散列函数,是为了防止数据被篡改。比如我们下载软件的时候,厂家都会提供一个MD5值,我们下载软件后,计算出MD5值,来比对两个值是否一样来判断下载的软件有没有被改过。同样的应用场景还有app的登录口令加密,我们的用户密码不会明文进行传递的。

0x06 数字签名

  前面我们已经可以通过单向散列函数来判断消息是否被篡改过,但还是一种情况?如何证明消息是对方本人发的,因为在前面非对称加密中我们提到消息接收者者将公钥发送出去,这样就导致任何发送者都可以拿来加密,对方可以被伪装、篡改或对方否认是自己发的等。

  要证明消息是消息发送者发的,其实很简单,我们把过程反过来,用消息发送者的私钥进行加密,因为私钥是消息发送者本人持有的,再加上单向散列函数的过程就是数字签名过程,具体流程如下图:

数字签名示意图

  所以,整个数字签名流程里我们可以了解到:

  • 确保了消息的完整性

  • 知道消息是否被篡改了

  • 防止别人否认

      但是,我们知道数字签名的流程前提是公钥是得属于真正的发送者,但是如果遇到中间人攻击,数字签名将会失效,那么什么是中间人攻击呢?

    中间人攻击示意图

      这样公钥是不是发送者本人的不能保证了,那么这个又该如何解决?

0x07 证书

  为了解决中间人攻击这样的问题,我们需要一个权威组织或受信任的个人给自己的公钥加上数字签名,来确保公钥不会被替换。我们前面说过数字签名可以确保数据完整性,加上是受信任的组织给签上的数字签名,所以只要拿着受信任组织的公钥去解密这个数字签名,验证完整性就能确保拿到的对方的公钥是没被替换过的。

  密码学中的证书叫做公钥证书,里面包含姓名、邮箱、本人公钥等个人信息。然后由认证机构施加数字签名。

  CA就是这样能够证明“公钥是本人”的权威组织或个人。其作用如下:

CA组织作用示意图

0x08 iOS签名

  iOS签名的作用就是保证安装到用户手机上的app都是经过官方允许的。那么,我们的签名是如何被加上的呢?这就需要结合上面我们看到的知识。

  首先,我们要生成Mac上的公钥和私钥,请求完成后会得到一个CertificateSigningRequest.certSigningRequest这样的文件。

生成Mac上的公钥和私钥示意图

  然后,我们需要获得由苹果的私钥来给前面生成的Mac上的公钥进行签名后的证书,通过这些会生成开发者或者发布的cer证书,这些证书文件名是cer结尾的。

生成Mac公钥证书示意图

生成Mac公钥证书示意图

生成Mac公钥证书示意图

生成Mac公钥证书示意图

  最后,需要生成mobileprovision描述文件,里面包含之前生成的的证书外,还包括设备信息、apple id、app权限等信息。

  生成的入口如下,具体生成步骤不详细表述。

生成mobileprovision证书示意图

生成mobileprovision证书示意图

签名流程

  所有的证书生成完毕后,具体的签名步骤是什么样的呢?iOS签名具体流程如下:

iOS签名示意图

深入分析Category

发表于 2018-05-09 | 分类于 iOS

0x01 前言

​ em…..好像没啥说的,直接撸起袖子干代码。

前言示意图

0x02 举个例子

   首先,我们新建一个Animal类,然后再建一个Animal的分类。

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
// 头文件
@interface Animal : NSObject

- (void)eat;

@end

@interface Animal (Category)

- (void)sleep;

@end

// 内部实现文件
@implementation Animal

- (void)eat {

}

@implementation Animal (Category)

- (void)sleep {

}

@end

   用clang命令将Animal转成底层代码看下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Animal.m -o Animal-Arm.cpp

   通过下面解析出来的内容,我们可以知道Category在底层是以_category_t的结构体存在的,分类的实例方法是在这个结构体的instance_methods成员变量内。

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
// Category结构体
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};

// 将Category结构体内的instance_methods指定为_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Category
static struct _category_t _OBJC_$_CATEGORY_Animal_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Animal",
0, // &OBJC_CLASS_$_Animal,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Category,
0,
0,
0,
};

// 将_category_t结构的cls指定为Animal类,相当于说明自己是Animal的分类
static void OBJC_CATEGORY_SETUP_$_Animal_$_Category(void ) {
_OBJC_$_CATEGORY_Animal_$_Category.cls = &OBJC_CLASS_$_Animal;
}

// 前面说的_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Category就是这里
// 我们写在分类的sleep方法就在这个结构体内
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"sleep", "v16@0:8", (void *)_I_Animal_Category_sleep}}
};

// 生成一个保存在__Data段下__objc_catlist里的数组,长度为1,如果多个分类,这个数组长度延长
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Animal_$_Category,
};

   以此类推,我们同样可以知道Category里的属性、类方法、协议同样被存放在结构体_category_t内。但是结构里是没有存放变量的成员,所以从结构体也可以知道Category是不可以添加变量的,同样属性的setter和getter也只有声明,没有实现。所以这也解释了为什么分类不会自动生成getter和setter,以及为什么不能添加成员变量。

  上面代码里我们也说过,会在Data段下生成一个保存在objc_catlist里的数组,我们通过Mach-O证实这一点。首先是没有分类的Category的Mach-O文件结构图,我们发现在Data段下没有objc_catlist:

没有Category的Mach-O示意图

  而有分类的Category的Mach-O文件结构图里,明显多出了objc_catlist:

有Category的Mach-O示意图

   那么,Category我们知道是通过runtime在运行时被加载的,那么要继续深入挖掘的话,我们需要通过runtime源码进行了解。

0x03 Category信息的加载

    程序启动的时候很多依赖库会在main函数执行之前被执行。比如Runtime所在的libObjc库,这些库都是统一由dyld进行加载的。Runtime的初始化函数在_objc_init 方法,我们首先看下_objc_init是怎么被执行到的,我们下一个_objc_init符号断点。

​ objc_init调用示意图

  我们可以看到,首先是dyld动态链接器启动,然后把Mach-O加载进来,进行读取操作包括我们的类、分类、方法等,然后libSystem库的初始化,里面包括了libobjc和libdispatch等库,所以libobjc库,也在这一刻初始化。接着看_objc_init方法的实现

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

environ_init();
tls_init();
static_init();
lock_init();
exception_init();
// 注册dyld事件的监听
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

  map_images对Mach-O中一些符号信息进行初始化。

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
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[]) {
rwlock_writer_t lock(runtimeLock);
// 这个函数最终主要关注在_red_imgaes函数
return map_images_nolock(count, paths, mhdrs);
}

// 读取类信息、协议信息、分类信息等
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
......
// 选取代码片段
......
// 读取Category信息
for (EACH_HEADER) {
// _getObjc2CategoryList下面会展开
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();

for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);

if (!cls) {
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}

bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
// 建立一个映射表。key是类名,value是Category表。因为可能一个类有多个Category
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
// 重建类的结构,分类里的属性、方法、协议等需要重新加入进类里原来的属性、方法、属性表中。下面会拆开这个函数讲解
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}

if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
......
if (DebugNonFragileIvars) {
// 使得类处于可用状态
realizeAllClasses();
}
......
}

// GETSECT是个宏定义,而__objc_catlist就是前面说的Data段下的
GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
#define GETSECT(name, type, sectname) \
type *name(const headerType *mhdr, size_t *outCount) { \
return getDataSection<type>(mhdr, sectname, nil, outCount); \
} \
type *name(const header_info *hi, size_t *outCount) { \
return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \
}

  这里要讲下,如何重建类的结构,也就是 class_rw_t这个结构体。通过前插的方法,将分类的方法、属性、协议信息加入到类原本的方法、属性、协议结构中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 重建类的结构
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;

runtimeLock.assertWriting();

isMeta = cls->isMetaClass();

if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
// 重建的过程在这个函数里
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}

// 这个函数很好理解,取出分类的属性、协议、方法信息。通过attachLists函数附加到类原本的结构里
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);

bool isMeta = cls->isMetaClass();

method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));

int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
// 读取方法、属性、协议信息
while (i--) {
auto& entry = cats->list[i];

method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}

property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}

protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}

auto rw = cls->data();
// 将拿到的Category信息,附加到类结构中,如何附加在attachLists函数中
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);

rw->properties.attachLists(proplists, propcount);
free(proplists);

rw->protocols.attachLists(protolists, protocount);
free(protolists);
}

void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;

if (hasArray()) {
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
// 先将原来的结构移动出足够空间。
// 移动是一种前插行为,比如要添加10个方法进来,就需要将原来结构全部后移出10个位置,这样前面10个位置就空出来了
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
// 把数据填入移动空出的位置
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
list = addedLists[0];
}
else {
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}

  因为是通过前插来调整类的结构信息,所以举个例子,比如分类中如果有跟类中一样的方法,就被插入到前面去了,执行方法的时候会去查找方法,但会首先找到分类的方法,所以这就是为什么执行的是分类的方法,而不是类里的方法,而这往往会给我们造成“被覆盖”的感觉,其实原来类的这个方法还是存在在类的方法结构中,只是排在后面而已,如果要执行这个类的方法,我们其实也可以做到的。

0x04 Load方法

  回到前面,我们在说_objc_init的时候,map_images里面做了一些准备工作,让类处于待用状态,而我们或许也会注意到旁边还有一个load_images函数,那么这个函数是干嘛的?

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

environ_init();
tls_init();
static_init();
lock_init();
exception_init();

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

  load_images函数其实就是调用类以及分类里面的load方法,因为是在程序启动的时候执行的,所以我们也能证实load方法执行时机最早,且只会执行一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!hasLoadMethods((const headerType *)mh)) return;

recursive_mutex_locker_t lock(loadMethodLock);

// 找到所有的load方法,先找类的,保存到loadable_classes表内;再找分类的,保存到loadable_categories表内。
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}

// 调用load方法
call_load_methods();
}

void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {

while (loadable_classes_used > 0) {
// 遍历loadable_classes表,拿到load方法,然后执行调用执行load方法
// 所以原来类的load方法是早于分类的load方法
call_class_loads();
}
// 遍历loadable_categories表,拿到load方法,然后执行调用执行load方法
more_categories = call_category_loads();

} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}

  找到所有的load方法后,就要去执行load方法,这里只拿call_class_loads函数举例,call_category_loads其实是类似的。

  从loadable_classes表中拿到每个类的load方法,然后直接执行。需要注意的是,这里是直接执行,而不是调用消息发送机制来执行方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void call_class_loads(void)
{
int i;

struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 拿到load方法
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;

if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
// 直接调用执行,而不是走消息发送机制
(*load_method)(cls, SEL_load);
}

if (classes) free(classes);
}

  正因为是直接执行load方法,不走objc_msgSend消息发送函数,所以也就不存在去方法表中查找方法的过程。所以分类的load方法,并不会”覆盖“原来类的load方法。而且源码里,首先执行的是原来类的load方法,其次执行分类的load方法,所以原来类的load方法执行时间早于分类的load方法。

0x05 Initialize 方法

  说到了load方法,就不得不提initialize方法,这两个方法经常拿在一起进行比较,这里也不例外,我们继续通过源码来挖掘什么时候开始执行initialize方法。

  我们在initialize方法打个断点看下调用栈

​ initialize调用示意图

  我们初始化一个对象的时候,常用的方法是alloc或者new,这两个方法都是通过消息发送来调用的。我们可以看到,在objc_msgSend的汇编代码中,调用了objc_msgSend_uncached。

  在objc_msgSend_uncached里面,调用了MethodTableLookup这个宏

1
2
3
4
5
6
7
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

MethodTableLookup
br x17

END_ENTRY __objc_msgSend_uncached

  MethodTableLookup宏的定义如下:里面我们可以看到跳转到了__class_lookupMethodAndLoadCache3函数函数。

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
.macro MethodTableLookup

// push frame
stp fp, lr, [sp, #-16]!
mov fp, sp

// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]

mov x2, x16
// 跳转到__class_lookupMethodAndLoadCache3函数
bl __class_lookupMethodAndLoadCache3

mov x17, x0

ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]

mov sp, fp
ldp fp, lr, [sp], #16

.endmacro

  _class_lookupMethodAndLoadCache3里面又调用了lookUpImpOrForward函数

1
2
3
4
5
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

  lookUpImpOrForward函数里面入参initialize接收的是YES,而且第一次进来的时候cls->isInitialized()是肯定为NO的。所以只当这个对象第一次调用objc_msgSend的时候肯定会调用_class_initialize函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
......
// bool isInitialized() {
// return getMeta()->data()->flags & RW_INITIALIZED;
// }
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
......
}

  _class_initialize函数里面首先调用父类的initialize方法,然后调用callInitialize函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void _class_initialize(Class cls)
{
......
// 同时调用父类的initialize方法
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
......
{
callInitialize(cls);

if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
pthread_self(), cls->nameForLogging());
}
}
}

  通过消息调用,执行initialize方法。

1
2
3
4
5
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}

  调用方式来看,我们可以发现load方法是直接执行,而initialize方法是通过消息发送来执行的。所以在这里,我们可以知道有分类实现initialize方法的情况下,只会调用分类的initialize方法,原来的类的initialize方法并不会被执行,而且执行顺序是先执行父类的initialize方法,再执行子类的initialize方法。我们举个例子,新建Animal类、Animal分类、继承Animal的子类Dog、Dog分类。

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
@implementation Animal

+(void)initialize {
NSLog(@"%@", [self description]);
}

@end

@implementation Animal (Category)

+(void)initialize {
NSLog(@"%@-Category", [self description]);
}

@end

@implementation Dog

+(void)initialize {
NSLog(@"%@", [self description]);
}

@end

@implementation Dog (Category)

+(void)initialize {
NSLog(@"%@-Category", [self description]);
}

@end

// 打印结果
2018-05-09 15:35:56.587531+0800 testData[36930:28695275] Animal-Category
2018-05-09 15:35:56.587665+0800 testData[36930:28695275] Dog-Category

  结果可以看到,如果有Category的情况下,都是先执行Category的initialize方法;其次先调用父类的initialize方法,再调用子类的initialize方法。同时,我们可以测试下,如果子类的initialize方法不实现,而只实现父类的initialize方法会有什么效果?

1
2
2018-05-09 15:42:46.513940+0800 testData[37636:28757829] Animal-Category
2018-05-09 15:42:46.514091+0800 testData[37636:28757829] Animal-Category

  父类的的initialize方法被执行了两次,这又是为什么呢?

0x06 isa指针与SuperClass

  我们首先看下Class的结构:

1
typedef struct objc_class *Class;

  指向objc_class结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct objc_class : objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;

class_rw_t *data() {
return bits.data();
}
......
};

struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

// 继承自的objc_object里面只有一个isa。
// 所以相当于如下结构
struct objc_class {
Class _Nonnull isa;
Class superclass;
// 调用过的方法会被保存到cache,下次调用的时候先从Cache查找,提高效率
cache_t cache;
class_data_bits_t bits;
// class_rw_t是一个很重要的结构
class_rw_t *data() {
return bits.data();
}
......
};

struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;
// 方法表
method_array_t methods;
// 属性表
property_array_t properties;
// 协议表
protocol_array_t protocols;
......
}

  受益于苹果开源,我们知道现在Class的结构如上面代码所示,而不是很多博客里的说到的这样的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这是Objective-C 1.0的结构
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
const uint8_t *ivar_layout;
struct old_class_ext *ext;
......
};

  所以我们可以看到上面结构中都有一个isa和superClass成员,之间的关系可以通过下面这张图概况。

isa和superClass关系示意图

  那么isa的作用如下:

  • 实例对象的isa指向类

    调用实例方法时,会先通过isa找到类,再在类的方法表中找到这个实例方法进行调用。

  • 类的isa指针指向元类

    调用类方法时,先通过类的isa找到元类,再在元类的方法表中找到这个类方法进行调用。

  同样superClass的作用,显而易见是找到父类,下面会通过例子详细了解调用过程,进行实验前,我们必须记住的是调用实例方法是到这个类的方法表中查找的,实例结构自己是不保存这些的,它只做值的存储,比如age属性,实例结构里面只保存age的值,比如20这样的。而调用类方法是到这个类的元类里的方法表进行查找的。仔细看下上面这张图就可以很好理解了。

Example 实例对象调用自己的方法

  测试代码如下:

1
2
3
4
5
6
7
8
9
@interface Animal : NSObject

@end

@interface Dog : Animal

- (void)eat;

@end

  这个过程是首先实例方法首先通过isa找到它的类对象,然后遍历类对象的方法表,找到eat方法进行调用。

Example 实例对象调用父类的方法

  测试代码如下:

1
2
3
4
5
6
7
8
9
@interface Animal : NSObject

- (void)eat;

@end

@interface Dog : Animal

@end

  这个过程是首先实例方法首先通过isa找到它的类对象,然后遍历类对象的方法表,但是方法表里并没有这个方法,于是通过superClass找到父类对象,再到父类的方法表里进行查找,最后找到eat方法进行调用。假设父类还是没有,再一层一层上去找,直到基类为止,如果还是没找到就报错 unrecognized selector sent 。

Example 类对象调用自己的方法

  测试代码如下:

1
2
3
4
5
6
7
8
9
@interface Animal : NSObject

@end

@interface Dog : Animal

+ (void)eat;

@end

  这个过程是直接在自己的元类方法表里进行查找。同样的,如果是父类的类方法,就调用superClass找到父类对象,再到父类的元类里的方法表进行查找,直到基类元类为止,如果还是没找到就报错 unrecognized selector sent 。

Example 一个特殊情况

  测试代码如下:这个例子里,只有NSObject实现eat的实例方法(OC里NSObject是所有类的基类),而调用方式是[Dog eat]这样的调用类方法,运行后结果如何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@interface NSObject (Category)

- (void)eat;

@end

@interface Animal : NSObject

@end

@interface Dog : Animal

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

[Dog eat];
}

@end

  执行结果是成功运行了,为什么我明明调用的是类方法,而实例方法却被触发了。如果仔细看过上面图的话,我们可以发现,当调用类方法时,元类一层一层往上进行查找,如果基类的元类里面的方法表也没有,就会来到基类(NSObject)里面的方法表进行查找,我们知道类里面保存的是实例方法,所以就会调用test方法成功。

  回到前面留下的问题,Category里没实现initialize方法,而只在类里面实现initialize方法,这个类的initialize方法为什么会被执行两次?回到_class_initialize这个函数里可以看到父类的一次执行是在这里执行的。而第二次是因为自己类的元类方法表里找不到initialize方法,所以去父类的元类方法表里进行查找了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void _class_initialize(Class cls)
{
......
// 父类的initialize方法被执行一次
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
......
{
callInitialize(cls);

if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
pthread_self(), cls->nameForLogging());
}
}
}

0x07 objc_getAssociatedObject和objc_setAssociatedObject

  我们知道分类里面可以添加property属性,但不会生成getter、settter方法和实例变量。所以给属性设置和获取值是通过objc_setAssociatedObject和objc_getAssociatedObject实现的。那么为何一定要通过这种方式实现,比如下面的代码会有什么问题?我们通过全局变量来控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface Animal (Category)

@property (nonatomic, assign) int age;

@end


// 定义全局变量
int g_Age = 0;

@implementation Animal (Category)

- (void)setAge:(int)age {
g_Age = age;
}

- (int)age {
return g_Age;
}

@end

  显然是有问题的,全局变量共用的是一份,如果创建多份实例,去修改这个g_Age,那么每个实例对象的age属性值不具备唯一性,我们通过测试代码看下运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
    Animal *a = [[Animal alloc] init];
a.age = 10;

NSLog(@"生成b对象前,a对象的年龄是%@岁", @(a.age));

Animal *b = [[Animal alloc] init];
b.age = 15;

NSLog(@"生成b对象后,a对象的年龄是%@岁", @(a.age));

// 运行结果:我们发现对象a的age,被对象b改掉了
2018-05-09 18:16:11.724830+0800 testData[4751:54320510] 生成b对象前,a对象的年龄是10岁
2018-05-09 18:16:11.725054+0800 testData[4751:54320510] 生成b对象后,a对象的年龄是15岁

  所以上面这种方案肯定不行的。说到唯一性,肯定还会想到每个实例对象肯定都不同的,如果以对象为key的字典,那么就能确保唯一性,比如下面这样的。

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
@interface Animal (Category)

@property (nonatomic, assign) int age;

@end

// 定义一份全局字典
NSMutableDictionary *g_AgeDict;
@implementation Animal (Category)

+(void)load {
g_AgeDict = [NSMutableDictionary dictionary];
}

- (void)setAge:(int)age {
[g_AgeDict setValue:@(age) forKey:[NSString stringWithFormat:@"%p", self]];
}

- (int)age {
NSNumber *age = [g_AgeDict objectForKey:[NSString stringWithFormat:@"%p", self]];
return age.intValue;
}

@end

// 测试代码
Animal *a = [[Animal alloc] init];
a.age = 10;

NSLog(@"生成b对象前,a对象的年龄是%@岁", @(a.age));

Animal *b = [[Animal alloc] init];
b.age = 15;

NSLog(@"生成b对象后,a对象的年龄是%@岁", @(a.age));

// 运行结果:
2018-05-09 18:30:49.964833+0800 testData[6293:54441445] 生成b对象前,a对象的年龄是10岁
2018-05-09 18:30:49.965028+0800 testData[6293:54441445] 生成b对象后,a对象的年龄是10岁

  我们根据运行结果可以看到我们保证了唯一性,但是缺点也很明显,我们如果有多个属性,岂不是要创建很多全局的字典。那么我们又想到可以这样做:

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
@interface Animal (Category)

@property (nonatomic, assign) int age;

@end

NSMutableDictionary *g_Dict;
@implementation Animal (Category)

+(void)load {
g_Dict = [NSMutableDictionary dictionary];
}

- (void)setAge:(int)age {
NSMutableDictionary *dict = [g_Dict objectForKey:[NSString stringWithFormat:@"%p", self]];
if(!dict) dict = [NSMutableDictionary dictionary];

dict[@"age"] = @(age);

[g_Dict setValue:dict forKey:[NSString stringWithFormat:@"%p", self]];
}

- (int)age {
NSMutableDictionary *dict = [g_Dict objectForKey:[NSString stringWithFormat:@"%p", self]];
NSNumber *age = dict[@"age"];
return age.intValue;
}

@end

  虽然可以实现了我们的需求,但是这样写还是麻烦,那我们看看官方怎么做的,先看objc_setAssociatedObject源码:

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
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}

// 里面维护了一份全局关联哈希表,里面一个对象对应一份对象关联表。对象对应的对象关联表里保存的就是我们objc_setAssociatedObject设置的内容。
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}

   所以我们可以明白通过objc_setAssociatedObject设置值,其实就是往一份全局的哈希表中给自己所对应的关联表中以key为健值,设置以value为值的过程。同样objc_getAssociatedObject也是相同步骤,只是换成了取值而已。我们发现,跟我之前自己实现的方案思路是差不多的,但这个显然方便多了。

  category_t结构中是没有存放ivar表的,所以上述操作相当于给对象关联一个成员变量,只是在普通类中这个成员变量在ivar表中,而分类中这个成员变量被维护在一个哈希表中。我们以前经常说如何给分类添加一个属性,所以严谨的来说,我们添加的不是属性,而是手动的生成setter和getter方法,并且维护一个成员变量到一个哈希表中。分类的属性的信息是可以被存到分类的_prop_list_t表中的。

  看完源码,我们再看下使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
static const void *ageKey = &ageKey;
@implementation Animal (Category)

- (void)setAge:(int)age {
objc_setAssociatedObject(self, ageKey, @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)age {
NSNumber *age = objc_getAssociatedObject(self, ageKey);
return age.intValue;
}

@end

  首先看下第三个参数objc_AssociationPolicy ,它一共有以下几个值:

  • OBJC_ASSOCIATION_ASSIGN

    相当于@property(nonatomic, assign)

  • OBJC_ASSOCIATION_RETAIN_NONATOMIC

    相当于@property(nonatomic, strong)

  • OBJC_ASSOCIATION_COPY_NONATOMIC

    相当于@property(nonatomic, copy)

  • OBJC_ASSOCIATION_RETAIN

    相当于@property(atomic, strong)

  • OBJC_ASSOCIATION_COPY

    相当于@property(atomic, copy)

  再看下第二个参数key,我们看到需要一个const void *指针,也就是一个任意指针都可以,所以我们精简至如下也是可以的,不用创建那么多key也可以达到我们的需求:

1
2
3
4
5
6
7
8
9
10
11
12
@implementation Animal (Category)

- (void)setAge:(int)age {
objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)age {
NSNumber *age = objc_getAssociatedObject(self, _cmd);
return age.intValue;
}

@end
12

朝暮

联系方式:leylfl@foxmail.com

17 日志
3 分类
© 2018 朝暮
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4