kingcos / Perspective

📝 Write something with perspectives.
https://kingcos.me
185 stars 15 forks source link

Obj-C 中成员变量和类的访问控制 #74

Open kingcos opened 5 years ago

kingcos commented 5 years ago
Platform Env
macOS 10.14.2 gcc, clang

Preface

Obj-C 中的成员变量,即 Instance Variables,通常称为 ivar。在面向对象的概念中,一个类的对外暴露决定了其所提供的能力,对子类则提供扩展性,但有些时候我们也不希望外界甚至子类不知道一些细节,这时就用到了访问控制(Access Control)。在 C++、Java、Swift 等大多数高级语言中都有这样的概念,Obj-C 中总共有四种等级,这次就来简单谈谈 Obj-C 中成员变量的访问控制。

访问控制修饰符

@public

@interface Computer : NSObject {
    @public
    NSString *_name;
}
@end

@implementation Computer
@end

Computer *cpt = [[Computer alloc] init];
cpt->_name = @"My PC";
NSLog(@"%@", cpt->_name);

// My PC

声明为 @public 的成员变量是访问控制中开放范围最大的,其允许外界可以直接访问到(当然,前提是引入包含该类的头文件)。

@protected

@interface Computer : NSObject {
    int _memorySize;

    @protected
    int _diskSize;
}

@end

@implementation Computer
@end

@interface Mac : Computer
- (instancetype)initWithDiskSize:(int)diskSize memorySize:(int)memorySize;
- (instancetype)init NS_UNAVAILABLE;
- (void)printDiskAndMemoryInfo;
@end

@implementation Mac
- (instancetype)initWithDiskSize:(int)diskSize memorySize:(int)memorySize {
    self = [super init];
    if (self) {
        _diskSize = diskSize;
        _memorySize = memorySize;
    }
    return self;
}

- (void)printDiskAndMemoryInfo {
    NSLog(@"My Mac's disk size is %d GB, memory size is %d GB.", _diskSize, _memorySize);
}
@end

Mac *mac = [[Mac alloc] initWithDiskSize:512 memorySize:16];
[mac printDiskAndMemoryInfo];

// My Mac's disk size is 512 GB, memory size is 16 GB.

声明为 @protected 的成员变量只能在本类或子类中使用。注意,当不使用任何访问控制修饰符时,成员变量默认即为 @protected。其实,声明为 @property 的属性,系统会自动为我们创建一个 _ivar 的成员变量,这个成员变量的可见程度默认也是 @protected

@private

@interface Computer : NSObject {
    @private
    int _secretKey;
    int _secretData;
}
@end

@implementation Computer

- (instancetype)init {
    self = [super init];
    if (self) {
        _secretKey = arc4random();
    }
    return self;
}

- (void)setData:(int)data {
    _secretData = data ^ secretKey;
}
@end

声明为 @private 的成员变量是访问控制中开放范围最小的,只能被本类访问到。

@package

建立一个 Cocoa Framework 的工程,导入以下代码:

// Fruit.h
#import <Foundation/Foundation.h>
@interface Fruit : NSObject {
    @package
    NSString *_name;
}
@end

// FrameworkPublicHeader.h
#import "Person.h"

// main.m
Fruit *fruit = [[Fruit alloc] init];
fruit->_name = @"Apple";
NSLog(@"%@", fruit->_name);

// Dynamic Library:
// Undefined symbols for architecture x86_64:
//   "_OBJC_IVAR_$_Fruit._name", referenced from:
//       _main in main.o
// ld: symbol(s) not found for architecture x86_64

// Static Library:
// Apple

将构建后的 .framework 导入到另外的 macOS Command Line 工程中,尝试发现:当 Framework 的 Mach-O Type 为动态库(Dynamic Library)时,将出现上述错误,无法访问到 @package 修饰的 _name 成员变量;但当 Framework 的 Mach-O Type 为静态库(Static Library)时,却可以正常编译并运行。在 StackOverflow 上的一个问题中提到了 Image(镜像),@package 其实开放于同一个镜像内,可能是动态库与静态库的差别,但具体原因我仍在尝试找寻答案,也希望有理解的读者能够提供一些思路。

根据官方文档和上述实践的 Demo:

在 64 位系统,将不会导出声明为 @package 的成员变量符号,当在该类框架外使用时,将会出现链接错误。

类扩展

在 Obj-C 的类扩展(Class Extension)中,我们可以定义一些不想暴露在外界(.h)的属性、成员变量、或方法,做到「物理」隔离。

// Person-Inner.h
#import "Person.h"

@interface Person ()

- (void)doSomething;

@end

// Person.m
#import "Person.h"
#import "Person-Inner.h"

@implementation Person
- (void)doSomething {
    NSLog(@"Do something.");
}
@end

如上,对外我们只需要暴露 Person.h,而将类扩展所在的 Inner.h 不暴露为 Public Header 即可。

符号(Symbols)

nm 是 macOS 自带的命令行程序,可以用来查看 mach-o 文件的 LLVM 符号表,但默认情况将打印全部的符号,如果希望只显示外部的全局符号,可以使用 -g 参数。

// main.m
#import <Foundation/Foundation.h>

@interface Computer : NSObject  {
    int _memorySize;

    @public
    NSString *_name;

    @package
    NSString *_macAdress;

    @protected
    int _diskSize;

    @private
    int _secret;
}
@property(nonatomic, copy) NSString *arch;
@end

@implementation Computer
@end

int main(int argc, const char * argv[]) {
    return 0;
}

// nm executable-mach-o-file
// 0000000100000e00 t -[Computer .cxx_destruct]
// 0000000100000d90 t -[Computer arch]
// 0000000100000dc0 t -[Computer setArch:]
// 0000000100001270 S _OBJC_CLASS_$_Computer
//                  U _OBJC_CLASS_$_NSObject
// 0000000100001218 s _OBJC_IVAR_$_Computer._arch
// 0000000100001238 S _OBJC_IVAR_$_Computer._diskSize
// 0000000100001220 s _OBJC_IVAR_$_Computer._macAdress
// 0000000100001230 S _OBJC_IVAR_$_Computer._memorySize
// 0000000100001240 s _OBJC_IVAR_$_Computer._secret
// 0000000100001228 S _OBJC_IVAR_$_Computer._name
// 0000000100001248 S _OBJC_METACLASS_$_Computer
//                  U _OBJC_METACLASS_$_NSObject
// 0000000100000000 T __mh_execute_header
//                  U __objc_empty_cache
// 0000000100000e70 T _main
//                  U _objc_getProperty
//                  U _objc_msgSend
//                  U _objc_setProperty_nonatomic_copy
//                  U _objc_storeStrong
//                  U dyld_stub_binder

// nm -g executable-mach-o-file
// 0000000100001270 S _OBJC_CLASS_$_Computer
//                  U _OBJC_CLASS_$_NSObject
// 0000000100001238 S _OBJC_IVAR_$_Computer._diskSize
// 0000000100001230 S _OBJC_IVAR_$_Computer._memorySize
// 0000000100001228 S _OBJC_IVAR_$_Computer._name
// 0000000100001248 S _OBJC_METACLASS_$_Computer
//                  U _OBJC_METACLASS_$_NSObject
// 0000000100000000 T __mh_execute_header
//                  U __objc_empty_cache
// 0000000100000e70 T _main
//                  U _objc_getProperty
//                  U _objc_msgSend
//                  U _objc_setProperty_nonatomic_copy
//                  U _objc_storeStrong
//                  U dyld_stub_binder

在 64 位的 Obj-C 中,每个类以及实例变量的访问控制都有一个与之关联的符号,类或实例变量都会通过引用此符号来使用。类符号的格式为 _OBJC_CLASS_$_ClassName_OBJC_METACLASS_$_ClassName,成员变量符号的格式为 _OBJC_IVAR_$_ClassName.IvarName

在 C/C++ 中也有类似的符号概念。

可见程度(Visibility)

在不明确指定的默认情况下,这些符号均是暴露给外界的。但 gcc 编译器都可以通过 -fvisibility 参数设定可见程度,但优先级低于直接在源代码中声明可见程度。-fvisibility=default 即默认可见程度,-fvisibility=hidden,使得编译源文件内未明确指定的符号隐藏。

虽然看似 clang 也支持该参数,但在测试中,本机的 clang 却无法达到和 gcc 同样的效果。

建立一个 Test.cpp 的 C++ 源文件,但在文件内部不进行任何的可见程度设定,建立 main.cpp 并在主函数中调用 Test 中的方法。我们将先把 Test.cpp 编译并打包为静态库文件,再用 main.cpp 编译好的目标文件将其链接起来:

// Test.cpp
#include <stdio.h>

void test() {
    printf("Test");
}

// main.cpp
void test();

int main() {
    test();
}

// gcc:
// ➜  ~ g++ -shared -o libTest.so Test.cpp
// ➜  ~ g++ -o main main.cpp -L ./ -lTest
// ➜  ~ ./main
// Test⏎

// clang:
// ➜  ~ clang++ -c Test.cpp
// ➜  ~ ar -r libTest.a Test.o
// ar: creating archive libTest.a
// ➜  ~ clang++ -c main.cpp
// ➜  ~ clang++ main.o -L. -lTest -o main
// ➜  ~ ./main
// Test⏎

使用 nm 查看其符号表,注意:C++ 中的符号在使用时是解码过的,所以默认输出也是解码后的符号,我们可以使用 -C 参数限制其解码,-C-g 一起可以直接使用 -gC

// gcc
// ➜  ~ nm libTest.so 
// 0000000000000f70 T __Z4testv
//                  U _printf
//                  U dyld_stub_binder

// ➜  ~ nm -gC libTest.so
// 0000000000000f70 T test()
//                  U _printf
//                  U dyld_stub_binder

// ➜  ~ nm -gC main
//                  U test()
// 0000000100000000 T __mh_execute_header
// 0000000100000f80 T _main
//                  U dyld_stub_binder

保持源代码文件不更改,添加编译参数为 -fvisibility=hidden,则将出现链接错误:

// gcc:
// ➜  ~ g++ -shared -o libTest.so -fvisibility=hidden Test.cpp
// ➜  ~ g++ -o app main.cpp -L ./ -lTest
// Undefined symbols for architecture x86_64:
//   "test()", referenced from:
//       _main in main-0369ae.o
// ld: symbol(s) not found for architecture x86_64
// clang: error: linker command failed with exit code 1 (use -v to see invocation)

//➜  ~ nm -gC libTest.so
//                 U _printf
//                 U dyld_stub_binder

在 C/C++源文件中,也可以通过 __attribute((visibility("value"))) 设定某个方法或类的可见程度。尝试将 Test.cpp 的 test() 方法设置为 hidden,也将出现链接错误:

// Test.cpp
#include <stdio.h>

__attribute((visibility("hidden")))
void test() {
    printf("Test");
}

// gcc:
// ➜  ~ g++ -shared -o libTest.so Test.cpp
// ➜  ~ g++ -o app main.cpp -L ./ -lTest
// Undefined symbols for architecture x86_64:
//   "test()", referenced from:
//       _main in main-45a3c6.o
// ld: symbol(s) not found for architecture x86_64
// clang: error: linker command failed with exit code 1 (use -v to see invocation)

// ➜  ~ nm libTest.so
//                  U _printf
//                  U dyld_stub_binder
__attribute__((visibility("hidden")))
@interface ClassName : SomeSuperclass

官方文档中提到,在 Obj-C 中可以通过 __attribute__((visibility("hidden"))) 来设定类的可见程度,但关于这点我并没有实践成功,尝试将代码翻译为 C++,但似乎并有因为该语句而增加有用的信息。但在 objc-api.h 中有针对默认可见程度 __attribute__((visibility("default"))) 的宏定义,它被定义为一个更简单使用的宏 OBJC_VISIBLE(当然,该宏在 Win 32 系统中代表 __declspec(dllexport)__declspec(dllimport)),关于这点的延伸,本文暂不涉及。

// objc-api.h
#if !defined(OBJC_VISIBLE)
#   if TARGET_OS_WIN32
#       if defined(BUILDING_OBJC)
#           define OBJC_VISIBLE __declspec(dllexport)
#       else
#           define OBJC_VISIBLE __declspec(dllimport)
#       endif
#   else
#       define OBJC_VISIBLE  __attribute__((visibility("default")))
#   endif
#endif

Reference