__attribute__詳解及應用


之前做過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之前執行。 constructordestructor會在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__詳解及應用/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM