背景:由于基础比较差,所以最近在恶补C语言以及其他基础学科例如:操作系统之类的基础学科。然后结合笔记和自己的理解写了一点C和OC的联系。
首先从最基础的开始-指针 C语言的精华就在于指针(pointer),接下来我们看图说话 贴一段示范代码:
刚开始我们声明了一个int类型的指针变量但是没有初始化,然后给a赋值,这时候我们运行程序会爆内存泄露的问题。这是因为初始化a的时候系统默认a这个指针变量指向0x0这个地址(IDE为Xcode,编译器为LLVM),这时候向*a所指向的地方进行赋值是也就是向0x0这个地址赋值,这样的行为被编译器默认是非法的,假如是使用的其他的编译器,如果编译器不认定这种行为非法,可能会产生各种诡异的情况,因为没有初始化过的指针对象有可能指向内存的任何区域,冒然的对所指向的地址进行赋值可能会引发程序崩溃。 这时候我们需要先对指针变量进行初始化操作如图:
因此我们可以得出结论:b作为指针变量,它存的值与a在内存中地址是一致的,通过'*'b对指针变量所存的地址进行取值也就是2。然后我们打印指针b的地址 &b,结果与之前的相差8个字节,这说明指针b也是一个变量,它在内存中也是有地址的,所存储的值就是所指向变量的地址。
由打印结果引出下面的讨论:由于我们声明的是本地变量,所以变量被分配到栈里,由编译器进行生命周期的管理(ARC就是编译器特性),在栈里存放的是函数的参数值,本地变量的值。由于移动端的设备属于寄存器计算机,一切运算都是在寄存器中完成,内存是无法进行运行的,下文的rsp和rbp是指针寄存器,当一个函数被执行的时候,首先会把本地变量和所需参数压入栈内,然后通过rsp(栈顶指针)和rbp(基地址指针)进行操作,当函数被调用时,rbp先入栈,保存当前栈的状态值,用来后面恢复本栈的状态,然后将rsp装入rbp更新栈的底部,然后切换到下一个函数(也就是下一个栈),然后将rsp减去所需空间大小,抬高栈顶。在函数执行完毕后会先pop局部变量和参数,然后根据栈底的rbp指针找到下一个函数的入口以及其他本地变量和参数。在OC中我们所说的作用域就是函数执行的范围,本地变量执行完后会自动销毁是因为在栈内被pop了,导致引用计数减1。而全局变量静态变量都在内存的静态区域,所以不需要程序员进行管理。接下来我们看另一种打印图:
我们可以发现全局变量和静态变量在同一片区域,本地变量,成员变量在栈区,但是两者的地址差距非常大,这是为什么呢?首先我们的清楚成员变量本质是ivar,而每个OC对象都是struct objc_object,在这个结构体的内部有一个isa指针,这个isa指针指向struct objc_class。这个结构体应该大家都熟悉:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
//...
};
superclass是指向metaClass的指针。 cache是这个类的方法缓存,每当调用方法的时候会现在cache里查找,没有就添加到缓存中,提高命中率。 bits 给类分配空间的标志。 data 存的就是方法列表,成员变量等数据,通过查找可以看到ivars的结构体。
struct ivar_list_t {
uint32_t entsize;
uint32_t count;
ivar_t first;
};
通过查找这个结构体里的first结构体
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
//...
};
我们发现了offset,而且是一个指针,它记录了这个ivar,所以当我们打印成员变量地址的时候会首先根据struct objc_object这个结构体里的isa指针找到成员变量所在的类的struct objc_class,runtime根据类对象在内存中的地址然后加上offset就可以找到成员变量了。因此我们可以根据Extension在编译期添加成员变量,runtime会通过修改offset来更新成员变量的偏移距。那么为什么不能在程序运行期间动态添加成员变量呢?那是因为在编译期,编译期就会根据给定的成员变量计算空间大小并布局,假如可以动态的添加成员变量,就会导致runtime在访问已创建出来的子类的时候出现无法识别的情况,因为布局改变,runtime通过offset查找成员变量的时候就无法保证准确性。 那么为什么runtime可以动态添加方法呢? 1.因为方法列表struct objc_method_list **methodLists 这个结构体没有isa指针,所以没有指向一个固定的内存区域。 2.苹果给这个结构体声明了一个二阶指针,因此决定了外界可以动态的修改方法。 3.这个方法列表的数据结构是一个链表,由于Category可以“覆盖”类的原方法,但是经检测证明原方法还在只不过位于Category方法的后一个,因此可以推测应该是单向链表,当添加一个Category方法对原方法进行覆盖时,首先runtime会遍历这个链表,直到找到这个方法为止,每个方法也是一个结构体,
typedef struct method_t{
const char *name;
struct method_t * next;
//...
}
当需要插入的这个方法前面时,首先判断方法名与目标方法名是否相等: 若相等,先创建节点a(也就是struct method_t),然后使前一个节点的next指针指向a,然后使a->next指向原方法的节点。这样子保证Category的方法能一直在原方法前面,runtime调用同名方法根据next指针一个个查找,直到发现同名方法为止。
然后我们来看一下操作系统。 首先提出一点,操作系统中,线程的切换其本质是栈的切换。 然后又分为用户级线程和核心级线程,每一个用户级线程维护一个栈,每个核心机线程维护一套栈。当用户级线程相互切换的时候,是通过一个映射表将ESP切换到内存不同位置,保证目标栈的唯一性。当用户级线程切换到内核级线程是系统通过调用fork从而将用户级线程切换到内核级线程,这时候函数调用就是在内核栈中进行的。
我们iOS上所用到的GCD就是基于内核级(XNU)线程实现的,因此普通代码级别的线程调度是无论如何都比不上内核级线程调度来的快,因为代码级别的线程一般都要在系统级别上实现。并且苹果已经将GCD的APi优化的足够高了,以至于GCD的API中关于block的循环引用问题都不需要我们考虑(因为Block只被DispatchQueue管理)。因为基于GCD 的block不是直接加入DispatchQueue中,而是先加入DispatchContinuation这个dispatch_continuation_t这个结构体中,然后再加入DispatchQueue这个FIFO的队列。该DispatchContinuation用于存储block所属的的dispatchGroup和其他信息,在执行GCD追加的block的时候,libdispatch(一个C函数库)从DispatchQueue取到DispatchContinuation,然后调用pthread_workqueue_np,将queue本身以及回调函数传递给参数,然后XNU内核级线程根据系统状态判断是否生成线程。
后面我会继续学习,假如有合适的机会还是会写笔记的。
相关链接: 网易云课堂-C语言程序设计进阶