目录
C 语言的条件编译(Conditional Compile)
-
一般编译器的预编译指令,分为以下4类:
- 文件包含:例如 #include,是一种最为常见的预处理,用于文件的引用
- 条件编译:例如 #if、#ifdef、#ifndef、#endif,预编译时进行有选择的编译,注释掉一些指定的代码,以达到版本控制、防止对文件重复包含的功能
- 布局控制:例如 #pragma,主要功能是为编译程序提供非常规的控制流信息
- 宏替换:例如 #define,它可以定义符号常量、功能函数、进行重新命名和字符串的拼接等等
-
条件编译
#if 格式
功能:当表达式的值为真时,编译代码段1,否则编译代码段2。其中,#else 和 代码段2 可有可无#if 表达式 代码段1 #else 代码段2 #endif
#ifdef 格式
功能:当标识符已被定义时(用 #define 定义),编译代码段1,否则编译代码段2。其中 #else 和 代码段2 可有可无#ifdef 标识符 代码段1 #else 代码段2 #endif
#ifndef 格式
功能:当标识符没被定义时(用 #define 定义),编译代码段1,否则编译代码段2。其中 #else 和 代码段2 可有可无#ifndef 标识符 代码段1 #else 代码段2 #endif
注意:
预编译器根据条件编译指令有选择的删除代码,编译器编译时不知道代码分支的存在
#if、#ifdef、#ifndef、#endif 被预编译器处理,而 if … else… 语句被编译器处理
条件编译指令在预编译期进行分支判断,而 if … else … 语句在程序运行期间进行分支判断 -
#include 和 条件编译的应用
#include 的本质是将已经存在的文件内容嵌入到当前文件中,#include 的间接包含同样会产生嵌入文件内容的操作。可以理解为:预编译时, #include 会将指定文件的内容复制到 #include 所在的位置。
如下代码会在 main.c 中产生重复定义问题:// ---------------- Calc00.h ---------------- #include <stdio.h> int add(int num0, int num1); int sub(int num0, int num1); // ---------------- Calc00.c ---------------- #include "Calc00.h" int add(int num0, int num1) { return num0 + num1; } int sub(int num0, int num1) { return num0 - num1; }
// ---------------- Calc01.h ---------------- #include <stdio.h> #include "Calc00.h" int multipl(int num0, int num1); int divis(int num0, int num1); int mix(int num0, int num1); // ---------------- Calc01.c ---------------- #include "Calc01.h" int multipl(int num0, int num1) { return num0 * num1; } int divis(int num0, int num1) { if (0 == num1) { return 0; } return num0 / num1; } int mix(int num0, int num1) { return multipl(add(num0, num1), sub(num0, num1)); }
// ---------------- main.c ---------------- #include <stdio.h> #include "Calc00.h" #include "Calc01.h" int main(int argc, const char * argv[]) { int result = add(1, 2); return 0; }
在 XCode 中编译 C 或者 Objective-C 代码时,其实参与编译的只有 .c 文件和 .m 文件,.h文件是不参与编译的。我们可以在 Build Phases - Compile Sources 中看到,参与编译的只有 .c 文件和 .m 文件:
但是因为我们在 .c 文件中,使用了 #include 导入头文件;在 .m 文件中使用了 #import 导入头文件,所以(C 和 Objective-C)头文件 .h 里面的内容,实际上也会参与编译。在 XCode 中,创建 C 代码的时候,.h 文件里面默认会生成如下预编译指令,这些预编译指令的作用就是防止重复导入(相当于 Objective-C 中 #improt 的功能):
Test.h 里面的内容如下:#ifndef Test_h #define Test_h #include <stdio.h> // 这里编写代码 #endif /* Test_h */
Test.c 里面的内容如下:
#include "Test.h" // 这里编写代码
Objective-C 中的 __bridge、__bridge_retained、__bridge_transfer
-
__bridge 只做类型转换,但是不修改对象(内存)管理权
-
__bridge_retained(也可以使用 CFBridgingRetain)将 Objective-C 的对象转换为 Core Foundation 的对象,同时将对象(内存)的管理权交给程序员,后续需要使用 CFRelease 或者相关方法来释放对象
-
__bridge_transfer(也可以使用 CFBridgingRelease)将 Core Foundation 的对象转换为 Objective-C 的对象,同时将对象(内存)的管理权交给ARC
初识 LLDB
- LLDB 全称为 Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器,默认内置于 Xcode 中,是 XCode 默认的调试器。我们平时用 Xcode 运行程序,实际走的都是 LLDB
- 在本节中,只需要了解 LLDB 的 x 指令即可
- x 指令 相当于 memory read 指令,是 memory read 指令的缩写
- 使用方式:
// 读取从 list 地址开始的,16 个 Byte 的内存数据,并以 16 进制显式 (lldb) x/16bx list 0x100534f10: 0x0a 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x100534f18: 0x60 0x56 0x53 0x00 0x01 0x00 0x00 0x00
// 读取从 list 地址开始的,16 个 Word 的内存数据,并以 16 进制显式 // 注意,在 64bit 的 CPU 里面,1Word == 4Byte (lldb) x/16wx list 0x100534f10: 0x0000000a 0x00000000 0x00535660 0x00000001 0x100534f20: 0x534e5b2d 0x63756f54 0x72614268 0x6f6c6f43 0x100534f30: 0x73694c72 0x63695074 0x4272656b 0x4372756c 0x100534f40: 0x61746e6f 0x72656e69 0x77656956 0x696e6920
HCGLinearList 封装
-
上一篇中虽然完成了 HCGLinearList 的功能,但没有对其进行封装和优化,HCGLinearList 还存在以下缺陷:
- LinearList 结构体的定义暴露在 .h 文件中,外界可以直接拿到 LinearList 结构体的成员进行操作。如:通过设置 LinearList.length = 0,就可以达到清空线性表的目的(listClear() 函数形同虚设);将 LinearList.length 的值设置得比 LinearList.capacity 的值大;通过修改LinearList.capacity 的值企图修改线性表的容量;通过 LinearList.value 直接操作存储在线性表中的元素,修改 LinearList.value 指向的内存地址 等等
- 由于线性表中定义的 LinearListNodeValue 为 int 类型,所以线性表只能存储 int 类型的数据,不够通用
基于以上分析,对 HCGLinearList 做如下修改:
-
将 HCGLinearList.h 中 对 LinearList 结构体的定义放到 HCGLinearList.c,对外只暴露一个 void 类型的 LinearList,这样外界无法知道 LinearList 的具体定义,也就修改不了 LinearList 结构体的成员了。同时,使用条件编译,防止在 HCGLinearList.h 和 HCGLinearList.c 中对结构体 LinearList 和指针 LinearListNodeValue 进行重复定义。
-
将 LinearListNodeValue 的类型由 int 改为 void*,即表明线性表存储的节点数据为指针类型。那么此时 LinearList.value 为指向指针数据的指针。
HCGLinearList.h 如下:// -------------------------------- HCGLinearList.h -------------------------------- #ifndef LinearList_h #define LinearList_h #include <stdio.h> #pragma mark - 宏定义(条件编译) #ifndef LINEARLIST_STRUCT // 线性表节点数据 typedef void* LinearListNodeValue; // 线性表 typedef void LinearList; #endif #pragma mark - 创建 销毁 清空 // 创建线性表 // param0.线性表容量 // return.线性表指针 LinearList* listCreat(int capacity); // 销毁线性表 // param0.线性表指针 void listRelease(LinearList* list); // 清空线性表 // param0.线性表指针 void listClear(LinearList* list); #pragma mark - 属性获取 // 获取线性表的长度 // param0.线性表指针 // return.线性表长度 int listLength(LinearList* list); // 获取线性表容量 // param0.线性表指针 // return.线性表容量 int listCapacity(LinearList* list); #pragma mark - 增 // 往线性表中插入数据 // param0.线性表指针 // param1.要插入的位置的索引 // param2.要插入的值 void listInsert(LinearList* list, int index, LinearListNodeValue value); // 往线性表中添加数据(添加在表尾) void listAdd(LinearList* list, LinearListNodeValue value); #pragma mark - 删 // 删除线性表中指定索引位置的元素 // param0.线性表指针 // param1.索引 void listRemove(LinearList* list, int index); #pragma mark - 改 // 修改线性表中指定位置的元素为指定的值 // param0.线性表指针 // param1.索引 // param2.值 void listSet(LinearList* list, int index, LinearListNodeValue value); #pragma mark - 查 // 获取线性表指定索引处元素的值 // param0.线性表指针 // param1.索引 // return.元素的值 LinearListNodeValue listGet(LinearList* list, int index); #pragma mark - 特殊功能 // 删除线性表中具有指定值的所有元素 // param0.线性表指针 // param1.要删除的值 void listRemoveValue_1(LinearList* list, LinearListNodeValue value); // 删除线性表中具有指定值的所有元素 // param0.线性表指针 // param1.要删除的值 void listRemoveValue_2(LinearList* list, LinearListNodeValue value); // 打印线性表 // param0.线性表指针 void listPrint(LinearList* list); #endif /* LinearList_h */
HCGLinearList.c 如下:
// -------------------------------- HCGLinearList.c -------------------------------- #pragma mark - 定义线性表结构体 // 数据节点指针 typedef void* LinearListNodeValue; // 线性表结构体 typedef struct { int capacity; // 容量 int length; // 长度 LinearListNodeValue* value; // 节点数据的指针(相当于 void** value,指向指针的指针) } LinearList; /* 注意:宏 LINEARLIST_STRUCT 的定义,一定要在导入 HCGLinearList.h 之前 这是为了防止上面的 LinearListNodeValue 指针和 LinearList 结构体被重复定义 **/ // #define LINEARLIST_STRUCT #pragma mark - 导入头文件 #include "HCGLinearList.h" // 使用 mallc函数 需要导入 #include <stdlib.h> #pragma mark - 创建 销毁 清空 // 创建线性表 LinearList* listCreat(int capacity) { if (capacity < 0) { return NULL; } // 分配线性表结构体的内存空间(在堆区) // malloc函数如果遇到内存资源紧张,给不了这么多字节,可能会返回空 LinearList* list = malloc(sizeof(LinearList)); if (list) { list->capacity = capacity; list->length = 0; // 分配存储线性表元素的内存空间 list->value = malloc(capacity * sizeof(LinearListNodeValue)); if (!list->value) { return NULL; } } return list;; } // 销毁线性表 void listRelease(LinearList* list) { if (NULL == list) { return; } if (list->value) { free(list->value); } free(list); } // 清空线性表 void listClear(LinearList* list) { if (NULL == list) { return; } // 这里不需要对线性表中的元素都置0 // 只要将线性表的长度置为0,下次使用线性表的时候,就会对之前的数据进行覆盖了 list->length = 0; } #pragma mark - 属性获取 // 获取线性表的长度 int listLength(LinearList* list) { if (NULL == list) { return 0; } return list->length; } // 获取线性表容量 int listCapacity(LinearList* list) { if (NULL == list) { return 0; } return list->capacity; } #pragma mark - 增 // 往线性表中插入数据 void listInsert(LinearList* list, int index, LinearListNodeValue value) { if (NULL == list) { return; } // 可以在表尾进行插入,因此这里的条件是 index > list->length,而不是 index >= list->length if (index < 0 || index > list->length) { return; } if (list->length == list->capacity) { return; } // 反向for循环挪动数据:从表尾数据开始挪动直到index标识的位置,每个数据依次向后挪动一个步长 for (int i = list->length - 1; i >= index; i--) { list->value[i + 1] = list->value[i]; } // 将新值value插入到index的位置 list->value[index] = value; // 线性表的长度 + 1 list->length++; } // 往线性表中添加数据(添加在表尾) void listAdd(LinearList* list, LinearListNodeValue value) { if (NULL == list) { return; } listInsert(list, list->length, value); } #pragma mark - 删 // 删除线性表中指定索引位置的元素 void listRemove(LinearList* list, int index) { if (NULL == list) { return; } if (index < 0 || index > list->length - 1) { return; } // 反向for循环挪动数据:从(index + 1)标识的位置开始直到表尾,每个数据依次向前挪动一个步长 for (int i = index + 1; i < list->length; i++) { list->value[i - 1] = list->value[i]; } /* // 等效写法 for (int i = index; i < list->length - 1; i++) { list->value[i] = list->value[i + 1]; } */ // 线性表的长度 - 1 list->length--; } #pragma mark - 改 // 修改线性表中指定位置的元素为指定的值 void listSet(LinearList* list, int index, LinearListNodeValue value) { if (NULL == list) { return; } if (index < 0 || index > list->length - 1) { return; } list->value[index] = value; } #pragma mark - 查 // 获取线性表指定索引处元素的值 LinearListNodeValue listGet(LinearList* list, int index) { if (NULL == list) { return 0; } if (index < 0 || index > list->length - 1) { return 0; } return list->value[index]; } #pragma mark - 特殊功能 // 删除线性表中具有指定值的所有元素 - 代码简单,但是效率低(时间复杂度高) void listRemoveValue_1(LinearList* list, LinearListNodeValue value) { if (NULL == list) { return; } // 遍历所有元素 for (int i = 0; i <= list->length - 1; i++) { while (list->value[i] == value && i <= list->length - 1) { listRemove(list, i); } } } // 删除线性表中具有指定值的所有元素 - 效率较高 void listRemoveValue_2(LinearList* list, LinearListNodeValue value) { if (NULL == list) { return; } // 遍历所有元素 int removeCount = 0; for (int i = 0; i <= list->length - 1; i++) { if (list->value[i] == value) { removeCount++; } else { list->value[i - removeCount] = list->value[i]; } } // 将长度减去删除的个数 list->length -= removeCount; } // 打印线性表 void listPrint(LinearList* list) { if (NULL == list) { return; } printf("list{\n"); printf("\tlength = %d;\n", list->length); printf("\tcapacity = %d;\n", list->capacity); printf("\tvalue = ["); for (int i = 0; i <= list->length - 1; i++) { printf("%p", list->value[i]); if (i <= list->length - 2) { printf(","); } } printf("];\n\t}\n\n"); }
Person.h 和 Person.m 如下:
// -------------------------------- Person.h -------------------------------- #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject #pragma mark - 属性 // 姓名 @property (nonatomic, copy) NSString* name; // 年龄 @property (nonatomic, assign) int age; #pragma mark - 构造方法 +(instancetype)personWithName:(NSString *)name age:(int)age; -(instancetype)initWithName:(NSString *)name age:(int)age; #pragma mark - 对象方法 -(void)introduce; @end NS_ASSUME_NONNULL_END // -------------------------------- Person.m -------------------------------- #import "Person.h" @implementation Person #pragma mark - 构造方法 +(instancetype)personWithName:(NSString *)name age:(int)age { return [[self alloc] initWithName:name age:age]; } -(instancetype)initWithName:(NSString *)name age:(int)age { if (self = [super init]) { self.name = name; self.age = age; } return self; } #pragma mark - 对象方法 -(void)introduce { NSLog(@"Hello, I am %@, I am %d year old!", self.name, self.age); } @end
main.m 如下:
#import <Foundation/Foundation.h> #import "HCGLinearList.h" #import "Person.h" // 线性表存储基本数据类型 void saveElementaryDemo() { LinearList* list = listCreat(10); // 基本数据类型,需要强转为 LinearListNodeValue(即 void*)类型后再保存到线性表里面 listAdd(list, (LinearListNodeValue)1); listAdd(list, (LinearListNodeValue)2); listAdd(list, (LinearListNodeValue)3); listAdd(list, (LinearListNodeValue)4); listPrint(list); /* 打印结果:value[] 里面以 16进制保持着 0 1 2 3 4 list{ length = 5; capacity = 10; value = [0x0,0x1,0x2,0x3,0x4]; } */ listRelease(list); list = NULL; } // 线性表存储指针类型 void savePointerDemo() { LinearList* list = listCreat(10); // Objective-C 的对象本质上(在底层)就是一个结构体的指针 Person* person0 = [Person personWithName:@"P0" age:0]; Person* person1 = [Person personWithName:@"P1" age:1]; Person* person2 = [Person personWithName:@"P2" age:2]; NSLog(@"%p,%p,%p", person0, person1, person2); // 输出结果 // 0x1004b1620,0x1004b1bf0,0x1004b1c10 // __bridge 是给编译器看的,告诉编译器将 Objective-C 环境下的对象,变成 C 环境下的指针类型。 // 通过 __bridge 将 Objective-C 环境下的 Person 对象,转换为 C 环境下的 LinearListNodeValue(void*)类型 listAdd(list, (__bridge LinearListNodeValue)(person0)); listAdd(list, (__bridge LinearListNodeValue)(person1)); listAdd(list, (__bridge LinearListNodeValue)(person2)); listPrint(list); /* 打印结果(value[] 里面保存的值,为 person0, person1, person2 的地址,即 value[] 里面保存的是指针): list{ length = 3; capacity = 10; value = [0x1004b1620,0x1004b1bf0,0x1004b1c10]; } */ // 取出节点数据 Person* p = (__bridge Person *)(listGet(list, 0)); [p introduce]; // 打印结果: // Hello, I am P0, I am 0 year old! listRelease(list); list = NULL; } int main(int argc, const char * argv[]) { @autoreleasepool { // saveElementaryDemo(); // savePointerDemo(); } return 0; }
这里有个有趣的地方:
Objective-C 的 NSArray、NSMutableArray,可以直接存放对象,但是如果要存放基本数据类型,则需要对基本数据类型进行包装。
C 的 LinearList,可以直接存放指针类型,但是存放基本数据类型需要强转,存放 Objective-C 对象需要桥接。
LLDB 内存分析 与 逆向初步
-
通过 LLDB 对 LinearList 的内存进行分析
main 函数中的代码如下:
通过 LLDB 读取 list 的内存进行分析,我们可以看到:int main(int argc, const char * argv[]) { @autoreleasepool { LinearList* list = listCreat(10); listAdd(list, (LinearListNodeValue)0); listAdd(list, (LinearListNodeValue)1); listAdd(list, (LinearListNodeValue)2); listAdd(list, (LinearListNodeValue)3); listAdd(list, (LinearListNodeValue)4); listPrint(list); listRelease(list); list = NULL; } return 0; }
- 由于 LinearList 的 length(int)、capacity(int)、value(void*) 分别占用 4Byte、4Byte、8Byte,而且结构体的成员在内存中的存储顺序与结构体成员在结构体中的定义顺序是一致的,所以可以看出
list->capacity = 10
list->length = 5
list->value = 0x00000001005a6f90
LLDB 控制台输出如下:(lldb) x/32xb list 0x10059d8b0: 0x0a 0x00 0x00 0x00 0x05 0x00 0x00 0x00 0x10059d8b8: 0x90 0x6f 0x5a 0x00 0x01 0x00 0x00 0x00 0x10059d8c0: 0x2d 0x5b 0x4e 0x53 0x54 0x61 0x62 0x50 0x10059d8c8: 0x69 0x63 0x6b 0x65 0x72 0x56 0x69 0x65
- 通过 LLDB 读取 list->value 保存的地址,可以看到存储在堆区的线性表 list 的元素的值
(lldb) x/96xb 0x01005a6f90 0x1005a6f90: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x1005a6f98: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x1005a6fa0: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x1005a6fa8: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x1005a6fb0: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x1005a6fb8: 0x74 0xa3 0x05 0x10 0x00 0x00 0x00 0x20 0x1005a6fc0: 0x66 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x1005a6fc8: 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x1005a6fd0: 0x28 0x65 0x78 0x70 0x65 0x72 0x69 0x6d 0x1005a6fd8: 0x65 0x6e 0x74 0x61 0x6c 0x5f 0x6d 0x61 0x1005a6fe0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x1005a6fe8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
-
通过 LLDB 对 NSArray 的内存进行分析
main 函数中的代码如下:int main(int argc, const char * argv[]) { @autoreleasepool { // 注意: // 1.字符串常量,在编译的那一刻,地址就确定了,不会再发生改变 // 2.根据字符串的这一特性,可以利用字符串的地址,反推 NSArray 存储数组元素的位置 NSString* name0 = @"jack"; NSString* name1 = @"jones"; NSString* name2 = @"hcg"; NSString* name3 = @"hzp"; NSArray* array = @[name0, name1, name2, name3]; NSLog(@"name0 = %p, name1 = %p, name2 = %p, name3 = %p", name0, name1, name2, name3); NSLog(@"array = %@", array); NSLog(@"array.count = %ld", array.count); /* 输出结果如下: name0 = 0x100002048, name1 = 0x100002068, name2 = 0x100002088, name3 = 0x1000020a8 array = ( jack, jones, hcg, hzp ) array.count = 4 */ } return 0; }
通过 LLDB 读取 array 的内存进行分析,我们可以看到:
NSArray 存储数组对象的属性与元素的内存空间是连续的
我们可以猜测,NSArray 底层可能只通过一次 malloc 就一次性开辟了存储 数组对象的属性和元素的内存空间
而不是像 HCGLinearList 那样使用两次 malloc,一次用于开辟存储数组属性的内存空间,一次用于开辟存储数组元素的内存空间
这样做的好处是:少调用了一次 malloc 和 free,代码简洁,提高了性能
属性和元素放在连续的内存空间里,根据局部性原理,也可以提高性能
注意:可以通过反复修改不可变数组的长度再并运行,来确定:不可变数组在内存中的第二个8字节,是用来存储数组的 count 属性。(lldb) x/64xb array 0x10060d5f0: 0x59 0xf4 0xbd 0x8c 0xff 0xff 0x1d 0x01 0x10060d5f8: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x10060d600: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x10060d608: 0x68 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x10060d610: 0x88 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x10060d618: 0xa8 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x10060d620: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x10060d628: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 (lldb) po 0x0100002048 jack (lldb) po 0x0100002068 jones (lldb) po 0x0100002088 hcg (lldb) po 0x01000020a8 hzp
-
拓展
原本 NSArray 的 count 为只读属性,但是我们可以通过定位到 array 对象在内存中的地址,通过直接操作内存,来修改 array.countint main(int argc, const char * argv[]) { @autoreleasepool { // 注意: // 1.字符串常量,在编译的那一刻,地址就确定了,不会再发生改变 // 2.根据字符串的这一特性,可以利用字符串的地址,反推 NSArray 存储数组元素的位置 NSString* name0 = @"jack"; NSString* name1 = @"jones"; NSString* name2 = @"hcg"; NSString* name3 = @"hzp"; NSArray* array = @[name0, name1, name2, name3]; NSLog(@"name0 = %p, name1 = %p, name2 = %p, name3 = %p", name0, name1, name2, name3); NSLog(@"array = %@", array); NSLog(@"array.count = %ld", array.count); /* 输出结果: name0 = 0x100002048, name1 = 0x100002068, name2 = 0x100002088, name3 = 0x1000020a8 array = ( jack, jones, hcg, hzp ) array.count = 4 **/ // 直接操作内存,修改 NSArray 的只读属性 count // 注意:iOS 底层有防御性编程,不可变数组 array 初始化时的容量为4,即系统为不可变数组分配的内存空间大小为 4 * 8Byte = 32Byte // 如果将 array.count 修改为小于等于 4 的数值,程序可以运行通过。 // 但是如果将 array.count 修改为 大于 4 的数值,由于运行时发生内存越界,程序会奔溃。 void* address = (__bridge void*)array; *((long *)address + 1) = 2; // 打印输出 NSLog(@"array.count = %ld", array.count); NSLog(@"array = %@", array); /* 输出结果: array.count = 2 array = ( jack, jones ) **/ } return 0; }
修改后,通过 LLDB 读取 array 的内存地址,可以看到,存储 count 属性值的内存地址,已经被修改为 2(原先的值为 4)
(lldb) x/64xb array 0x10060beb0: 0x59 0xf4 0xbd 0x8c 0xff 0xff 0x1d 0x01 0x10060beb8: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x10060bec0: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x10060bec8: 0x68 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x10060bed0: 0x88 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x10060bed8: 0xa8 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x10060bee0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x10060bee8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
NSMutableArray 扩容分析
-
Objective-C 中的数组
Objective-C 中的数组本身是一个对象,数组中存放的元素是对象的引用
Objective-C 中的数组分为:不可变数组(NSArray)和可变数组(NSMutableArray)
不可变数组(NSArray)初始化之后不能修改数组的内容
可变数组(NSMutableArray)初始化之后可以随时修改数组的内容(增加元素,删除元素,修改元素)
可变数组的容量:- 随着数组元素的增加而动态地增加
- 随着数组元素的删除而动态的减少
-
通过 LLDB 对 NSMutableArray 的内存进行分析
main 函数中的代码如下:int main(int argc, const char * argv[]) { @autoreleasepool { // 打印字符串常量的地址 NSString* str = @"jack"; NSLog(@"str addr = %p", str); // 创建可变数组 int capacity = 0; NSMutableArray* mArr = [NSMutableArray arrayWithCapacity:capacity]; // 往数组里面添加元素 int total = 10; for (int i = 0; i < total; i++) { [mArr addObject:str]; } } return 0; }
通过 LLDB 查看 mArr 的内存情况:
str addr = 0x100002048 (lldb) x/96xb mArr 0x100535720: 0x89 0xdd 0xbd 0x8c 0xff 0xff 0x1d 0x01 0x100535728: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x100535730: 0xc0 0x5a 0x53 0x00 0x01 0x00 0x00 0x00 0x100535738: 0x00 0x00 0x00 0x00 0x0a 0x00 0x00 0x00 0x100535740: 0x0b 0x00 0x00 0x00 0x0a 0x00 0x00 0x00 0x100535748: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x100535750: 0x2d 0x5b 0x4e 0x53 0x54 0x69 0x74 0x6c 0x100535758: 0x65 0x62 0x61 0x72 0x43 0x6f 0x6e 0x74 0x100535760: 0x61 0x69 0x6e 0x65 0x72 0x56 0x69 0x65 0x100535768: 0x77 0x20 0x74 0x69 0x74 0x6c 0x65 0x48 0x100535770: 0x65 0x69 0x67 0x68 0x74 0x54 0x6f 0x48 0x100535778: 0x69 0x64 0x65 0x49 0x6e 0x46 0x75 0x6c
发现:
字符串常量 str 的地址,并没有保存在可变数组 mArr 的堆区
我们推测,可变数组 mArr 应该是在堆区另外开辟了一段内存空间用于存储 mArr 的元素
在可变 mArr 的堆区,应该会有 8Byte 的内存空间,记录着指向 mArr 存储数组元素的堆区的首地址
观察可变 mArr 堆区的内存数据,发现:第 3 行的数值,非常像一个堆区的内存地址
因此,将第 3 行当成内存地址,并进行打印:(lldb) x/96xb 0x0100535ac0 0x100535ac0: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100535ac8: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100535ad0: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100535ad8: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100535ae0: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100535ae8: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100535af0: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100535af8: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100535b00: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100535b08: 0x48 0x20 0x00 0x00 0x01 0x00 0x00 0x00 0x100535b10: 0x03 0x3c 0x14 0x79 0xff 0x7f 0x00 0x00 0x100535b18: 0x9a 0x20 0x52 0xb8 0x00 0x00 0x00 0x00
我们看到了在 0x0100535ac0 这段内存地址中,存储着 10 个字符串常量 str 的地址
由此可以确定,可变数组 mArr 堆区的第 3 行(8Byte)记录着指向 存储数组元素的堆区的首地址修改 main 函数中的变量 capacity 和 total,反复进行单步调试并通过 LLDB 打印 mArr 每次添加元素后堆区的内存变化
我们发现:- 当可变数组 mArr 的元素添加到一定程度时,mArr 堆区第 3 行(记录着 指向存储数组元素的堆区的首地址)的数值会发生改变,即 mArr 进行了扩容
- 当 mArr 堆区第 3 行的数值发生改变时,mArr 堆区第 4 行的后 4 个字节的数值,也会增加一点,猜测其数值为可变数组 mArr 的当前容量(capacity)
- 可变数组 mArr 每次添加元素时,mArr 堆区第 5 行的前 4 个字节和后 4 个字节,都会 + 1。并且:
mArr 堆区第 5 行的前 4 个字节永远 == 当前元素数量 + 1
mArr 堆区第 5 行的后 4 个字节永远 == 当前元素数量
我们猜测:
mArr 堆区第 5 行的前 4 个字节作用未知
mArr 堆区第 5 行的后 4 个字节为 mArr.count
为了更好地分析 NSMutableArray 的扩容机制,编写 NSMutableArray 的分类 MemoryAnalyse 如下:
// -------------------------------- NSMutableArray+MemoryAnalyse.h -------------------------------- #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface NSMutableArray (MemoryAnalyse) // 返回存储数组的元素的堆区的首地址 -(void *)elementHeapAddress; // 获取当前数组的容量 -(int)getCurrentCapacity; // 设置当前数组的容量 -(void)setCurrentCapacity:(int)capacity; // 获取当前数组的元素个数 -(int)getCurrentCount; // 设置当前数组的元素个数 -(void)setCurrentCount:(int)count; @end NS_ASSUME_NONNULL_END
// -------------------------------- NSMutableArray+MemoryAnalyse.m -------------------------------- #import "NSMutableArray+MemoryAnalyse.h" @implementation NSMutableArray (MemoryAnalyse) // 返回存储数组的元素的堆区的首地址 -(void *)elementHeapAddress { // 获取数组对象的首地址 void* arrAddress = (__bridge void*)self; // void* 为万能指针,可以指向任意数据类型,即 void*指针 的步长是不确定 // 因此,void* 类型的指针,不能直接通过 + 或 - 来移动指针 // void** 为 指向 void*类型 的指针,其步长为确定的 8Byte(因为 void* 占8个字节 )(因为在 64bit CPU 下,一个指针占 8Byte)。 // 根据之前的观察,NSMutableArray 的第 3 行是 存储数组的元素的堆区的首地址 // 偏移量为 3 * 8Byte = 24Byte,步数从 0 开始计算,void** 的步长为 8Byte,则有 24 / 8 - 1 = 2 void* elementHeapAddress = *((void **)arrAddress + 2); return elementHeapAddress; } // 获取当前数组的容量 -(int)getCurrentCapacity { // 获取数组对象的首地址 void* arrAddress = (__bridge void*)self; // 根据之前的观察,NSMutableArray 的第 4 行的后 4 个 Byte 为 当前数组的容量 // 偏移量为 4 * 8Byte = 32Byte,步数从 0 开始计算,int* 步长为 4Byte,则有 32 / 4 - 1 = 7 return *((int *)arrAddress + 7); } // 设置当前数组的容量 -(void)setCurrentCapacity:(int)capacity { // 获取数组对象的首地址 void* arrAddress = (__bridge void*)self; // 根据之前的观察,NSMutableArray 的第 4 行的后 4 个 Byte 为 当前数组的容量 // 偏移量为 4 * 8Byte = 32Byte,步数从 0 开始计算,int* 步长为 4Byte,则有 32 / 4 - 1 = 7 *((int *)arrAddress + 7) = capacity; } // 获取当前数组的元素个数 -(int)getCurrentCount { // 获取数组对象的首地址 void* arrAddress = (__bridge void*)self; // 根据之前的观察,NSMutableArray 的第 5 行的后 4 个 Byte 为 当前数组的元素个数 // 偏移量为 5 * 8Byte = 40Byte,步数从 0 开始计算,int* 步长为 4Byte,则有 40 / 4 - 1 = 9 return *((int *)arrAddress + 9); } // 设置当前数组的元素个数 -(void)setCurrentCount:(int)count { // 获取数组对象的首地址 void* arrAddress = (__bridge void*)self; // 根据之前的观察,NSMutableArray 的第 5 行的后 4 个 Byte 为 当前数组的元素个数 // 偏移量为 5 * 8Byte = 40Byte,步数从 0 开始计算,int* 步长为 4Byte,则有 40 / 4 - 1 = 9 *((int *)arrAddress + 9) = count; } @end
在 main 函数中,编写代码,分析 NSMutableArray 的扩容机制:
int main(int argc, const char * argv[]) { @autoreleasepool { // 打印字符串常量的地址 NSString* str = @"jack"; NSLog(@"str addr = %p", str); // 创建可变数组 int capacity = 10; NSMutableArray* mArr = [NSMutableArray arrayWithCapacity:capacity]; // 打印可变数组扩容时机,扩容容量等 int total = 512; int currentTimes = 0; int preTimes = currentTimes; void* currentElementHeapAddr = mArr.elementHeapAddress; // 可变数组的容量,随着数组元素的增加而动态地增加 NSLog(@"Arr存储了[%03d]个元素时 currentElementHeapAddr = %p, currentCapacity = %d", 0, mArr.elementHeapAddress, [mArr getCurrentCapacity]); for (currentTimes = 0; currentTimes < total; currentTimes++) { [mArr addObject:str]; // 存储数组的元素的堆区的首地址发生改变 - 可变数组进行扩容 if (currentElementHeapAddr != mArr.elementHeapAddress) { NSLog(@"currentElementHeapAddr = %p, currentTimes = %03d, capacity = %03d", mArr.elementHeapAddress, currentTimes, [mArr getCurrentCapacity]); currentElementHeapAddr = mArr.elementHeapAddress; preTimes = currentTimes; } } // 可变数组的容量,随着数组元素的减少而动态地减少 NSLog(@"Arr存储了[%03d]个元素时 currentElementHeapAddr = %p, currentCapacity = %d", total, mArr.elementHeapAddress, [mArr getCurrentCapacity]); for (currentTimes = 0; currentTimes < total; currentTimes++) { [mArr removeLastObject]; // 存储数组的元素的堆区的首地址发生改变 - 可变数组进行扩容 if (currentElementHeapAddr != mArr.elementHeapAddress) { NSLog(@"currentElementHeapAddr = %p, currentTimes = %03d, capacity = %03d", mArr.elementHeapAddress, currentTimes, [mArr getCurrentCapacity]); currentElementHeapAddr = mArr.elementHeapAddress; preTimes = currentTimes; } } /* 打印输入如下: str addr = 0x100003048 Arr存储了[000]个元素时 currentElementHeapAddr = 0x100751170, currentCapacity = 10 currentElementHeapAddr = 0x100405730, currentTimes = 010, capacity = 016 currentElementHeapAddr = 0x100751200, currentTimes = 016, capacity = 026 currentElementHeapAddr = 0x1007512d0, currentTimes = 026, capacity = 042 currentElementHeapAddr = 0x100507470, currentTimes = 042, capacity = 068 currentElementHeapAddr = 0x100507700, currentTimes = 068, capacity = 110 currentElementHeapAddr = 0x102808200, currentTimes = 110, capacity = 192 currentElementHeapAddr = 0x102808800, currentTimes = 192, capacity = 320 currentElementHeapAddr = 0x102809200, currentTimes = 320, capacity = 576 Arr存储了[512]个元素时 currentElementHeapAddr = 0x102809200, currentCapacity = 576 currentElementHeapAddr = 0x101009200, currentTimes = 291, capacity = 256 currentElementHeapAddr = 0x10066f0c0, currentTimes = 414, capacity = 098 currentElementHeapAddr = 0x10066eb50, currentTimes = 474, capacity = 038 currentElementHeapAddr = 0x10066efe0, currentTimes = 497, capacity = 014 currentElementHeapAddr = 0x10066f050, currentTimes = 506, capacity = 006 currentElementHeapAddr = 0x100661ec0, currentTimes = 509, capacity = 002 **/ } return 0; }
注意:
mArr[0] 是获取数组第一个元素的值,我们无法通过 mArr[0] 来获取 可变数组存储元素的堆区的首地址
并且我们不能通过常规手段,获取到可变数组存储元素的堆区的首地址
HCGLinearList 扩容
-
通过上面对 NSMutableArray 的内存进行分析,得到 Objective-C 对于 NSMutableArray 动态变更容量的思路:
- 当 NSMutableArray 的元素数量增加或减少到一定程度时,就 malloc 出一块适当大小的新的堆空间
- 将旧堆空间的数据 复制到 新的堆空间
- 释放掉旧的堆空间
-
按照 NSMutableArray 扩容的思路,优化 HCGLinearList 代码:
HCGLinearList.h 文件,相对于上面的代码,没有做任何改变:#ifndef LinearList_h #define LinearList_h #include <stdio.h> #pragma mark - 宏定义(条件编译) #ifndef LINEARLIST_STRUCT // 线性表节点数据 typedef void* LinearListNodeValue; // 线性表 typedef void LinearList; #endif #pragma mark - 创建 销毁 清空 // 创建线性表 // param0.线性表容量 // return.线性表指针 LinearList* listCreat(int capacity); // 销毁线性表 // param0.线性表指针 void listRelease(LinearList* list); // 清空线性表 // param0.线性表指针 void listClear(LinearList* list); #pragma mark - 属性获取 // 获取线性表的长度 // param0.线性表指针 // return.线性表长度 int listLength(LinearList* list); // 获取线性表容量 // param0.线性表指针 // return.线性表容量 int listCapacity(LinearList* list); #pragma mark - 增 // 往线性表中插入数据 // param0.线性表指针 // param1.要插入的位置的索引 // param2.要插入的值 void listInsert(LinearList* list, int index, LinearListNodeValue value); // 往线性表中添加数据(添加在表尾) void listAdd(LinearList* list, LinearListNodeValue value); #pragma mark - 删 // 删除线性表中指定索引位置的元素 // param0.线性表指针 // param1.索引 void listRemove(LinearList* list, int index); #pragma mark - 改 // 修改线性表中指定位置的元素为指定的值 // param0.线性表指针 // param1.索引 // param2.值 void listSet(LinearList* list, int index, LinearListNodeValue value); #pragma mark - 查 // 获取线性表指定索引处元素的值 // param0.线性表指针 // param1.索引 // return.元素的值 LinearListNodeValue listGet(LinearList* list, int index); #pragma mark - 特殊功能 // 删除线性表中具有指定值的所有元素 // param0.线性表指针 // param1.要删除的值 void listRemoveValue_1(LinearList* list, LinearListNodeValue value); // 删除线性表中具有指定值的所有元素 // param0.线性表指针 // param1.要删除的值 void listRemoveValue_2(LinearList* list, LinearListNodeValue value); // 打印线性表 // param0.线性表指针 void listPrint(LinearList* list); #endif /* LinearList_h */
HCGLinearList.c 文件,相对于上面的代码,做了三处更改:
- 头文件处 #include <string.h>
- listCreat(int capacity) 函数 capacity 为 0 时,不申请堆空间
- listInsert(LinearList* list, int index, LinearListNodeValue value) 函数添加了扩容的代码
#pragma mark - 定义线性表结构体 // 数据节点指针 typedef void* LinearListNodeValue; // 线性表结构体 typedef struct { int capacity; // 容量 int length; // 长度 LinearListNodeValue* value; // 节点数据的指针(相当于 void** value,指向指针的指针) } LinearList; /* 注意:宏 LINEARLIST_STRUCT 的定义,一定要在导入 HCGLinearList.h 之前 这是为了防止上面的 LinearListNodeValue 指针和 LinearList 结构体被重复定义 **/ // #define LINEARLIST_STRUCT #pragma mark - 导入头文件 #include "HCGLinearList.h" // 使用 mallc函数 需要导入 #include <stdlib.h> // 使用内存拷贝函数 memcpy 需要导入 #include <string.h> #pragma mark - 创建 销毁 清空 // 创建线性表 LinearList* listCreat(int capacity) { if (capacity < 0) { return NULL; } // 分配线性表结构体的内存空间(在堆区) // malloc函数如果遇到内存资源紧张,给不了这么多字节,可能会返回空 LinearList* list = malloc(sizeof(LinearList)); if (list) { list->capacity = capacity; list->length = 0; // 分配存储线性表元素的内存空间 list->value = (list->capacity == 0 ? NULL : malloc(capacity * sizeof(LinearListNodeValue))); if (!list->value) { return NULL; } } return list;; } // 销毁线性表 void listRelease(LinearList* list) { if (NULL == list) { return; } if (list->value) { free(list->value); } free(list); } // 清空线性表 void listClear(LinearList* list) { if (NULL == list) { return; } // 这里不需要对线性表中的元素都置0 // 只要将线性表的长度置为0,下次使用线性表的时候,就会对之前的数据进行覆盖了 list->length = 0; } #pragma mark - 属性获取 // 获取线性表的长度 int listLength(LinearList* list) { if (NULL == list) { return 0; } return list->length; } // 获取线性表容量 int listCapacity(LinearList* list) { if (NULL == list) { return 0; } return list->capacity; } #pragma mark - 增 // 往线性表中插入数据 void listInsert(LinearList* list, int index, LinearListNodeValue value) { if (NULL == list) { return; } // 可以在表尾进行插入,因此这里的条件是 index > list->length,而不是 index >= list->length if (index < 0 || index > list->length) { return; } // 判断是否需要扩容 if (list->length == list->capacity) { // 1.申请新的堆空间 int tempCapacity = list->capacity + 10; LinearListNodeValue* tempValue = malloc(sizeof(LinearListNodeValue) * tempCapacity); if (NULL == tempValue) { return; } // 2.将旧的堆空间的数据 复制到 新的堆空间 /* for (int i = 0; i < list->capacity; i++) { tempValue[i] = list->value[i]; } */ // param0.目标地址 // param1.来源地址 // param2.要拷贝的字节数 memcpy(tempValue, list->value, sizeof(LinearListNodeValue) * list->capacity); // 3.释放旧的堆空间 free(list->value); list->value = tempValue; // 4.修改capacity list->capacity = tempCapacity; } // 反向for循环挪动数据:从表尾数据开始挪动直到index标识的位置,每个数据依次向后挪动一个步长 for (int i = list->length - 1; i >= index; i--) { list->value[i + 1] = list->value[i]; } // 将新值value插入到index的位置 list->value[index] = value; // 线性表的长度 + 1 list->length++; } // 往线性表中添加数据(添加在表尾) void listAdd(LinearList* list, LinearListNodeValue value) { if (NULL == list) { return; } listInsert(list, list->length, value); } #pragma mark - 删 // 删除线性表中指定索引位置的元素 void listRemove(LinearList* list, int index) { if (NULL == list) { return; } if (index < 0 || index > list->length - 1) { return; } // 反向for循环挪动数据:从(index + 1)标识的位置开始直到表尾,每个数据依次向前挪动一个步长 for (int i = index + 1; i < list->length; i++) { list->value[i - 1] = list->value[i]; } /* // 等效写法 for (int i = index; i < list->length - 1; i++) { list->value[i] = list->value[i + 1]; } */ // 线性表的长度 - 1 list->length--; } #pragma mark - 改 // 修改线性表中指定位置的元素为指定的值 void listSet(LinearList* list, int index, LinearListNodeValue value) { if (NULL == list) { return; } if (index < 0 || index > list->length - 1) { return; } list->value[index] = value; } #pragma mark - 查 // 获取线性表指定索引处元素的值 LinearListNodeValue listGet(LinearList* list, int index) { if (NULL == list) { return 0; } if (index < 0 || index > list->length - 1) { return 0; } return list->value[index]; } #pragma mark - 特殊功能 // 删除线性表中具有指定值的所有元素 - 代码简单,但是效率低(时间复杂度高) void listRemoveValue_1(LinearList* list, LinearListNodeValue value) { if (NULL == list) { return; } // 遍历所有元素 for (int i = 0; i <= list->length - 1; i++) { while (list->value[i] == value && i <= list->length - 1) { listRemove(list, i); } } } // 删除线性表中具有指定值的所有元素 - 效率较高 void listRemoveValue_2(LinearList* list, LinearListNodeValue value) { if (NULL == list) { return; } // 遍历所有元素 int removeCount = 0; for (int i = 0; i <= list->length - 1; i++) { if (list->value[i] == value) { removeCount++; } else { list->value[i - removeCount] = list->value[i]; } } // 将长度减去删除的个数 list->length -= removeCount; } // 打印线性表 void listPrint(LinearList* list) { if (NULL == list) { return; } printf("list{\n"); printf("\tlength = %d;\n", list->length); printf("\tcapacity = %d;\n", list->capacity); printf("\tvalue = ["); for (int i = 0; i <= list->length - 1; i++) { printf("%p", list->value[i]); if (i <= list->length - 2) { printf(","); } } printf("];\n\t}\n\n"); }
main.c 文件,测试 HCGLinearList 的扩容功能:
#import <Foundation/Foundation.h> #import "HCGLinearList.h" int main(int argc, const char * argv[]) { @autoreleasepool { // 创建线性表 LinearList* list = listCreat(5); // 添加数据 int total = 20; for (int i = 0; i < total; i++) { listAdd(list, (LinearListNodeValue)i); NSLog(@"list->length = %02d, list->value = %p", listLength(list), *((void**)list + 1)); } // 打印 listPrint(list); // 销毁线性表 listRelease(list); list = NULL; /** 输出结果如下: list->length = 01, list->value = 0x10064b520 list->length = 02, list->value = 0x10064b520 list->length = 03, list->value = 0x10064b520 list->length = 04, list->value = 0x10064b520 list->length = 05, list->value = 0x10064b520 list->length = 06, list->value = 0x100407b40 list->length = 07, list->value = 0x100407b40 list->length = 08, list->value = 0x100407b40 list->length = 09, list->value = 0x100407b40 list->length = 10, list->value = 0x100407b40 list->length = 11, list->value = 0x100407b40 list->length = 12, list->value = 0x100407b40 list->length = 13, list->value = 0x100407b40 list->length = 14, list->value = 0x100407b40 list->length = 15, list->value = 0x100407b40 list->length = 16, list->value = 0x100408d40 list->length = 17, list->value = 0x100408d40 list->length = 18, list->value = 0x100408d40 list->length = 19, list->value = 0x100408d40 list->length = 20, list->value = 0x100408d40 list{ length = 20; capacity = 25; value = [0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf,0x10,0x11,0x12,0x13]; } */ } return 0; }