普通视图

发现新文章,点击刷新页面。
昨天以前小猪的博客

发现iOS SDK的Bug - Hopper使用教程向

作者 DreamPiggy
2019年6月14日 05:39

Hopper简介

Hopper,全称Hopper Disassembler,是一个macOS和Linux平台上的反汇编IDE。提供了诸如伪代码,子程序,脚本,Debugger,Hex编辑等等一些列工具。相比于其他知名的反汇编工具如IDA,最大的好处是对平台特性,也就是Objective-C的反汇编有优化,提供非常贴近原始代码的伪代码(IDA目前则会是保留诸如objc_msgSend的伪代码),并且新版本也对Swift提供了一定的反汇编符号优化,因此作为探究iOS平台上的SDK实现,可以说是一利器。

Hopper安装

Hopper本身目前是收费的软件,提供了免费的使用(30分钟)。官方下载地址为:https://www.hopperapp.com

Mac版本后解压,拖到Application下即可使用。

对于个人使用,价格不菲,有两种方案,个人比较推荐第一种

  • Per User:收费为¥700,允许同一时间唯一激活,不绑定机器硬件
  • Per Computer,收费¥900,和一台电脑的机器硬件绑定

对于只是尝鲜或者轻度使用,其实使用免费版即可。网上现在也有针对旧版本的Cracked版本,不过存在一些问题和崩溃。如果是在需要,可尝试链接

Hopper使用

Hopper提供了一个教程,可以参考官方简易教程

针对我们的场景:分析iOS的SDK内实现或者问题,我这里提供了一个Step By Step的过程,教你如何查找问题。

获取需要反汇编的二进制文件

首先,我们需要获取一份iOS SDK的二进制Mach-O文件。最简单的方式,是通过Xcode提供的iPhone模拟器去获取它。在获取之前,我们先了解一下iOS SDK对应的二进制文件路径。

  • Xcode自带模拟器,对应系统根路径:

Xcode 11:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot

Xcode 10:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/

  • 已下载的历史版本固件的模拟器,对应系统根路径,自己根据版本版本修改中间的数字:

/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime/Contents/Resources/RuntimeRoot/

  • 真机的系统根路径,不用说了吧:

/

iOS 系统提的库和二进制,可以简单分以下几类,按照需要选择对应的相对根路径:

  • 公开Framework: /System/Library/Framework
  • 私有Framework:/System/Library/PrivateFrameworks
  • 系统App:/Applications
  • UNIX动态库: /usr/lib

这里我们以Xcode 10自带的iOS 12 SDK,UIKitCore为例(注意,UIKit从iOS 12开始,为了支持部署到macOS,将代码基本全盘移动到了私有Framework的UIKitCore.framework中,UIKit.framework只是一个外层的壳),我们就能直接去访问这个路径,获取它的Mach-O二进制:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore

一般来说,iPhone模拟器提供的二进制Mach-O即可够用,虽然它实际上是x86_64架构的编译产物,但是基本上的逻辑和真机上是一致的。如果涉及到需要只能在真机可用的库,如AVFoundation的摄像机,建议可以从真机中提取(也可以从iOS的IPSW固件中提取),见下文。

真机获取系统库的二进制文件

获取dyld shared cache

在真机上,为了加快动态库的加载,并减少iOS 占用磁盘的体积,dyld采取了一个缓存,将多个Mach-O文件合并到一起,由系统启动后就预热。因此,实际上系统库(公开和私有)的路径上,只有Framework和其中的资源文件,却没有对应的Mach-O二进制。我们需要首先获取到这个cache,然后解压出来对应的二进制。参考dyld_shared_cache

对应dyld shared cache路径(以arm64机器为例):/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64

当然,除了使用已经越狱的真机,我们还可以通过IPSW,即iOS的固件包,来直接提取对应的dyld shared cache,并解压得到对应的Mach-O文件。

IPSW可以从这个网页上下载,选择你的设备以及iOS版本号,就可以下载对应的IPSW文件。

将下载的IPSW解压(建议使用zip命令行,或者BetterZip之类的解压软件,Mac自带的解压似乎会报错),可以找到一个最大容量的DMG文件,双击即可加载

加载后就是完整的iOS系统根路径了,从对应路径下找到dyld shared cache。

解压dyld shared cache

为了解压dyld shared cache,市面上一些工具其实早已过期,要么不支持arm64,要么存在Bug。但实际上,Apple自己开源的dyld源代码,就已经包含了这样一个命令行工具,叫做dsc_extractor,我们这里直接用来源码来编译一份来使用即可。

进入opensource.apple.com选择最新的macOS的版本,我这里例子使用的是我写这篇文章最新公开的 macOS 10.14.1

然后下载两份代码,一份是dyld,一份是CommonCrypto

为了编译,需要一点小技巧,但是对于iOS开发者我觉得挺简单

  1. 用Xcode,打开dyld代码中的dyld.xcodeproj
  2. 修改Build Settings中,把对应的Base SDK,从macosx.internal改成公开的macOS
  3. 进入dsc_extractor.cpp,看到最后有一个test program,把上面的#if 0改成#if 1
  4. 我在编译新版本时发现依赖了一个叫做CommonDigestSPI.h的私有头文件,这个在下载的CommonCrypto工程中,拖进来改一下引用方式即可
  5. 选择dsc_extractor,Archive得到一个产物,叫做dsc_extractor.bundle,然而他实际就是一个Mach-O二进制,直接删掉后缀,chmod+x,即可使用

如果上面的编译比较麻烦,可以直接下载我这里编译好的一份二进制,然后放到你的PATH路径下:dsc_extractor

然后我们可以使用dsc_extractor来解压我们提取到的dyld shared cache,很简单的命令

1
dsc_extractor ./dyld_shared_cache_arm64e ./output

会得到所有dyld shared cache中的二进制Mach-O文件,按照路径排列,然后我们就可以用自己想反编译的库,如UIKitCore,来使用Hopper了。

载入Hopper

现在我们已经有了一个UIKitCore的Mach-O文件了,我们打开Hopper来载入它。我们可以使用Command+Shift+O来选择一个Mach-O文件,也可以将文件拖动到Hopper界面上来打开。

载入Mach-O文件后,Hopper会弹出框来选择具体分析的内容,大部分情况直接确认即可。如果是分析其他类型的文件,可能有特例如下:

  • 分析一个.a或者.dylib,并且该二进制由多个.a或者.dylib合成,这时候会提示你选择具体的某个编译产物
  • 分析一个FAT Binary,这时候会提示你选择具体的某一个架构的文件

载入开始后,一般需要等待一段时间来分析(下方会有进度条),等待分析完成后,你可以将当前分析的结果,保存成一个.hop结尾的文件,未来就不再需要分析了,非常有用(注:免费版不可用)。

符号分析

左侧有一个符号框,从左到右依次表示:

  • Labels: C/C++Objective-C的符号,包括类名,方法名,全局变量等
  • Proc:子程序,对应C/C++的函数,Objective-C的方法,Block代码段等
  • Str:常量段,包括了所有C/C++Objective-C字面量,即代码中直接用@"", ""写的内容

每项内容都支持搜索,一般来说取决于我们要解决的问题,有大概几个场景

  1. 分析特定方法的实现:使用Proc搜索
  2. 运行时抛出的异常或者Log:使用Str搜索关键字
  3. 得知一个类的所有方法:可以使用Label,但更好的方式是通过Class-dump获取头文件(见下)

Class-dump与私有头文件

Class-dump是一个能够解析Mach-O文件,对应的Objective-C符号,以生成一个完整的头文件的工具。得益于Objective-C运行时和符号的特点,可以方便的还原回基本接近原始的类声明代码。具体使用也很简单,参见项目的Readme,编译得到二进制,放到PATH中,然后执行:

1
class-dump UIKitCore.framework -r -o output -H

对于重头戏,关于iOS SDK的所有头文件,早有专人建立了一个在线网站去分析,点击跳转:iOS Runtime Headers

在这个网页上,可以支持Framework/类/方法级别的搜索,支持点击头文件跳转链接,非常的方便,一般的分析iOS SDK都可以采取这个网页的结果来辅助分析。

伪代码分析

当我们了解到需要分析的符号方法后,下一步一般就会进行伪代码分析。在Hopper中,点击到一个子程序入口,然后点击上方的这个像是if (b)代码的图标,即可打开伪代码分析框

对于简单的代码,我们基本上能够还原回100%可读的Objective-C代码,由于ARC时便一起,我们可以看到对应的Retain和Realse调用

分析调用关系

我们可以通过对应的子程序页面,右键选择”References To Selector”,来查看所有对这个Selector的调用。(由于Objective-C运行时的特点,只能是Selector级别的调用,如果有不同类的同名Selector,可以在弹出的窗口中搜索或者依次检查)

常见的分析姿势

Block

Objective-C会使用到Block,而Block由于其实现原理,会生成对应的C方法,Hopper目前原生解析的Block语法并不是很直观,这里提供一个简单的说明。

其实Hopper反编译出来就是Block实现的原理,如果对于Block实现原理不清楚,建议可以先看一遍《这个教程》

简单Block

1
2
3
dispatch_async(dispatch_get_main_queue(), ^{
printf("%d", 1);
});

Hopper原生反编译如下,实际Block代码会单独在另一个C方法中,在block implemented at:提示对应的方法中

1
2
3
4
5
6
dispatch_async([objc_retainAutoreleaseReturnValue(*__dispatch_main_q) retain], ^ {/* block implemented at ___29-[ViewController viewDidLoad]_block_invoke */ } });

void ___29-[ViewController viewDidLoad]_block_invoke(void * _block) {
printf("%d", 0x1);
return;
}

捕获变量

如果Block捕获了变量,那么根据Block的实现原理,可以知道这些变量在Block中可见的变量都是被值宝贝,对于NSObject就是指针

如果使用__block修饰,那么会保留原始的变量的指针,对于NSObject就是对象指针的指针,我们可以通过这个简单识别。

比如对于这样代码:

1
2
3
4
5
6
NSObject *obj = [NSObject new];
__block NSObject *obj2 = [NSObject new];
[self testBlock:^(int value){
NSLog(@"%@", obj);
obj2 = nil;
}];

实际反编译出来的结果长这样:

1
2
3
4
5
int ___29-[ViewController viewDidLoad]_block_invoke(int arg0, int arg1) {
NSLog(@"%@", *(arg0 + 0x20));
rax = objc_storeStrong(*(*(arg0 + 0x28) + 0x8) + 0x28, 0x0);
return rax;
}

对应的arg0就是第一个参数,而最后参数对应的是block_impl_0实现结构体,可以忽略。

CGRectMake等inline的C方法

一些带有inline数值计算的方法,会被苹果的clang在编译时优化,实际上并不是你看到的头文件的样子,这种就需要我们枚举出来,人肉还原回他的实现,举个例子:

这样的代码:

1
2
CGRect rect = CGRectMake(0, 1, 2, 3);
NSLog(@"%@", NSStringFromCGRect(rect));

反编译结果:

1
2
3
4
5
intrinsic_movsd(xmm1, *double_value_1);
intrinsic_movsd(xmm2, *double_value_2);
intrinsic_movsd(xmm3, *double_value_3);
_CGRectMake(&var_30, _cmd, rdx, rcx);
NSLog(@"%@", [NSStringFromCGRect(*(&var_30 + 0x10), *(&var_30 + 0x18)) retain]);

可以看到有mov之类的汇编命令调用,其实这就是为了压栈其实大部分场景我们只要熟悉简单的mov add sub mul几个基本的汇编命令的意义即可。

Swift

Swift作为Apple一致力推的下一代官方编程语言,随着iOS 13的发布,现在已经可以作为第一优先的SDK支持语言了,iOS 13上出现了4个Swift Only的库,因此对于Swift相关的反编译需求,也会慢慢出现。然而,不同于动态性强的Objective-C代码,Swift天生的静态强类型语言特性,造成了相当高的反编译难度(堪比C++开O2优化),在这里基本不细讲,只是大概说一下目前的状况。

Hopper从v4开始支持了对Swift符号的符号化,我们不再需要使用swift来反解决mangled的符号名。

由于Swift支持完整的命名空间,查询符号需要带上完整的符号

同时,Swift由于clang的优化,会讲很多编译器检查到的频繁的代码调用,自动转换为一个以sub开头的函数,以减少二进制大小。

对于Swift非@objcdynamic的属性和方法,会类似于C++的虚函数表,实际上的调用都是编译器展开的地址偏移,而不像Objective-C那样有符号可查。这种时候我们需要就是类似C++反编译那样,通过分析Swift class或者struct的属性,来对照偏移量得知调用。

对于Swift的会触发运行态的一些语法,需要你对Swift语言实现有了解,比如Protocol Extension Where子句,会生成Protocol Witness,我们可以在Hooper中搜索到它

可以看到,目前的Hopper对Swift有相应的支持,但受限于Swift的语言性质很难直观阅读,必要时候还是需要一些汇编,以及传统C++的反编译分析模式去对待它

总结

这篇教程基本上是从我个人的使用经验来介绍,以工具和流程为主,主要是为了给目标iOS平台,且不是专攻二进制安全的人来阅读。

其实对大部分iOS平台开发者,最主要的目的,其实是为在发现一些iOS SDK表现奇怪的行为,或者Crash时,能够有一定的分析和判断能力,去尝试定位原因,绕过问题,并最终能够有底气,去向Apple提交Bug Report。

反编译本身就是二进制安全中的灰色地带,而且还有类似二进制加固等攻防模式,并不是万能方式去了解一个程序运行的方式。还需要配合自己的编写代码经验,才能更好地解决问题

参考

Objective-C代码库的实现隐藏

作者 DreamPiggy
2017年6月5日 01:46

虽然Swift现在是开发iOS推荐入手的最佳语言,但是对于代码库而言,最大的一个问题是Swift ABI仍然没有定下(今年发布的的Swift 4.0,依然放弃ABI稳定性,而注重于Swift源代码3.x->4.0的兼容性)。所以这就意味着Swift 3.x编译的二进制库,在Swift 4.0将无法链接,只能重新代码编译。看来这又将是Objective-C这门古老的语法,能够作为一些framework首选开发语言的一年。

对于一个代码库来说,有时候我们为了隐藏一些实现的细节,或者内部处理流程,需要编译到二进制进行分发,并提供Public Header来供其他开发者调用。

因此,开发代码库的时候,需要明确哪些API是对外公开的,可以由其他开发者调用。那些是库内部之间互相调用的,不应该由外部使用者调用。而Objective-C不像C++提供了private关键字来限制直接访问成员变量和成员方法。因此,就需要尽量避免私有属性和私有方法的定义出现在头文件中。只要不引入私有的头文件,那就无法直接访问这些属性和方法。

隐藏内部属性

私有属性,可以分成两种,一种是希望放到类内部而纯粹不想暴露给任何人的,可以叫做内部属性。一种是希望暴露到Private Header中,只限于引入该头文件的地方进行访问。

内部属性的声明非常简单,我们可以直接使用类扩展声明属性,而编译器会自动生成getter和setter,不需要任何额外工作。

1
2
3
4
5
6
// Person.m
@interface Person ()

@property (nonatomic, strong) NSObject *internalObject;

@end

改变属性修饰符

对于很多情况,我们需要对外暴露属性是readonly的,以防止使用者手动修改,但是内部流程的时候也需要这个属性,并且希望是readwrite的,这个在类扩展中直接可以重新声明已有的属性,并修改属性修饰符。

1
2
3
4
5
6
7
8
9
10
11
12
// Person.h
@interface Person

@property (nonatomic, strong, readonly) NSNumber *number;

@end
// Person.m
@interface Person ()

@property (nonatomic, strong, readwrite) NSNumber *number;

@end

注意,由于类扩展是可以在任何地方声明的(不限于.m实现文件),我们也可以把属性修饰符的修改,放到Private Header(可以用+Private后缀,也可以参考UIKit等框架起名为UIKitInternal.h)中,这样引入了Private Header的地方可以readwrite,没有引入的地方是readonly。

1
2
3
4
5
6
// Person+Private.h
@interface Person ()

@property (nonatomic, strong, readwrite) NSNumber *number;

@end

隐藏私有属性

但是很多时候,我们希望一些属性是私有的,即类实现处和引入了Private Header的地方才可以访问。这种时候就需要采取别的方式了。常见的方法是通过类扩展(主要针对类的实现文件可见)或者使用关联对象(主要针对类的实现文件不可见,如其他第三方库的类)两种方式。

类扩展(Class Extension)

通常情形

类扩展,不同于Category,最大的优势在于可以直接添加实例变量ivar到类的本身实现中,而Category是无法添加实例变量的。而在类扩展中声明的属性,也可以自动在编译期合成,同普通类声明属性的方式相同,不了解的参见:CustomizingExistingClasses。因此,实际上类扩展非常适合隐藏私有属性。

1
2
3
4
5
6
// Person+Private.h
@interface Person ()

@property (nonatomic, strong) NSString *privateID;

@end

自定义存取方法

对于通常case来说,这是非常好的解决方法(不用任何额外代码)。但是有一个问题,如果你想自定义这个属性的存取方法(比如,实例变量的惰性初始化),那就会遇到问题。因为属性合成的ivar,是只在类本身实现中创建的,在Category中无法创建,而且类的实现只能实现一次(在原始的Person.m中实现)。试想一下这样子的情况,就会出现编译错误:

1
2
3
4
5
6
7
8
9
10
11
// Person+Private.m
@implementation Person (Private)

- (NSString *)privateID
{
if (!_privateID) { //Compile Error: undeclared identifier:_privateID
_privateID = @"foo";
}

return _privateID;
}

第一种解决方案:

最简单的方式,就是直接把自定义的存取方法写在类本身实现文件中,然后在Category中暴露头文件,并用@dynamic来标记这个属性(否则由于Category看不到编译器自动生成的getter和setter会报warning)。自定义存取方式就和普通的写法一模一样。这相当于是一种把内部属性暴露出来的方法。不过容易导致耦合(因为其实我们的私有属性目标是用于和外部类交互的,不希望放到Private Category以外)。

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
//Person.m

@interface Person ()

@property (nonatomic, strong) NSString *privateID;

@end

@implementation Person

- (NSString *)privateID
{
if (!_privateID) {
_privateID = @"foo";
}

return _privateID;
}

@end

//Person+Private.h
@interface Person (Private)

@property (nonatomic, strong) NSString *privateID;

@end

//Person+Private.m
@implementation Person (Private)
@dynamic privateID;

@end

第二种解决方案:

当然,聪明的你自然会想到,既然Category没法定义ivar,那直接在类扩展中声明一个ivar不就行了。于是你可以这样写,但是这会出现一个编译警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Person+Private.h
@interface Person () {
NSString *_privateID;
}

@property (nonatomic, strong) NSString *privateID;

@end

// Person+Private.m
@implementation Person (Private)

// Compile Warning: category override method from class
- (NSString *)privateID
{
if (!_privateID) {
_privateID = @"foo";
}

return _privateID;
}

由于在类扩展中已经定义了属性,那么这个类在编译期间会自动合成存取方法,而在Private Category中覆盖就会覆盖本身合成的方法(虽然我们确实需要这样),但由于可以在多处定义Category,并且方法覆盖的顺序不定,无法保证你的存取方法就是真实想要的,所以这是编译警告。对于这种需要自定义存取方法的私有属性的case,应该在类扩展中定义ivar,在Private Category中定义属性并实现。注意由于在类扩展定义了ivar,不会自动生成getter+setter,需要自行同时定义setter和getter,注意对不同属性修饰符,比如copy的话setter需要用[-copy]weak的话ivar要标注__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
// Person+Private.h
@interface Person () {
NSString *_privateID;
}

@end

@interface Person (Private)

@property (nonatomic, strong) NSString *privateID;

@end

// Person+Private.m
@implementation Person (Private)

- (NSString *)privateID
{
if (!_privateID) {
_privateID = @"foo";
}

return _privateID;
}

- (void)setPrivateID:(NSString *)privateID
{
_privateID = privateID;
}

分类(Category)和关联对象

由于Objective-C的属性,其实就是ivar+getter方法+setter方法,我们可以在使用的地方通过Runtime来获取ivar。但是这种方式实际上来说是用的人非常少。第一个是复杂,第二个是不好使用一个通用的宏进行转换(因为ivar需要计算offset,根据不同类型的type encoding还不同……),而且对于这种需求来说优点大材小用了。因此我们一般都是使用关联对象(不了解的参见:Associated Object

使用了关联对象后,为了方便不必要繁琐地书写objc_getAssociatedObjectobjc_setAssociatedObject,我们可以定义一些宏来方便使用。由于属性是包括了语义和引用计数相关内容的,因此针对不同的属性修饰符,需要采用不同的宏来保证属性的语义。

属性修饰符的语义,可以参考clang官网的说明:Objective-C Automatic Reference Counting,如下:

assign implies __unsafe_unretained ownership.
copy implies __strong ownership, as well as the usual behavior of copy semantics on the setter.
retain implies __strong ownership.
strong implies __strong ownership.
unsafe_unretained implies __unsafe_unretained ownership.
weak implies __weak ownership.

由于属性修饰符只会影响setter,而不是getter,我们可以定义一个通用宏。对应的setter就需要单独根据情况编写。

1
2
#import <objc/runtime.h>
#define __GET_PROPERTY(property) objc_getAssociatedObject(self, @selector(property));

strong(retain)

strong或者retain,就是所有对象的默认属性存取行为,隐含着对对象进行retain而使引用计数+1。这个可直接通过关联对象的行为设置。

宏:

1
#define __SET_STRONG(property) objc_setAssociatedObject(self, @selector(property), property, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

示例:

1
2
3
4
5
6
7
8
9
10
11
@property (nonatomic, strong) NSNumber *number;

- (NSNumber *)number
{
return __GET_PROPERTY(number);
}

- (void)setNumber:(NSNumber *)number
{
__SET_STRONG(number)
}

copy

copy属性修饰,表示在调用setter的时候,首先需要对对象进行copy操作,然后再表示strong,在Objective-C中其实就是发送了copyWithZone:消息。这个可直接通过关联对象的行为设置。

宏:

1
#define __SET_COPY(property) objc_setAssociatedObject(self, @selector(property), property, OBJC_ASSOCIATION_COPY_NONATOMIC);

示例:

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

- (NSString *)name
{
return __GET_PROPERTY(name);
}

- (void)setName:(NSString *)name
{
__SET_COPY(name);
}

unsafe_unretained

unsafe_unretainedassign的语义是相同的,前者是ARC下加入的,而后者从MRC开始存在。一般来说,对于原始类型(intdoubleBOOLNSInteger)这些,由于本身就是copy by value,而且不存在对象和引用计数管理,因此属性声明用assign(很少见写unsafe_unretained,虽然允许)。

而对于对象而言,一般如果想表示不改变任何引用计数的弱引用,现在都用的是weak,因为unsafe_unretained不会像weak那样,在对象引用计数降到0被销毁后,自动置nil,而会保持指向的地址,因此可能随时都成为野指针而不安全。但是由于历史代码缘故,还有很少的代码库在用,姑且暂时保留。

这里我们定义一个宏,仅用于表示对象的unsafe_unretainedassign。这个可直接通过关联对象的行为设置。而对于原始类型的属性,参见下面的assign

宏:

1
#define __SET_UNSAFE_UNRETAINED(property) objc_setAssociatedObject(self, @selector(property), property, OBJC_ASSOCIATION_ASSIGN);

示例:

1
2
3
4
5
6
7
8
9
10
11
@property (nonatomic, unsafe_unretained) NSObject *unsafeObject;

- (NSObject *)unsafeObject
{
return __GET_PROPERTY(unsafeObject);
}

- (void)setUnsafeObject:(NSObject *)unsafeObject
{
__SET_UNSAFE_UNRETAINED(unsafeObject);
}

assign

区别于上面针对对象的unsafe_unretained assign语义,这里的assign特指对原始类型的属性修饰符。由于Runtime的Associated Object一定是一个Object,因此我们需要把原始类型进行装箱,封装为一个Object,在getter中拆箱,拿到真实的原始数据。这个过程由于我们一定是一个Object箱子,只装一个真实的原始数据,因此没有必要进行copy(箱子是唯一的,但是内容的原始数据来源是copy by value)。可以用strong来修饰。

对于不同的原始类型,装箱的方式不同,一般来说,对于数值类型(int、double、NSInteger),可以使用NSNumber来装箱。对于其他类型,比如结构体,可以使用NSValue来进行装箱(比如CGRect,NSRange, Pointer)。对于不同的装箱来说方式不同,因此不好在宏里面进行处理,直接接收一个装好箱的value就可以了。

宏:

1
#define __SET_ASSIGN(property, value) objc_setAssociatedObject(self, @selector(property), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

由于装箱方式不同,拆箱方式肯定不同。不过只要拿到箱子之后,自己根据类型来进行相应拆箱即可。

示例:

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
@property (nonatomic, assign) int age;
@property (nonatomic, assign) CGRect frame;

- (int)age
{
NSNumber *value = __GET_PROPERTY(age);
return value.intValue;
}

- (void)setAge:(int)age
{
__SET_ASSIGN(age, @(age));
}

- (CGRect)frame
{
NSValue *value = __GET_PROPERTY(frame);
return value.CGRectValue;
}

- (void)setFrame:(CGRect)frame
{
NSValue *value = [NSValue valueWithCGRect:frame];
__SET_ASSIGN(frame, value);
}

weak

weak属性指的是一个弱引用,不改变对象的引用计数,同时和assignunsafe_unretained的最大区别,在于有着自动置nil的安全性质。一旦weak对象被销毁,该引用不会成为一个野指针,而会被立即置为nil,保证了安全。对于如今的现代Objective-C,能表示弱引用全部使用weak,应当避免使用assignunsafe_unretained表示一个弱引用(就算考虑上性能问题,weak立即置nil采用了一个全局的weak表,由Runtime管理,开销和手动release基本一致,不太可能成为性能问题)。

由于weak的特殊性(全局weak表),关联对象本身就没有提供weak的语义行为,但是我们可以来模拟一个等价的行为。

第一种解决方案:
我们使用一个WeakContainer,只包含一个weak的属性,来存放真实的weak引用对象。这样,通过关联对象把整个WeakContainer关联到Category的属性上,然后存取使用的时候进行装箱和拆箱,解决方案即可。不过唯一的缺点是由于需要引入一个WeakContainer类,无法做到Header Only。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface WeakObjectContainer : NSObject

@property (nonatomic, weak) id object;

+ (instancetype)containerWithObject:(id)object;

@end

@implementation WeakObjectContainer

+ (instancetype)containerWithObject:(id)object
{
WeakObjectContainer *container = [[WeakObjectContainer alloc] init];
container.object = object;
return container;
}

@end

宏:

1
2
3
#import "WeakObjectContainer.h"
#define __SET_WEAK(property) objc_setAssociatedObject(self, @selector(property), [WeakObjectContainer containerWithObject:property], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
#define __GET_WEAK(property) [objc_getAssociatedObject(self, @selector(property)) object];

第二种解决方案:

为了做到Header only,我们需要借助一个匿名的block,首先定义一个weak引用指向属性值,然后block捕获它。这样子,只要把block关联到对象上,那么在getter的时候,通过直接执行block返回这个weak对象,就可以拿到真正的弱引用(实现时,block要用copy,而且要判空)。

宏:

1
2
3
4
5
#define __SET_WEAK(property) id __weak __weak_object = property; \
id (^__weak_block)() = ^{ return __weak_object; }; \
objc_setAssociatedObject(self, @selector(property), __weak_block, OBJC_ASSOCIATION_COPY);

#define __GET_WEAK(property) objc_getAssociatedObject(self, @selector(property)) ? ((id (^)())objc_getAssociatedObject(self, @selector(property)))() : nil;

示例:

1
2
3
4
5
6
7
8
9
10
11
@property (nonatomic, weak) id delegate;

- (id)delegate
{
return __GET_WEAK(delegate);
}

- (void)setDelegate:(id)delegate
{
__SET_WEAK(delegate);
}

自定义存取方法

自定义存取方法一般类的属性写法类似。比如说想要惰性初始化(即只有在第一次调用getter的时候,才会初始化属性)这里就不用_name来操作ivar,而是通过setter(当然也能用__SET_* 宏来直接操作关联对象)就可以了。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSString *)name
{
NSString *name = __GET_PROPERTY(name);
if (!name) {
name = @"foo";
[self setName:name];
}
return name;
}

- (void)setName:(NSString *)name
{
__SET_COPY(name);
}

隐藏内部方法

类扩展实现类的内部方法

Objective-C没有真正意义上的私有方法,毕竟是C语言的超集嘛。但是Objective-C提供了一个类扩展语法,允许定义方法的接口。因此,只要我们在.m实现文件中定义了一些内部方法,就可以对外隐藏(当然,class-dump selector这些是可以直接调用的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Person.m

@interface Person ()

- (void)internalMethod;

@end

@implementation Person

- (void)internalMethod
{
//...
}

@end

隐藏私有方法

分类实现类的私有方法

但一些情况下,我们需要很多库内部使用的类的私有方法(私有方法和内部方法虽然都不对外可见,但是其实目标不一样,私有方法一般是一些可以直接设置实例的状态,内部数据的危险方法,用于库内部的一些类之间,互相调用来使用。而内部方法一般放一些复杂流程处理,工具方法,是为了简化代码逻辑而使用的)这些方法需要和公开头文件的方法分开,保持对外隐藏。这时候就得用到Category。

我们可以把想要隐藏的私有方法,全部放到一个Private Category里面,库内部其他需要操作的地方,引用这个头文件即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Person+Private.h

@interface Person (Private)

- (void)privateMethod;

@end

// Person+Private.m
@implementation Person (Private)

- (void)privateMethod
{
//...
}

@end

暴露公开类的内部方法

对于公开类,我们有可能在实现中定义很多内部的方法,这些方法可能依赖一些上下文,或者是只在类扩展里面定义的属性(而不是在我们的Private分类里面)。当我们在库的其他地方,也想使用这些内部方法时,但是方法定义不在Private Header中(虽然实际上在类内部已经实现了)。我们需要一种方式来暴露类的内部方法。

1
2
3
4
5
6
7
8
9
10
11
12
//Person.m

- (void)publicMethod
{
//...
[self internalMethod];
}
//我们想暴露这个方法给其他引用了Private Header的地方使用
- (void)internalMethod
{
//...
}

第一种解决方案(错误示范):

使用一个Private Category,在头文件中暴露这个方法。但是由于是类本身而不是Category的方法,编译器会报找不到internalMethod的实现的warning(虽然它确实在本身的类中实现了)。我们是可以警告编译器,忽略warning,因为你知道实际上这个方法已经有了实现,只不过头文件没有暴露罢了。但是这种方法忽略警告,会忽略所有Private Category的方法检查,假如Person+Private.h中定义的方法真的没有在Person+Private.m中实现,也不会有任何警告,所以非常不推荐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Person+Private.h

@interface Person (Private)

- (void)internalMethod; //在类本身实现中的内部方法,想要暴露出去
- (void)privateMethod;

@end

//Person+Private.m

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
@implementation Person (Private)
//...
@end
#pragma clang diagnostic pop

第二种解决方案:

使用类拓展(而不是Private Category)来暴露一个内部方法,实际上这才是最佳的方式,因为类扩展并不局限于任何地方,而且可以在任何.h或者.m中进行声明。实际上,类扩展只有@interface而不能有@implementation,是方法的接口而不是实现,不会出现方法重定义或者覆盖的问题。这样,我们在类扩展中加入实际类的内部方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Person+Private.h

@interface Person ()

- (void)internalMethod;

@end

@interface Person (Private)

- (void)privateMethod;

@end

//Person+Private.m
@implementation Person (Private)
//...
@end

因为类扩展在编译器检查时,是需要对类本身实现的方法进行检查的,因此假如Person类本身没有实现internalMethod,编译器会报warning,这也保证了正确性。

总结

Objective-C毕竟已经几十年的语言了,语法层面上对抽象隐藏支持的就不好,不像Swift提供了四种访问控制关键字:publicinternalfileprivateprivate,而且支持Module,再也不用担心命名和重定义问题了。不过Swift的现状,在Swift 4.0 ABI还不能稳定的情况下,代码库分发就只能使用源代码,这点对于很多开发者还有企业的影响确实比较大。不过了解Objective-C的实现也不是什么坏事,毕竟谁不定总会有需要写的的时候。希望这些代码库的接口与实现隐藏的方法,能够帮到一些平时没有接触过代码库开发的人吧。

资料

  1. 完整Category属性宏
❌
❌