Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
kingcos opened this Issue Mar 30, 2019 · 0 comments

Comments

Projects
None yet
1 participant
@kingcos
Copy link
Owner

kingcos commented Mar 30, 2019

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:

  • 在 32 位系统中,@package 等同于 @public
  • 在 64 位系统中,且在定义该类的 Framework 中,为 @public
  • 在 64 位系统中,但不在定义该类的 Framework 中,为 @private

在 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

@kingcos kingcos added the [Practice] label Mar 30, 2019

@kingcos kingcos self-assigned this Mar 30, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.