美文网首页iOS
iOS:OC变量的存储细节

iOS:OC变量的存储细节

作者: stonly916 | 来源:发表于2018-05-17 12:02 被阅读0次

    在iOS或者其他系统中,程序运行中的存储根据功能分类,基本都有这几个分类:

    • 栈区:用于存放临时内容,一般为局部变量,多为变量指针。
    • 堆区:存放可能需要保存时间长些的内容,这里存储需要申请存储空间,在OC中一般存放OC对象、malloc申请的结构体等。
    • 数据区:存放常量(包括静态常量, 常量字符串)等。

    这里说的静态常量与是否有static、const修饰无关。

    本文代码运行于Xcode模拟器iphoneX,64位内核,16位地址,以下出现指针地址不足16位的高位都补0;其栈地址一般是0x7ffeed386a60 这样以7为起始的12位16进制地址;堆地址一般是0x600000220df8 这样以6为起始的12位16进制地址;数据区地址一般是0x10287feaf 这样以1为起始的9位16进制地址。

    先看通常情况下的变量存储,用代码演示,本文所提到的输出地址都是采用代码printf(“%p”,value);

    1.存储在栈区

    + (void)theory
    {
        int i = 88;
        char a = 'a';
    }  
    

    上面这个变量i的地址 &i 为0x7ffeed386a6****0 这里是局部变量,i是直接存储在栈中的,&i就是这个栈内存的地址。

    + (void)theory
    {
       NSString *str0 = @"a";
    } 
    

    输出&str0:0x7ffeea2efcd8。str0是一个指针,指向字符传常量@“a”的指针,&str0是该指针的地址,该指针依然是局部变量,虽然字符串常量在堆中,但是该指针存储在栈中。

    2.存储在堆区

    typedef struct SY__block_impl_test0 {
    
      struct  __block_impl impl;
    
     char *ss;
    
     int saf;
    
    } SY__block_impl;
    
    SY__block_impl *aas = malloc(sizeof(SY__block_impl));
    
    LearnStruct *stru = [[LearnStruct  alloc] init];
    

    结构体使用malloc是在堆上申请内存空间的,aas的输出地址:0x604000457a30 这是一个堆地址。

    在OC中使用alloc方法创建的对象都是在堆中,oc对象也是结构体,所以也需要在堆区申请内存空间,上述stru输出地址:0x604000201af0 这同样是一个堆地址

    3.存储在数据区

    该区会存放全局数据:

    @interface  StoreTheory()
    
     ...
    
    @end
    
    NSString *stringGloba;
    
    NSString *stringGloba1 = @"123321123a";
    
    NSObject *obj = nil;
    
    @implementation StoreTheory
    
     ...
    
    @end
    

    这里的三个全局变量的指针都是存储在数据区的:

    &stringGloba  ->  0x10965f608
    
    &stringGloba1  ->  0x10965f458
    
    &obj  ->  0x10965f600
    

    数据区也分初始化数据(相对未初始化数据地址低)和未初始化数据,从&stringGloba和&obj的地址也可以看出来两个未初始化的数据是相连的。

    存放字符串常量:

    NSString *stringGloba1 = @"123321123a";
    NSString *str0 = @"a";
    NSString *str1 = @"b";
    NSString *string0 = @"a";
    NSString *string1 = @"b";
    

    这里的stringGlobal的输出地址:0x10a722790 ,字符串存储在数据区;这里还能发现str0和string0的地址都是:0x10a7227b0,str1和string1的地址也一样:如果是一样的字符串,那么他们都是指向同一个地址,系统不会重新申请空间存放一样的字符串常量

    以上讲了一些变量、常量的存储位置,接下来补充讲解下block的存储细节。

    Blk blockTest = ^() {
     printf("assa\n");
    };
    

    这是一个全局block,blockTest的输出是0x10a4714f0 表明了全局block的身份,&blockTest的输出是0x7ffee57967f8 这里blockTest是局部变量,地址在栈中也正常,但是我们会发现如果我们在控制台用po命令打印blockTest:

    po blockTest
    
    0x000000010a46c2e0
    

    为什么会出现一个比printf("%p",blockTest)地址要低的这个地址呢?我们可以想象比数据区的地址还低的是不是就是代码区,我们再回顾下block的clang源码

    struct __ClangProxy__staff_block_impl_0 {
     struct __block_impl impl;
     struct __ClangProxy__staff_block_desc_0* Desc;
     __ClangProxy__staff_block_impl_0(void *fp, struct __ClangProxy__staff_block_desc_0 *desc, int flags=0) {
     impl.isa = &_NSConcreteStackBlock;
     impl.Flags = flags;
     impl.FuncPtr = fp;
     Desc = desc;
     }
    };
    
    static void __ClangProxy__staff_block_func_0(struct __ClangProxy__staff_block_impl_0 *__cself) {
     printf("assa\n");
     }
    
    static struct __ClangProxy__staff_block_desc_0 {
     size_t reserved;
     size_t Block_size;
    } __ClangProxy__staff_block_desc_0_DATA = { 0, sizeof(struct __ClangProxy__staff_block_impl_0)};
    

    我们可以推测,我使用printf("%p",blockTest) 打印的地址其实是结构体struct __ClangProxy__staff_block_impl_0 存储在数据的地址,而直接在控制台po打印的地址是block的实现方法void __ClangProxy__staff_block_func_0(struct __ClangProxy__staff_block_impl_0 *__cself)在代码区的首地址,为了验证这一猜测,我添加了这个方法:

    void userWithBlock()
    {
      printf("%p",userWithBlock);
    }
    

    这个打印结果是0x10a46b800 这和block的直接打印地址在同一区段。

    并且,我在打印一个堆block时发现堆block的po打印地址是(void (^)()) myBlock = 0x000000010a46c310这个地址更接近0x10a46c2e0 ,由此也能看出来确实是代码区地址,各类型block也只有block实现方法是统一存储在代码区的

      __block int h = 9;    
      __block char *string = str;
      NSMutableString * string0 = [[NSMutableString  alloc]initWithString:@"123"];
      __block int *te = tt;
    
      printf("h--地址%p\n",&h);
      printf("string--地址%p\n",string);
      printf("&string--地址%p\n",&string);
      printf("te--地址%p\n",te);
      printf("&te--地址%p\n",&te);
    
     void(^staBlock1)(void) = ^{
       printf("h--地址%p\n",&h);
       printf("string--地址%p\n",string);
       printf("&string--地址%p\n",&string);
       printf("te--地址%p\n",te);
       printf("&te--地址%p\n",&te);
    
       h = 13;
       printf("h--地址%p",&h);
       te=&h;
       printf("te--地址%p",te);
       printf("&te--地址%p",&te);
    
       string = "bbssaaaaaaaasdf";
       printf("string--地址%p\n",string);
       printf("&string--地址%p\n",&string);
    
       [string0 appendString:@"456"];
     };
    

    上述代码打印结果:

    h--地址0x7ffee6f3f6f0
    string--地址0x108cc6e2d
    &string--地址0x7ffee6f3f618
    te--地址0x7ffee6f3f654
    &te--地址0x7ffee6f3f5c8
    
    h--地址0x604000431cd8
    string--地址0x108cc6e2d
    &string--地址0x60000023d658
    te--地址0x7ffee6f3f654
    &te--地址0x604000431cf8
    
    h--地址0x604000431cd8
    te--地址0x604000431cd8
    &te--地址0x604000431cf8 
    
    string--地址0x108cc6f0b
    &string--地址0x60000023d658
    

    首先要知道这里的block是从栈copy到堆的NSMallocBlock,然后看各个变量,

    • h:从这里可以看出h在使用__block修饰后,在block被copy到堆后,变量block也被copy到堆上了,如果熟悉block的clang源码就知道这里因为h转换成了结构体,结构体重新在堆上申请了内存空间,结构体的成员h是值拷贝到堆上的;所以h在block从block外的栈地址转变成了block内的堆地址,其赋值是改变内存中存储的值,不会使指针变化。

    • string:string是字符串常量的指针,在字符串没有改变时,它始终指向数据区的该字符串常量地址,所以不会改变,但是当字符串发生改变的时候,地址自然会出现改变。

    • &string:这是字符串指针string的存储地址,使用__block转换为结构体也是存储在栈的,在block外的时候是栈地址;在block内则会因为堆block而拷贝到堆上;这里需要注意的是随便地址由栈变为堆,但是存储的内容是字符串的地址,在未改变string值的时候是不会变化的。

    • te:这是一个指向栈内存tt的栈指针,在block内外都是指向tt的指针,所以没有变化;当将&h赋值给te时,te就是指向h的指针,此时与&h一样。

    • &te:相信到这里大家也能想到&te的变化了,&te是指针te的存储地址,block外是在栈中,block内则被拷贝到了堆中,&te其实就是结构体中的成员te的首地址,代码te=&h;的赋值只是改变了&te所在内存的内容,即修改了结构体中成员变量te的值,所以&te没有变化。

    block在拷贝到堆上的时候,会将引用到的在栈上的变量全部拷贝到堆上,因为栈内存由系统释放对于堆block的引用来说可能造成引用到已释放内存,所以在栈上的引用都会copy到堆上,这样在arc下这些变量的释放就由引用计数控制了。

    参考
    http://www.molotang.com/articles/2001.html

    相关文章

      网友评论

        本文标题:iOS:OC变量的存储细节

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