支持特定架构的框架实现

支持特定架构的框架实现

前言

有时候,我们在给第三方移动端提供工具库的时候,会出现不兼容x86的情况。

  • 比如:我们引用的一个框架不包含x86_64架构,导致我们的库无法编译出x86架构版本
  • 比如:框架本身就是为相机或AR等特定功能而生,甚至剔除x86_64架构代码可以节省库的大小

这些情况下,我们能只编译arm64,放弃其它架构吗?答案当然是NO。

直接放弃32位arm和x86架构的支持,非常直接的,会带来两个负面效果。

  1. 如果应用有上千万的用户,其中很小的比例就可能几十万用户,放弃支持等于放弃这部分用户。(iPhone 5S 年代CPU的用户)
  2. 开发者原本使用模拟器进行设备兼容性调试和开发,接入仅64位的库后,工程无法在模拟器继续编译,给调试和屏幕大小适配带来困扰

无论是1还是2,都是无法接受的结果。我们需要找到一种方案,在支持支持arm64的情况下缩减体积。

思路

我们要解决的核心问题有两个

  1. 解决第三方代码不支持指定架构导致无法编译对应架构的问题
  2. 在支持非目标时候,我们编译了很多无用的代码。

解决思路也很简单

  1. 使用 TargetConditionals.h 的架构定义来区分架构,在不支持的架构中省去这部分功能和代码
  2. 非目标架构情况,使用 TargetConditionals.h 屏蔽所有代码
  3. 掩盖掉所有内部修饰痕迹

栗子

比如我们有一个 OnlyARM.framework 我们要做到

  • arm64:功能正常
  • armv7 / x86_64:功能无法使用,但是不会导致工程编译问题

我们核心 OnlyARMCore ,可能是这样子的:

OnlyARMCore.h

1
2
3
4
5
6
7
8
9
10
11
12

#import <Foundation/Foundation.h>

@interface OnlyARMCore : NSObject

+ (instancetype)shared;

- (void)saySomething;

- (void)sayHi;

@end

OnlyARMCore.m

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

#import "OnlyARMCore.h"
#import "OnlyEvil64.h"

@implementation OnlyARMCore

+ (instancetype)shared
{
    static OnlyARMCore *core;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        core = [OnlyARMCore new];
    });
    return core;
}

- (void)saySomething
{
    NSLog(@"Hello~");
}

- (void)sayHi
{
    [OnlyEvil64 evil64];
}

@end

其中,OnlyEvil64 是邪恶的第三方框架,仅仅支持64位架构,这时候,如果我们想要编译支持所有架构的自己框架,就会得到如下结果

1
2
3
4
5
Undefined symbols for architecture arm64:
  "_OBJC_CLASS_$_OnlyEvil64", referenced from:
      objc-class-ref in OnlyARMCore.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

于是要在非arm64架构中干掉 OnlyEvil64

1
2
3
4
#if (TARGET_CPU_ARM64)
    [OnlyEvil64 evil64];
#else
#endif

但是,回到主题,如果我们的功能,原本就仅仅支持64位设计呢?我们其实可以直接区分两个OnlyARMCore;一个OnlyARMCore for arm64,一个for others。那我们可以这么写:

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
//
//  OnlyARMCore.m
//  OnlyARM
//
//  Created by Dikey on 2021/8/12.
//

#if (TARGET_CPU_ARM64)

#import "OnlyARMCore.h"
#import "OnlyEvil64.h"

@implementation OnlyARMCore

+ (instancetype)shared
{
    static OnlyARMCore *core;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        core = [OnlyARMCore new];
    });
    return core;
}

- (void)saySomething
{
    NSLog(@"Hello~");
    
}

- (void)sayHi
{
    [OnlyEvil64 evil64];
}

@end

#else

#import "OnlyARMCore.h"

@implementation OnlyARMCore

+ (instancetype)shared
{
    static OnlyARMCore *core;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        core = [OnlyARMCore new];
    });
    return core;
}

- (void)saySomething
{
    // 留空
}

- (void)sayHi
{
    // 留空
}

@end

#endif

到这里,我们又发现了新的问题,如果说我们的这个类,有200个方法,我们会需要重新实现200次像是 - (void)sayHi 这样的空函数,这既重复又容易出错。

有没有一劳永逸解决的方法呢?我们看了看 @interface OnlyARMCore : NSObject 中的NSObject,想起来了古老的消息转发。

很简单了,只要在 #if (TARGET_CPU_ARM64) #else 和 #endif之间处理掉所有非arm64架构的方法就可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implementation OnlyARMCore

+ (BOOL)resolveInstanceMethod:(SEL)name
{
    class_addMethod([self class], name, (IMP)OnlyARMCore_dynamicMethodIMP, "v@:");
    return [super resolveInstanceMethod:name];
}

+ (BOOL)resolveClassMethod:(SEL)name
{
    Class class = object_getClass([self class]);
    class_addMethod(class, name, (IMP)OnlyARMCore_dynamicMethodIMP, "@:@");
    return [super resolveClassMethod:name];
}

@end

然后,为了最小化痕迹,我们可以把这部分代码抽象到catagory中。

OnlyARMCore+OnlyARMCore_DynamicMethod.h

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
//
//  OnlyARMCore+OnlyARMCore_DynamicMethod.h
//  OnlyARM
//
//  Created by Dikey on 2021/8/12.
//

#if (TARGET_CPU_ARM64)
#else

#import "OnlyARMCore.h"

@implementation OnlyARMCore

+ (instancetype)shared
{
    static OnlyARMCore *core;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        core = [OnlyARMCore new];
    });
    return core;
}

@end

@interface OnlyARMCore (OnlyARMCore_DynamicMethod)
@end

#endif

OnlyARMCore+OnlyARMCore_DynamicMethod.m 中

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
//
//  OnlyARMCore+OnlyARMCore_DynamicMethod.m
//  OnlyARM
//
//  Created by Dikey on 2021/8/12.
//

#if (TARGET_CPU_ARM64)

#else

#import "OnlyARMCore+OnlyARMCore_DynamicMethod.h"
#include <objc/runtime.h>

void OnlyARMCore_dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"OnlyARMCore >> 不支持非arm64以外的机型调用");
}

@implementation OnlyARMCore (OnlyARMCore_DynamicMethod)

+ (BOOL)resolveInstanceMethod:(SEL)name
{
    class_addMethod([self class], name, (IMP)OnlyARMCore_dynamicMethodIMP, "v@:");
    return [super resolveInstanceMethod:name];
}

+ (BOOL)resolveClassMethod:(SEL)name
{
    Class class = object_getClass([self class]);
    class_addMethod(class, name, (IMP)OnlyARMCore_dynamicMethodIMP, "@:@");
    return [super resolveClassMethod:name];
}

@end

#endif

但是这时候,我们又有疑问了

1
2
3
Method definition for 'sayHi' not found

OnlyARMDemo/OnlyARM/OnlyARMCore+OnlyARMCore_DynamicMethod.h:17:17: Method definition for 'saySomething' not found

因为编译器在对外的.h 文件,我们是没有区分架构的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//
//  OnlyARMCore.h
//  OnlyARM
//
//  Created by Dikey on 2021/8/12.
//

#import <Foundation/Foundation.h>

@interface OnlyARMCore : NSObject

+ (instancetype)shared;

- (void)saySomething;

- (void)sayHi;

@end

在外面看来(或从编译器角度看),这时候是同时编译了64位和其它架构的。但是实际上在.m中,非64位架构,实际上想 saySomething 或者 sayHi 时候,实例方法和类方法分别是转发给了转发给了

1
+ (BOOL)resolveInstanceMethod:(SEL)name

1
+ (BOOL)resolveClassMethod:(SEL)name

在以上方法中,我们最终都将消息指向了 OnlyARMCore_dynamicMethodIMP ,来告诉外部开发者,我们的功能只在特定架构生效。

这时候,我们需要告诉编译器,这部分检查可以略过

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
//
//  OnlyARMCore+OnlyARMCore_DynamicMethod.h
//  OnlyARM
//
//  Created by Dikey on 2021/8/12.
//

#if (TARGET_CPU_ARM64)
#else

#import "OnlyARMCore.h"

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Weverything"

@implementation OnlyARMCore

+ (instancetype)shared
{
    static OnlyARMCore *core;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        core = [OnlyARMCore new];
    });
    return core;
}

@end

@interface OnlyARMCore (OnlyARMCore_DynamicMethod)
@end

#pragma clang diagnostic pop

#endif

我们忽略掉了所有push 和 pop 之间的warning

1
2
3
4
5
6
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Weverything"

// warnings ignored 

#pragma clang diagnostic pop

思考

  • Objective-C的动态机制,使得很多操作变成可能;但是要记住,更多的可能性意味着可能更难debug;好的代码不应该丢掉可维护性和可读性。
  • Swift 笔者还未尝试,理论上也是可以通过@objc使用Objective-C的动态特性,但是Swift毕竟本身是静态类型的语言,Swift应该会有更适合的方式吧。
  • 目前认知:可能还是支持x86架构会更好,在代码中通过宏定义来屏蔽x86的实现;以避免对第三方工程的入侵。
  • 至于32位,最近的移动设备是 iPhone 5;已经随着时间的长河成为了历史,已不太有支持的必要。

Demo

GitHub - DikeyKing/OnlyARMDemo: A Example of use arm64 only framework