之前做過App的啟動優化,遇到了+load優化的問題,后來想一想除了initializers代替+load還有沒有什么好的方法,然后就搜到了運用編譯屬性__attribute__優化,於是查找了很多文章,系統的整理了下__attribute__。本文大部分內容來自引用的文章,如果想看更多更詳細內容可以查看引用文章。
__attribute__ 介紹
__attribute__是一個編譯屬性,用於向編譯器描述特殊的標識、錯誤檢查或高級優化。它是GNU C特色之一,系統中有許多地方使用到。 __attribute__可以設置函數屬性(Function Attribute )、變量屬性(Variable Attribute )和類型屬性(Type Attribute)等。
__attribute__ 格式
1 2 |
__attribute__ ((attribute-list)) |
__attribute__ 常用的編譯屬性及簡單應用
format
這個屬性指定一個函數比如printf,scanf作為參數,這使編譯器能夠根據代碼中提供的參數檢查格式字符串。對於追蹤難以發現的錯誤非常有幫助。
format參數的使用如下:
1 |
format (archetype, string-index, first-to-check) |
第一參數需要傳遞archetype指定是哪種風格,這里是 NSString;string-index指定傳入函數的第幾個參數是格式化字符串;first-to-check指定第一個可變參數所在的索引.
C中的使用方法
1 2 |
extern int my_printf (void *my_object, const char *my_format, ...) __attribute__((format(printf, 2, 3))); |
在Objective-C 中通過使用__NSString__格式達到同樣的效果,就像在NSString +stringWithFormat:和NSLog()里使用字符串格式一樣
1 2 3 |
FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2); + (instancetype)stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); |
__attribute__((constructor))
確保此函數在 在main函數被調用之前調用,iOS中在+load之后main之前執行。 constructor和destructor會在ELF文件中添加兩個段-.ctors和.dtors。當動態庫或程序在加載時,會檢查是否存在這兩個段,如果存在執行對應的代碼。
1 2 3 4 5 |
__attribute__((constructor))
static void beforeMain(void) {
NSLog(@"beforeMain");
}
|
1 2 |
__attribute__((constructor(101))) // 里面的數字越小優先級越高,1 ~ 100 為系統保留 |
__attribute__((destructor))
1 2 3 4 |
__attribute__((destructor))
static void afterMain(void) {
NSLog(@"afterMain");
}
|
確保此函數在 在main函數被調用之后調
__attribute__((cleanup))
用於修飾一個變量,在它的作用域結束時可以自動執行一個指定的方法
關於這個Sunny在黑魔法__attribute__((cleanup))中講的很好很細,建議看看。
iOS中的應用
既然__attribute__((cleanup(...)))可以用來修飾變量,所以也可以用來修飾block
1 2 3 4 5 |
// void(^block)(void)的指針是void(^*block)(void)
static void blockCleanUp(__strong void(^*block)(void)) {
(*block)();
}
|
這里不得不提萬能的Reactive Cocoa中神奇的@onExit方法,其實正是上面的寫法,簡單定義個宏:
1 2 3 |
#define onExit\
__strong void(^block)(void) __attribute__((cleanup(blockCleanUp), unused)) = ^
|
這樣的寫法可以將成對出現的代碼寫在一起,比如說一個lock,用了onExit之后,代碼更集中了:
1 2 3 |
NSRecursiveLock *aLock = [[NSRecursiveLock alloc] init];
[aLock lock]; onExit { [aLock unlock]; };
|
當我看到這段代碼的時候第一個想到就是Swift中defer關鍵字
1 |
lock.lock(); defer { lock.unlock() }
|
used
used的作用是告訴編譯器,我聲明的這個符號是需要保留的。被used修飾以后,意味着即使函數沒有被引用,在Release下也不會被優化。如果不加這個修飾,那么Release環境鏈接器會去掉沒有被引用的段。gun的官方文檔
This attribute, attached to a variable with static storage, means that the variable must be emitted even if it appears that the variable is not referenced.
When applied to a static data member of a C++ class template, the attribute also means that the member is instantiated if the class itself is instantiated.
iOS中的運用,BeeHive中的一段代碼。
1 |
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
|
nonnull
這個屬性指定函數的的某些參數不能是空指針
1 2 3 |
extern void * my_memcpy (void *dest, const void *src, size_t len) __attribute__((nonnull (1, 2))); |
iOS中的應用
1 2 3 4 5 6 7 8 |
- (int)addNum1:(int *)num1 num2:(int *)num2 __attribute__((nonnull (1,2))){//1,2表示第一個和第二個參數不能為空
return *num1 + *num2;
}
- (NSString *)getHost:(NSURL *)url __attribute__((nonnull (1))){//第一個參數不能為空
return url.host;
}
|
objc_runtime_name
用於 @interface 或 @protocol,將類或協議的名字在編譯時指定成另一個
1 |
__attribute__((objc_runtime_name("<#OtherClassName#>")))
|
iOS中的應用
1 2 3 4 5 6 |
__attribute__((objc_runtime_name("OtherTest")))
@interface Test : NSObject
@end
NSLog(@"%@", NSStringFromClass([Test class])); // "OtherTest"
|
這個屬性可以用來做代碼混淆
noreturn
幾個標注庫函數,例如abort exit,沒有返回值。GCC能夠自動識別這種情況。noreturn屬性指定像這樣的任何不需要返回值的函數。當遇到類似函數還未運行到return語句就需要退出來的情況,該屬性可以避免出現錯誤信息。
iOS中的運用
AFNetworking庫為它的網絡請求顯示入口函數使用了該屬性。這個在生成一個專用的線程時使用,保證分離的線程能在應用的整個生命周期繼續執行
1 2 3 4 5 6 7 |
+ (void) __attribute__((noreturn)) networkRequestThreadEntryPoint:(id)__unused object {
do {
@autoreleasepool {
[[NSRunLoop currentRunLoop] run];
}
} while (YES);
}
|
noinline & always_inline
內聯函數:內聯函數從源代碼層看,有函數的結構,而在編譯后,卻不具備函數的性質。內聯函數不是在調用時發生控制轉移,而是在編譯時將函數體嵌入在每一個調用處。編譯時,類似宏替換,使用函數體替換調用處的函數名。一般在代碼中用inline修飾,但是能否形成內聯函數,需要看編譯器對該函數定義的具體處理
noinline不內聯always_inline總是內聯- 這兩個都是用在函數上
內聯的本質是用代碼塊直接替換掉函數調用處,好處是:快代碼的執行,減少系統開銷.適用場景:
- 這個函數更小
- 這個函數不被經常調用
1 |
void test(int a) __attribute__((always_inline)); |
這個在Swift有類似用法
1 2 3 4 5 6 7 8 9 10 11 12 |
extension NSLock { @inline(__always) func executeWithLock(_ block: () -> Void) { lock() block() unlock() } } |
warn_unused_result
當函數或者方法的返回值很重要時,要求調用者必須檢查或者使用返回值,否則編譯器會發出警告提示。
1 2 3 4 5 6 |
- (BOOL)availiable __attribute__((warn_unused_result))
{
return 10;
}
|
在Swift中應該是幾乎所有方法都是warn_unused_result,可以通過@discardableResult去掉警告提示。
Clang特有的
就像GCC的許多特性一樣,Clang支持__attribute__,而且添加了一些自己的小擴展。為了檢查一個特殊屬性的可用性,你可以使用__has_attribute指令。
availability
Clang引入了可用性屬性,這個屬性可以在聲明中描述跟系統版本有關的生命周期。例如:
官方例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (CGSize)sizeWithFont:(UIFont *)font NS_DEPRECATED_IOS(2_0, 7_0, "Use -sizeWithAttributes:") __TVOS_PROHIBITED; //來看一下 后邊的宏 #define NS_DEPRECATED_IOS(_iosIntro, _iosDep, ...) CF_DEPRECATED_IOS(_iosIntro, _iosDep, __VA_ARGS__) define CF_DEPRECATED_IOS(_iosIntro, _iosDep, ...) __attribute__((availability(ios,introduced=_iosIntro,deprecated=_iosDep,message="" __VA_ARGS__))) //宏展開以后如下 __attribute__((availability(ios,introduced=2_0,deprecated=7_0,message=""__VA_ARGS__))); //ios即是iOS平台 //introduced 從哪個版本開始使用 //deprecated 從哪個版本開始棄用 //message 警告的消息 |
introduced: 聲明被引入的第一個版本信息。deprecated: 第一次不建議使用的版本,意味着使用者應該移除這個方法的使用。obsoleted: 第一次被廢棄的版本,意味着已經被移除,不能夠使用了。unavailable: 意味着這個平台不支持使用。message: 當Clang發出一些關於廢棄或不建議使用的警告時的文本。用於引導使用者不要使用改接口了。
支持的平台有:
- ios: 蘋果的iOS操作系統。最小部署目標平台版本是通過
-mios-version-min=*version*或-miphoneos-version-min=*version*命令行指定的。 - macosx: 蘋果的OS X操作系統。最小部署目標平台版本是通過
-mmacosx-version-min=*version*命令行指定的。
1 2 3 4 5 6 7 8 9 |
//如果經常用,建議定義成類似系統的宏
- (void)oldMethod:(NSString *)string __attribute__((availability(ios,introduced=2_0,deprecated=7_0,message="用 -newMethod: 這個方法替代 "))){
NSLog(@"我是舊方法,不要調我");
}
- (void)newMethod:(NSString *)string{
NSLog(@"我是新方法");
}
|
在swift中也有類似的用法
1 2 |
@available(iOS 6.0, *)
public var minimumScaleFactor: CGFloat // default is 0.0
|
unavailable
告訴編譯器該方法不可用,如果強行調用編譯器會提示錯誤。比如某個類在構造的時候不想直接通過init來初始化,只能通過特定的初始化方法()比如單例,就可以將init方法標記為unavailable。
1 2 3 4 |
//系統的宏,可以直接拿來用 #define UNAVAILABLE_ATTRIBUTE __attribute__((unavailable)) #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE |
1 2 3 4 5 6 7 8 9 10 11 12 |
@interface Person : NSObject @property(nonatomic,copy) NSString *name; @property(nonatomic,assign) NSUInteger age; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age; @end |
實際上unavailable后面可以跟參數,顯示一些信息,如:
1 2 3 |
//系統的
#define NS_AUTOMATED_REFCOUNT_UNAVAILABLE __attribute__((unavailable("not available in automatic reference counting mode")))
|
overloadable
Clang在C中提供對C++標准函數重載的支持。函數重載在C中是通過overloadable屬性引入的。例如:你可以重載tgsin函數,寫出sin函數在入參不同時的不同版本。用於c語言函數,可以定義若干個函數名相同,但參數不同的方法,調用時編譯器會自動根據參數選擇函數原型。
1 2 3 4 5 6 7 8 |
__attribute__((overloadable)) void print(NSString *string){
NSLog(@"%@",string);
}
__attribute__((overloadable)) void print(int num){
NSLog(@"%d",num);
}
|
__attribute__ 在iOS開發中的復雜應用
說了這么多重點來了,那么這些屬性在iOS上有哪些奇妙的運用呢?有些比較簡單的運用在介紹屬性的時候就說了,這里主要講一些比較復雜的運用。
Swift沒有+load方法的替代帶方案
1 2 3 4 5 6 7 8 9 10 11 12 |
static void __attribute__ ((constructor)) Initer() {
Class class = NSClassFromString(@"AnnotationDemo.MyInitThingy");
SEL selector = NSSelectorFromString(@"appWillLaunch:");
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:class
selector:selector
name:UIApplicationDidFinishLaunchingNotification
object:nil];
}
|
1 2 3 4 5 6 |
class MyInitThingy: NSObject {
@objc static func appWillLaunch(_: Notification) {
print("App Will Launch")
}
}
|
BeeHive模塊注冊
模塊注冊有三種方式:Annotation方式注冊、讀取本地plist方式注冊、Load方法注冊。
首先把數據放在可執行文件的自定義數據段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 通過BeeHiveMod宏進行Annotation標記
#ifndef BeehiveModSectName
#define BeehiveModSectName "BeehiveMods"
#endif
#ifndef BeehiveServiceSectName
#define BeehiveServiceSectName "BeehiveServices"
#endif
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
// 這里我們就把數據存在data數據段里面的"BeehiveMods"段中
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
@interface BHAnnotation : NSObject
@end
|
從Mach-O section中讀取數據
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
SArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp);
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
{
NSArray *mods = BHReadConfiguration(BeehiveModSectName, mhp);
for (NSString *modName in mods) {
Class cls;
if (modName) {
cls = NSClassFromString(modName);
if (cls) {
[[BHModuleManager sharedManager] registerDynamicModule:cls];
}
}
}
//register services
NSArray<NSString *> *services = BHReadConfiguration(BeehiveServiceSectName,mhp);
for (NSString *map in services) {
NSData *jsonData = [map dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (!error) {
if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {
NSString *protocol = [json allKeys][0];
NSString *clsName = [json allValues][0];
if (protocol && clsName) {
[[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
}
}
}
}
}
__attribute__((constructor))
void initProphet() {
_dyld_register_func_for_add_image(dyld_callback);
}
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{
NSMutableArray *configs = [NSMutableArray array];
unsigned long size = 0;
#ifndef __LP64__
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
#else
const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif
unsigned long counter = size/sizeof(void*);
for(int idx = 0; idx < counter; ++idx){
char *string = (char*)memory[idx];
NSString *str = [NSString stringWithUTF8String:string];
if(!str)continue;
BHLog(@"config = %@", str);
if(str) [configs addObject:str];
}
return configs;
}
@implementation BHAnnotation
@end
|
__attribute__((constructor))就是保證在main之前讀取所有注冊信息。
使用
1 2 3 4 5 |
@BeeHiveMod(ShopModule) @interface ShopModule() <BHModuleProtocol> @end @implementation ShopModule |
延遲 premain code
把+load等main函數之前的代碼移植到了main函數之后。是探一種延遲 premain code 的方法這篇文章提出的,我還沒有嘗試過。
原理是把函數地址放到QWLoadable段中,然后主程序在啟動時獲取QWLoadable的內容,並逐個調用。
庫的地址 LoadableMacro
作者測試下來,100個函數地址的讀取,在iPhone5的設備上讀取不到1ms。新增了這不到1ms的耗時(這1ms也是可審計的),帶來了所有啟動階段行為的可審計,以及最重要的Patch能力。
msgSend observe
這個來自於質量監控-卡頓檢測 這篇文章 OC方法的調用最終轉換成msgSend的調用執行,通過在函數前后插入自定義的函數調用,維護一個函數棧結構可以獲取每一個OC方法的調用耗時,以此進行性能分析與優化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#define save() \
__asm volatile ( \
"stp x8, x9, [sp, #-16]!\n" \
"stp x6, x7, [sp, #-16]!\n" \
"stp x4, x5, [sp, #-16]!\n" \
"stp x2, x3, [sp, #-16]!\n" \
"stp x0, x1, [sp, #-16]!\n");
#define resume() \
__asm volatile ( \
"ldp x0, x1, [sp], #16\n" \
"ldp x2, x3, [sp], #16\n" \
"ldp x4, x5, [sp], #16\n" \
"ldp x6, x7, [sp], #16\n" \
"ldp x8, x9, [sp], #16\n" );
#define call(b, value) \
__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
__asm volatile ("mov x12, %0\n" :: "r"(value)); \
__asm volatile ("ldp x8, x9, [sp], #16\n"); \
__asm volatile (#b " x12\n");
__attribute__((__naked__)) static void hook_Objc_msgSend() {
save()
__asm volatile ("mov x2, lr\n");
__asm volatile ("mov x3, x4\n");
call(blr, &push_msgSend)
resume()
call(blr, orig_objc_msgSend)
save()
call(blr, &pop_msgSend)
__asm volatile ("mov lr, x0\n");
resume()
__asm volatile ("ret\n");
}
|
everettjf 同樣封裝了一個庫FishhookObjcMsgSend
這里是另一個實現方案 objc_msgSend
參考文章
探索 facebook iOS 客戶端 - section FBInjectable
探一種延遲 premain code 的方法 探一種延遲 premain code 的方法
__attribute__
Specifying Attributes of Variables
gnu-c-attributes
BeeHive —— 一個優雅但還在完善中的解耦框架
Declaring Attributes of Functions
__attribute__ 總結
OC中的 __attribute__
Clang 拾遺之objc_designated_initializer
Macro
Clang Attributes 黑魔法小記
黑魔法__attribute__((cleanup))
質量監控-卡頓檢測
https://woshiccm.github.io/posts/__attribute__詳解及應用/
