关于 static

本文尝试回答 static 关键字的区别

  • Objective-C 中 static 修饰的变量存储的位置?
  • OC 全局静态变量和局部静态变量的区别?
  • OC 全局静态变量和全局变量的区别?
  • static 关键字在 OC 和 Swift 中的区别?
  • Swift 中 static 修饰的类属性存储的位置?
  • static 变量和 macho文件的关系?

OC 中的 static 关键字是引用自 C 语言。被 static 修饰的变量被成为静态变量,主要影响变量的生命周期,被 static 修饰的变量是存储在内存的静态存储区。

static 相对的,变量还可能存储在栈上和堆上,栈上的内存是编译器管理的,堆上的内存是运行时动态分配的。

static 修饰的变量的生命周期就是程序进程的生命周期。但是 static 修饰变量的作用域是根据变量声明的位置变化而变化。

这里就涉及到全局静态变量和局部静态变量的区分

# 全局静态变量 vs 局部静态变量

不论是静态全局变量还是局部静态变量,变量的生命周期是没有区分的,主要区别在于作用域的区别。

在下面的例子中,我们声明了一个局部静态变量,每次点击按钮的时候会让 i 在原值基础上加一。

- (void)tap:(id)sender {
    static int i=0;    //定义局部静态变量
    i = i + 1;
    NSLog(@"i=%d",i);
}
i=1
i=2
i=3

说明变量 i 并没有像普通的自动变量一样,内存生命周期随着函数的调用结束而结束,而是一直存在着,但是 i 的作用域范围仅限在在 tap 函数之内,出了这个函数我们就访问不到 i 了。

我们定义全局静态变量的话是能让别的方法也能访问到这个变量,但是这里要重新梳理下全局的概念,下面两个例子有助于梳理在不同位置定义全局变量。

  • 在 .h 文件定义全局静态变量,那其他类引用静态全局变量所在类头文件后可以访问。

    //A.h
    static NSString *name = @"hello world";
    ---
    //B.h
    @interface B : NSObject
    @end
    //B.m
    #import "B.h"
    #import "A.h"
    @implementation B
    + (NSString *)getNameFromA {
        return name;   //这里访问的即为 A.h中定义的name
    }
    @end
    
  • 在 .m 文件定义全局静态变量,那就是只有在 .m 文件里方法能访问该变量。

    @implementation Test
    static int i = 5;
    - (void)increse { i = i + 1; }
    - (int)readi { return i; }
    @end
    

多说一句,我在网上看了一些文档 (opens new window),发现有人说 C 语言 static 修饰的全局静态变量,其他源文件不能访问。我自己试了试发现不是这样啊,C 和 OC 中 static 修饰符修饰静态全局变量的使用方式基本上一样;如果全局静态变量定义在 C 语言的 .h 文件中,其他的 C 语言源文件通过 .h 文件的引入,也能读取到对应的变量;当然你如果像 OC 一样把全局静态变量的定义放到 .m 里一样,放到 C 语言的 .c 文件里,别的文件当然就没办法读取到了。

# 全局静态变量和全局变量

我自己理解全局静态变量和全局变量在存储的位置并没有什么不同,都是存储在全局静态区,但是两者的使用方式是不同的。

我们上面已经说了全局静态变量在 .h 和 .m 中的定义方式和作用范围,同样我们也尝试声明全局变量在不同文件中的访问方式。

  • 在 .h 文件中定义

    //A.h
    NSString *name = @"global hello";
    //B.m
    #import "A.h"
    @implementation B
    - (NSString *)getNameFromA { return name; }
    

    这种情况下编译报错 duplicate symbol '_name' in: A.o 和 B.o,原因其实很简单,#import “A.h” 的时候本质上重新声明了一遍 name 变量。所以编译报错。这是和全局静态变量的区别,全局静态变量在这种情况下不会出现问题。

  • 在 .m 文件中定义,如下,这种场景下我理解全局静态变量定义和全局变量的作用基本上一样的。

    //A.m
    NSString *name = @"global hello";
    @implementation A
    - (void)modifyName { name = @"new global hello"; }
    - (NSString *)getName { return name; }
    @end
    

    如果我们想让外部文件也访问到此全局变量 name,我们需要在 .h 文件中添加 extern 声明

    //A.m
    NSString *name = @"global hello";
    //A.h
    extern NSString *name;
    //B.m
    #import "A.h"
    - (NSString *)getNameFromA { return name; }
    - (void)setNameToA {
    	name = @"set name to a from b";
    }
    

    如果想将此变量定义为常量的化,则使用 NSString * const name = @"global hello"; 这种声明方式。

# static 关键字在 OC 和 Swift 中的区别?

OC 中 static 关键字就表示定义全局静态变量或静态方法。在 Swift 中 static 关键字用于修饰类属性。比如

class A {
	static var a = A();
}

对应到 OC 这种类属性的写法如下,这种写法是从 iOS 10, Xcode 8 才开始支持的。

@interface A : NSObject
@property (class) A *a;
@end
@implementation 
//自己实现getter/setter方法
@end

至于 swiftstatic 变量存储的位置,参考了一些文档 (opens new window),应该也是存储在全局静态区的,一样是整个生命周期有效的。

所以 static 关键字在 OC 和 Swift 中只是使用的语义不同,但对应的存储位置还是一样的。

# 实战一下

**全局变量为值类型,**通过如下 DEMO 验证:

class A {
    static var a = "xxxx";
}

查看 A.a 的内存地址如下 100008140, 78 是 x 的 ASCII 码值。

Untitled

重新导出 memorygraph 文件,通过 vmmap --verbose TerminalToolSwiftDemo.memgraph 命令打印出来内存区间,发现 A.a 的内存地址 100008140 落在了 __DATA 区间上,确实是在静态区。

==== Writable regions for process 56120
REGION TYPE                    START - END         [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL
__DATA                      100008000-10000c000    [   16K    16K    16K     0K] rw-/rw- SM=COW          /path/to/TerminalToolSwiftDemo

全局变量为引用类型,通过如下 DEMO 验证:

class A {
	static var a = A();
}

导出 memgraph 文件之后,打印实例的内存地址之后发现类属性 A.a 的地址特别高 0x60000000c840MALLOC_NANO 内存区域里,并不在全局静态区里,因为感觉全局静态区是内存地址比较低的地方,而这个内存区域是用来分配特别小的对象。在想通过设置环境变量 MallocNanoZone 为 0 关掉这个特性,参考这里 (opens new window)

关掉之后发现 A.a 变量在 MALLOC_TINY 这个内存区域里,感觉全局静态区,应该是在 __DATA 这个 Region Type 里的,为什么是在 MALLOC_TINY 里?

我通过如下方式打印了下 A.a 变量本身的地址,得到结果 0x0000000100008140

withUnsafeMutablePointer(to: &A.a) { print("\($0)") }

发现指针本身的位置是在全局静态区,指针指向的位置是堆区。我感觉是因为 A 还是一个对象,需要通过 malloc 分配,可能没办法静态分配 (opens new window)内存。所以是这样的结果。

总结一下,当全局静态变量存储的变量为值类型的时候,值的内容也是存处在全局静态区;当全局静态变量存储的变量为引用类型的时候,真实对象存储的位置在堆上。

# static 变量和 macho文件的关系

没有直接的关系。理解这个问题需要知道macho和虚拟内存的映射关系。

如果 static 通过某个常量初始化的化,则该常量的存储位置在 mach-o 文件的 __DATA 段,这个 _DATA 段本质上也是映射到内存的全局静态区。

如果静态变量没有初始化的话,则会给变量在 .bss 段预留容量,最终也是映射到全局静态区,但是我们一定得知道静态变量的类型,否则没办法知道到底分配多大内存。Swift 中的类属性是一定要初始化好的。

Unlike stored instance properties, you must always give stored type properties a default value. This is because the type itself doesn’t have an initializer that can assign a value to a stored type property at initialization time.

以上如果有理解的不对的地方,欢迎指出来,有疑问也可以一起讨论一下

参考地址:

  1. extern——关键字 (opens new window)
  2. Open Nuts and Bolts of Memory Management in iOS : Swift Part 1 (opens new window)
  3. Memory Usage Performance Guidelines-Tips for Allocating Memory (opens new window)
  4. Xcode的vmmap、VM_Tracker和Allocations的调研笔记 (opens new window)
  5. Swift-Type Properties (opens new window)