美文网首页iOS 底层探索之路
iOS 底层探索:内存平移的分析验证

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

作者: 欧德尔丶胡 | 来源:发表于2020-12-07 11:10 被阅读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对象了;

    分析如下:

    • 我们知道对象方法的本质是:对象调用对象方法,是对象的isa指针去指向类本身,让类本身调用该对象方法。
    • 其次方法本质其实就是objc_msgSend进行消息转发,其中经过一系列的方法查找的过程;在快速查找过程中,就是去cache中查找方法,在这里回顾一下类和对象的数据结构如下:


    所以经过上面的分析,我们就可以知道,为什么通过获取类的首地址可以调用该对象方法,得出一个结论:就是类的首地址通过是可以通过强转为这个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]
    
    • 打印顺序为:
      self ->_cmd -> superclass -> self -> cls -> ht

    补充:
    函数内部定义的局部变量和数组,都存放在栈区; (比如每个函数都有的(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流程:person -> kc_hobby - 内存平移8字节

    • kc流程:经过内存平移+8,即为self,指向<ViewController:>

    其中 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。

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

    • alloc的对象 都在

    • 指针、对象中,例如person指向的空间中,person所在的空间在栈中

    • 临时变量

    • 属性值,属性随对象是在

    注意:

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

    相关文章

      网友评论

        本文标题:iOS 底层探索:内存平移的分析验证

        本文链接:https://www.haomeiwen.com/subject/icygwktx.html