iOS 底层探索之路

iOS 底层探索:内存平移的分析验证

2020-12-07  本文已影响0人  欧德尔丶胡

iOS 底层探索: 学习大纲 OC篇

前言

举例分析

@interface LGPerson : NSObject
- (void)sayHello;
@end
@implementation LGPerson
- (void) saySomething { NSLog(@"%s: 你好", __func__); }
@end

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad 
{
    [super viewDidLoad];
   // 指针读取类地址,强转为对象,调用sayHello。
    Class cls = [LGPerson class];
    void * ht = &cls;
    [(__bridge id)ht saySomething];
    
    // 实例化对象,调用sayHello
    LGPerson * person = [LGPerson new];
    [person saySomething];
}
@end

发现打印如下: 相同的结果
-[LGPerson saySomething] : 你好
-[LGPerson saySomething] : 你好
我们在写saySomething的时候会发现 image.png

这里有代码的自动提示

分析如下:

所以经过上面的分析,我们就可以知道,为什么通过获取类的首地址可以调用该对象方法,得出一个结论:就是类的首地址通过是可以通过强转为这个person的对象的。

那么我们给类加个属性然后再方法中打印试试如下:

- (void)saySomething
{ 
    NSLog(@"%s - %@",__func__,self.kc_hobby); //kc_hobby是个字符串
}

{ 
    Class cls = [LGPerson class];
    void  *kc = &cls;  //
    [(__bridge id)kc saySomething];
    
    LGPerson *person = [LGPerson alloc];
    [person saySomething]; // self.kc_name = nil - (null)
 }  
---------
 -[LGPerson saySomething] - ViewController
 -[LGPerson saySomething] - (null)

第二个打印为null,我们都知道,但是为什么第一个self.kc_hobby打印的为ViewController呢?
首先我们先打印下这个kc如下:

解释*(void **)的作用:

  • (void**) 代表的是指向指针的指针。 读取是地址值

  • 加上 * 进行解引用:*(void **) ,可读取到地址中存放的内容。

分析:在给属性赋值的时候发现kc是没法调用属性的set方法,其次在进行内存平移读取属性的值的时候,很明显,发生的异常,在内存中属性会遵循内存对齐原则,字符串占8个字符。其实这里读到viewcontroller是发生了越界读取。这里也可以看出来,(__bridge id)kc 只是一个只有指针的对象,并不包含任何属性信息。
总结: &cls 只是骗编译器 他是个isa 其实他不是 他只是和isa 一样 指向了LGProson。

拓展: 结构体压栈

可以通过自定义一个结构体,判断结构体内部成员的压栈情况

所以图中可以得出 20先加入,再加入10,因此结构体内部的压栈情况是 低地址->高地址,递增的,栈中结构体内部的成员是反向压入栈,即低地址->高地址,是递增的,

回到kc的案例,可以通过下面这段代码打印下栈的存储:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ViewController 当前的类
    // self cmd (id)class_getSuperclass(objc_getClass("LGTeacher")) self cls kc person
    Class cls = [LGPerson class];
    void  *kc = &cls;  // 
    LGPerson *person = [LGPerson alloc];
    person.kc_hobby = @"KC";
    NSLog(@"%p - %p",&person,kc);
    // 隐藏参数 会压入栈帧
    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);
        }
    }
    
    // LGPerson  - 0x7ffeea0c50f8
    [(__bridge id)kc saySomething]; // 1 2  - <ViewController: 0x7f7f7ec09490>
    
    [person saySomething]; // self.kc_name = nil - (null)
    //
}

// 打印如下:

<ViewController: 0x100f076a0> [对应的 self ]
viewDidLoad  [对应的 _cmd]
ViewController [对应的 superClass]
<ViewController: 0x100f076a0> [对应的self]
LGPerson  [对应的cls]
<LGPerson: 0x16f44db48>  [对应的kc]

补充:
函数内部定义的局部变量和数组,都存放在栈区; (比如每个函数都有的(id self, SEL _cmd))

Clang当前的ViewController.m文件。

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
image.png

所以内存地址从高位到低位的顺序为:
self -> _cmd ->__rw_objc_super对象->cls->ht

看下__rw_objc_super结构体:

struct __rw_objc_super { 
    struct objc_object *object;  //在当前控制器里 self
    struct objc_object *superClass; //在当前控制器里 ViewController
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};

结构体内部元素,是后面元素先插入内存栈中。所以__rw_objc_super内部的内存顺序为: ViewController -> self

最终内存顺序为: self -> _cmd -> ViewController -> self -> cls -> ht

其中为什么class_getSuperclassViewController,因为objc_msgSendSuper2返回的是当前类,两个self,并不是同一个self,而是栈的指针不同,但是指向同一片内存空间

其中 person 与 LGPerson的关系是 person是以LGPerson为模板的实例化对象,即alloc有一个指针地址,指向isa,isa指向LGPerson,它们之间关联是有一个isa指向;

而kc也是指向LGPerson的关系,编译器会认为 kc也是LGPerson的一个实例化对象,即kc相当于isa,即首地址,指向LGPerson,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即kc也有`kc_hobby。由于person查找kc_hobby是通过内存平移8字节,所以kc也是通过内存平移8字节去查找kc_hobby。

哪些东西在栈里 哪些在堆里?

注意:

  • 是从小到大,即低地址->高地址
  • 是从大到小,即从高地址->低地址分配
上一篇下一篇

猜你喜欢

热点阅读