一、抛砖引玉
现在有一个YDWPerson类 ,其中有一个属性 name 和一个实例方法saySomething,如下:
@interface YDWPerson : NSObject
@property ( nonatomic, copy) NSString * name;
- ( void ) saySomething;
@end
@implementation YDWPerson
- ( void ) saySomething {
NSLog ( @"%s" , __func__ ) ;
}
@end
Class cls = [ YDWPerson class] ;
void * boy = & cls;
[ ( __bridge id) boy saySomething] ;
二、调试分析
① 不访问类变量
YDWPerson * person = [ YDWPerson alloc] ;
[ person saySomething] ;
而 boy 指针指向 YDWPerson 类的地址,和对象 person 的 isa 指向 YDwPerson 类的地址一样,所以 [(__bridge id)boy saySomething];这里也能调用对象方法。
[person saySomething]的本质是对象发送消息,那么 person 的 isa 指向类YDWPerso,即 person 的首地址指向 YDWPerson 的首地址,我们可以通过YDWPerson 的内存平移找到 cache,在 cache 中查找方法:
[(__bridge id)boy saySomething] 中的 boy 是来自于 YDWPerson 这个类,然后有一个指针 boy,将其指向 YDWPerson 的首地址:
因此,person 是指向 YDWPerson 类的结构,boy 也是指向 YDWPerson 类的结构,然后都是在 YDWPerson 中的 methodList 中查找方法:
② 访问打印类变量
修改 saySomething 方法实现,如下所示:
@implementation YDWPerson
- ( void ) saySomething {
NSLog ( @"%s %@" , __func__ , self . name) ;
}
@end
可以看到:boy 调用打印的 name 是 <ViewController: 0x7fbee94051c0>,而 person 调用打印的 name 则是 (null),为什么打印不一致呢?
③ 原理分析
首先 person 调用 name,是由于 self 指向 person 的内存结构,然后通过内存平移8字节,去取值 name,即 self 指针首地址平移 8 字节获得,如下:
boy 是内存中一个 8 字节的指,指向 cls,相当于 person 指针指向 YDWPerson 的一个实例,所以 self.name 的获取,相当于 boy 首地址的指针也需要平移 8 字节寻找 name,那么此时的 boy 的指针地址是多少?平移 8 字节获得的是什么呢?
boy 是一个指针,是存在栈中的,栈是一个先进后出的结构,参数传入就是一个不断压栈的过程:
其中隐藏参数会压入栈,且每个函数都会有两个隐藏参数(id self,sel _cmd),可以通过 clang 查看底层编译;
隐藏参数压栈的过程,其地址是递减的,而栈是从高地址->低地址分配的,即在栈中,参数会从前往后一直压栈(栈是一个先进后出的队列,内存从高地址到低地址分配,所以先压入栈的地址高);
super 通过 clang 查看底层的编译,是objc_msgSendSuper,其第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass);
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" ) ) ;
Class cls = ( ( Class ( * ) ( id, SEL) ) ( void * ) objc_msgSend) ( ( id) objc_getClass ( "YDWPerson" ) , sel_registerName ( "class" ) ) ;
void * boy = & cls;
YDWPerson * person = ( ( YDWPerson * ( * ) ( id, SEL) ) ( void * ) objc_msgSend) ( ( id) objc_getClass ( "YDWPerson" ) , sel_registerName ( "alloc" ) ) ;
( ( void ( * ) ( id, SEL) ) ( void * ) objc_msgSend) ( ( id) ( __bridgeid) boy, sel_registerName ( "saySomething" ) ) ;
( ( void ( * ) ( id, SEL) ) ( void * ) objc_msgSend) ( ( id) person, sel_registerName ( "saySomething" ) ) ;
}
因此入栈的变量如下:self–>_cmd–> cls–> kc–> person,但是[super viewDidLoad];调用时会产生一个结构体传入参数,因此这个结构体也会被压入到当前栈中:self–>_cmd–>(id)class_getSuperclass(objc_getClass(“YDWTeacher”))–>self–> cls–> boy–> person;
self和_cmd是viewDidLoad方法的两个隐藏参数,是高地址->低地址正向压栈的;
class_getSuperClass 和 self为objc_msgSendSuper2中的结构体成员,是从最后一个成员变量,即低地址->高地址反向压栈的;
添加以下代码:
Class cls = [ YDWPerson class] ;
void * boy = & cls;
YDWPerson * person = [ YDWPerson alloc] ;
NSLog ( @"%p - %p" , & person, boy) ;
void * sp = ( void * ) & self ;
void * end = ( void * ) & person;
long count = ( sp - end) / 0x8 ;
for ( long i = 0 ; i< count; i++ ) {
void * address = sp - 0x8 * i;
if ( i == 1 ) {
NSLog ( @"%p : %s" , address, * ( char * * ) address) ;
} else {
NSLog ( @"%p : %@" , address, * ( void * * ) address) ;
}
}
[ ( __bridge id) boy saySomething] ;
[ person saySomething] ;
2021 - 03 - 20 20 : 19 : 16.190498 + 0800 内存平移[ 68463 : 5044422 ] 0x7ffee77cd058 - 0x7ffee77cd068
2021 - 03 - 20 20 : 19 : 16.190649 + 0800 内存平移[ 68463 : 5044422 ] 0x7ffee77cd088 : < ViewController: 0x7f7fbe2045f0 >
2021 - 03 - 20 20 : 19 : 16.190745 + 0800 内存平移[ 68463 : 5044422 ] 0x7ffee77cd080 : viewDidLoad
2021 - 03 - 20 20 : 19 : 16.190853 + 0800 内存平移[ 68463 : 5044422 ] 0x7ffee77cd078 : ViewController
2021 - 03 - 20 20 : 19 : 16.190934 + 0800 内存平移[ 68463 : 5044422 ] 0x7ffee77cd070 : < ViewController: 0x7f7fbe2045f0 >
2021 - 03 - 20 20 : 19 : 16.191027 + 0800 内存平移[ 68463 : 5044422 ] 0x7ffee77cd068 : YDWPerson
2021 - 03 - 20 20 : 19 : 16.191135 + 0800 内存平移[ 68463 : 5044422 ] 0x7ffee77cd060 : < YDWPerson: 0x7ffee77cd068 >
2021 - 03 - 20 20 : 19 : 16.191238 + 0800 内存平移[ 68463 : 5044422 ] - [ YDWPerson saySomething] < ViewController: 0x7f7fbe2045f0 >
2021 - 03 - 20 20 : 19 : 16.191333 + 0800 内存平移[ 68463 : 5044422 ] - [ YDWPerson saySomething] ( null)
其中,为什么 class_getSuperclass 是 ViewController,因为 objc_msgSendSuper2返回的是当前类,两个self,并不是同一个self,而是栈的指针不同,但是指向同一片内存空间;
[(__bridge id)kc saySomething]调用时,此时的 boy 是 YDWPerson: 0x7ffee77cd068,所以 saySomething 方法中传入 self 的还是 YDWPerson,但并不是我们通常认为的 YDWPerson,而是我们当前传入的消息接收者,即YDWPerson: 0x7ffee77cd068,是 YDWPerson 的实例对象,此时的操作与普通的 YDWPerson 是一致的,即 YDWPerson 的地址内存平移 8 字节;
普通person流程:person -> name - 内存平移8字节;
boy 流程:0x7ffee77cd068 + 0x80 -> 0x7ffee77cd070,即为self,指向<ViewController: 0x7f7fbe2045f0>,如下图所示:
其中 person 与 YDWPerson 的关系是 person 是以 YDWPerson 为模板的实例化对象,即 alloc 有一个指针地址,指向 isa,isa 指向 YDWPerson,它们之间关联是有一个 isa 指向。
而 boy 也是指向 YDWPerson 的关系,编译器会认为 boy 也是 YDWPerson 的一个实例化对象,即 boy 相当于 isa,即首地址,指向 YDWPerson,具有和 person 一样的效果,简单来说,我们已经完全将编译器“骗”过了,即 boy 也有 name。由于person查找 name 是通过内存平移 8 字节,所以 boy 也是通过内存平移 8 字节去查找name;
④ 栈和堆分别存放了什么?
alloc的对象存放在堆中;
指针、对象存放于栈中,例如 person 指向的空间在堆中,person 所在的空间在栈中;
临时变量存放栈中;
属性值存放在堆中,属性随对象是存放在栈中;
三、总结
堆是从小到大,即低地址->高地址;
栈是从大到小,即从高地址->低地址分配;
函数隐藏参数会从前往后一直压栈,即 从高地址->低地址 开始入栈,
结构体内部的成员是从低地址->高地址;
一般情况下,内存地址有如下规则:
0x60 开头表示在堆中;
0x70 开头的地址表示在栈中;
0x10 开头的地址表示在全局区域中。