// 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
// 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
Preface
Obj-C 中的成员变量,即 Instance Variables,通常称为 ivar。在面向对象的概念中,一个类的对外暴露决定了其所提供的能力,对子类则提供扩展性,但有些时候我们也不希望外界甚至子类不知道一些细节,这时就用到了访问控制(Access Control)。在 C++、Java、Swift 等大多数高级语言中都有这样的概念,Obj-C 中总共有四种等级,这次就来简单谈谈 Obj-C 中成员变量的访问控制。
访问控制修饰符
@public
声明为
@public
的成员变量是访问控制中开放范围最大的,其允许外界可以直接访问到(当然,前提是引入包含该类的头文件)。@protected
声明为
@protected
的成员变量只能在本类或子类中使用。注意,当不使用任何访问控制修饰符时,成员变量默认即为@protected
。其实,声明为@property
的属性,系统会自动为我们创建一个_ivar
的成员变量,这个成员变量的可见程度默认也是@protected
。@private
声明为
@private
的成员变量是访问控制中开放范围最小的,只能被本类访问到。@package
建立一个 Cocoa Framework 的工程,导入以下代码:
将构建后的
.framework
导入到另外的 macOS Command Line 工程中,尝试发现:当 Framework 的 Mach-O Type 为动态库(Dynamic Library)时,将出现上述错误,无法访问到@package
修饰的_name
成员变量;但当 Framework 的 Mach-O Type 为静态库(Static Library)时,却可以正常编译并运行。在 StackOverflow 上的一个问题中提到了 Image(镜像),@package
其实开放于同一个镜像内,可能是动态库与静态库的差别,但具体原因我仍在尝试找寻答案,也希望有理解的读者能够提供一些思路。根据官方文档和上述实践的 Demo:
@package
等同于@public
@public
@private
在 64 位系统,将不会导出声明为
@package
的成员变量符号,当在该类框架外使用时,将会出现链接错误。类扩展
在 Obj-C 的类扩展(Class Extension)中,我们可以定义一些不想暴露在外界(.h)的属性、成员变量、或方法,做到「物理」隔离。
如上,对外我们只需要暴露 Person.h,而将类扩展所在的 Inner.h 不暴露为 Public Header 即可。
符号(Symbols)
nm
是 macOS 自带的命令行程序,可以用来查看 mach-o 文件的 LLVM 符号表,但默认情况将打印全部的符号,如果希望只显示外部的全局符号,可以使用-g
参数。在 64 位的 Obj-C 中,每个类以及实例变量的访问控制都有一个与之关联的符号,类或实例变量都会通过引用此符号来使用。类符号的格式为
_OBJC_CLASS_$_ClassName
和_OBJC_METACLASS_$_ClassName
,成员变量符号的格式为_OBJC_IVAR_$_ClassName.IvarName
。在 C/C++ 中也有类似的符号概念。
可见程度(Visibility)
在不明确指定的默认情况下,这些符号均是暴露给外界的。但 gcc 编译器都可以通过
-fvisibility
参数设定可见程度,但优先级低于直接在源代码中声明可见程度。-fvisibility=default
即默认可见程度,-fvisibility=hidden
,使得编译源文件内未明确指定的符号隐藏。建立一个 Test.cpp 的 C++ 源文件,但在文件内部不进行任何的可见程度设定,建立 main.cpp 并在主函数中调用 Test 中的方法。我们将先把 Test.cpp 编译并打包为静态库文件,再用 main.cpp 编译好的目标文件将其链接起来:
使用
nm
查看其符号表,注意:C++ 中的符号在使用时是解码过的,所以默认输出也是解码后的符号,我们可以使用-C
参数限制其解码,-C
与-g
一起可以直接使用-gC
。保持源代码文件不更改,添加编译参数为
-fvisibility=hidden
,则将出现链接错误:在 C/C++源文件中,也可以通过
__attribute((visibility("value")))
设定某个方法或类的可见程度。尝试将 Test.cpp 的test()
方法设置为hidden
,也将出现链接错误:官方文档中提到,在 Obj-C 中可以通过
__attribute__((visibility("hidden")))
来设定类的可见程度,但关于这点我并没有实践成功,尝试将代码翻译为 C++,但似乎并有因为该语句而增加有用的信息。但在 objc-api.h 中有针对默认可见程度__attribute__((visibility("default")))
的宏定义,它被定义为一个更简单使用的宏OBJC_VISIBLE
(当然,该宏在 Win 32 系统中代表__declspec(dllexport)
或__declspec(dllimport)
),关于这点的延伸,本文暂不涉及。Reference
__attribute__((visibility("")))
- veryitman