1.什么是注解器
注解源自于java,可以理解为给代码打标记。我们可以给类定义打标签,也可以给方法,属性等打标签,被标签过的目标将按照我们所打的标签进行校验!如一个方法参数个数校验,参数类型校验!注解器,是与业务完全解耦的,完全删除注解也不会影响业务代码,当然类似于静态代码走查,注解也可以自定义。参考:https://blog.csdn.net/briblue/article/details/73824058
2.思考一个这样的问题
如果我需要给每一个UIViewController类,在不改类内部实现的情况下,标记一个ID,并且在该UIViewController类里面还能获取到该ID,我们会怎么做,在JAVA里面就比较方便了,在其紧挨着的类实现的地方加上一个组件标签[类的外面与类无关]。JAVA编译器会自动进行该标签所含有的规则校验。而objective-c中很遗憾,并没有提供注解相关实现,如我们再类外面加一段代码势必编译不过,只能想办法能不能加MARK而不影响编译。如下不能做到这样呢?
#pragma annotation(type:"default",param1:"value1",param2:"value2")
@interface UIApplicationDelegate()
3.iOS中的注解器
在objective-c中并没有注解的概念,那么我们可以尝试自己实现一套?确实可以,业内也实现了类似的方案,但是都不完美。
要模拟注解的过程,需要解决,1.不影响以前有的业务。 2.在被注解的源代码实现里面能方便的获取注解内容,可以理解为被注解的代码,在编译期间能自动生成一段代码在被注解类里面,或者我们需要建立一个“被注解者”与“注解代码”的对应关系。
目前业内的实现有两个方案,方案1,基于正则匹配,然后生成对应框架代码,加上自己OC实现的自定义规则,配合扫描结果关系表,来模拟注解的过程;方案2,居于类似FB的编译可配置来模拟的,用到__attribute((used, section("__DATA,"#sectname" "))),后面细聊;方案3,LLVM代码插桩;
3.1. 阿里的OCAnnotation
仓库地址:https://github.com/alibaba/OCAnnotation
是以方案1实现的,工程包括看一套ruby脚本,需要让其嵌入到我们的目标工程的build script里面。在我们编译期间将执行该脚本,该脚本将会扫码我们所有的源代码,并按规则生成对应的模板OC文件,该文件为一个配置对应关系。可理解为一个hashmap字典,以类注解为例 :
第一步:使用OCAnnotation Ruby工具条件,把编译期间要执行的扫码脚本集成到我们的目标工程。
第二步:注册
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
OCAAnnotationManager *annotationManager = [OCAAnnotationManager sharedManager];
// custom annotation type registration (optional)
[annotationManager registerAnnotationType:@"RemoteLog"
position:OCAAnnotationPositionMethod
class:[AFWRemoteLogMethodAnnotation class]];
// annotation setup
NSDictionary *configs = kAFWAnnotationConfigs; // using the macro name you write in the Annotation/.config file
[[OCAAnnotationManager sharedManager] addConfigsWithConfigDic:configs];
}
需要注意的是AFWRemoteLogMethodAnnotation就是我们自定义的规则,配合我们的mark标记来实现对应的校验的类。
第三步:标记具体需要的类
#pragma annotation(type:"default",param1:"value1",param2:"value2")
@interface UIApplicationDelegate()
第四步:编译
编译后将产生一个类名对应一个ID的枚举头文件,这个头文件是通过ruby脚本扫码所有文件,如果有第二步的标记就加入到该枚举头文件。
第五步:加入该头文件到工程
总结:说白了就是,通过在编译期间,调用正则匹配脚本,扫码并获取注解与目标对象之间的关系(类,方法,属性)。并且把这个对应关系保存到一个字典里面去,这个字典以头文件,是ruby脚本扫码结束后自动创建的OC文件。当我们把这OC文件导入进去目标工程,在启动后马上加载进入内存,作为全局可访问数据,后我们就可以使用该全局数据【配置表】和 我们自己定义的规则,来达到运行期间的注解校验。当然该工程有很大缺陷是,每次编译都要扫码源代码,虽然作者做了缓存,还有就是不支持framework.在组件化遍地开花的今天,这也很尴尬!
备注:为了解决“被注解者”与“注解代码”的桥梁问题,还有一种办法是生成注解对象的类别,如当前目标是被注解者,那么久生成改其类别扩展,并导入工程预编译中。这样的“类代码插庄”,也能间接的让我们获取到类外的注解内容。当然这个也一样存在编译期间扫描代码的问题,而且如果注解多,还会增加代码量。
3.来源于FB编译期配置
目前beehive组件化框架也使用了此类方案[https://github.com/alibaba/BeeHive]
简要实现:
#define KGAppModuleDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define KGAppModule(name) \
class NSObject;char * k##name##_mod KGAppModuleDATA(KGAppMods) = ""#name"";
NSArray<NSString *>* KGReadConfiguration(char *sectionName,const struct mach_header *mhp);
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
{
NSArray *mods = KGReadConfiguration(KGModSectName, mhp);
for (NSString *modName in mods) {
Class cls;
if (modName) {
cls = NSClassFromString(modName);
if (cls) {
[KGAppModuleLifeCycle kgRegisterModuleClass:cls config:@{
} priority:1];
}
}
}
}
//注册main之前的析构函数,析构函数仅爱周注解才能生效
//__attribute__((constructor))
//void initProphet() {
// //动态链接库加载的时候的hook,可能会回调次数比较多,可能不建议
// _dyld_register_func_for_add_image(dyld_callback);
//}
NSArray<NSString *>* KGReadConfiguration(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;
if(str) [configs addObject:str];
}
return configs;
}
使用,在implementation之前实现一个类似的注解,就能在启动过程像load函数一样加载指定函数。
@KGAppModule(KGWatchMoudleLifecyleMounter)
@implementation KGWatchMoudleLifecyleMounter
//启动过程中通过注解调用的
- (instancetype)initKGAppMoudleWithConfiguration:(NSDictionary *)configuration
{
if (self = [self init]) {
}
return self;
}
4.iOS注解器的模拟
5.总结
iOS客户端无完美方案,如果要做到更完美需要clang支持。我们上面方案模拟出来的都有一定的性能损耗!