热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

自定义iOS注解

1.项目背景注解源自于java,是在JDK5时引入的新特性,注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的

1. 项目背景

注解源自于java,是在 JDK5 时引入的新特性,注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。注解类型定义指定了一种新的类型,一种特殊的接口类型。 在关键词 interface 前加 @ 符号也就是用 @interface 来区分注解的定义和普通的接口声明。

注解的好处:

I.减少重复代码的书写,相同逻辑统一处理,降低出错率

II.复杂逻辑清晰化

III.降低代码耦合

但是在iOS中并没有注解的概念,鉴于注解的这些好处,就有了自定义iOS注解的想法。

2. 注解方案的实现思路

要模拟注解的过程,需要解决:

1. 不影响以前有的业务。

2. 在被注解的源代码实现里面能方便的获取注解内容,可以理解为被注解的代码,在编译期间能自动生成一段代码在被注解类里面,或者我们需要建立一个“被注解者”与“注解代码”的对应关系。

2.1 方案1

基于正则匹配,然后生成对应框架代码,加上自己OC实现的自定义规则,配合扫描结果关系表,来模拟注解的过程;

阿里的OCAnnotation(仓库地址:GitHub - alibaba/OCAnnotation: A light-weighted framework empowering Objective-C with annotation feature.)就是以方这个方案实现的,工程包括看一套ruby脚本,需要让其嵌入到我们的目标工程的build script里面。在我们编译期间将执行该脚本,该脚本将会扫码我们所有的源代码,并按规则生成对应的模板OC文件,该文件为一个配置对应关系,可理解为一个hashmap字典。

说白了就是,通过在编译期间,调用正则匹配脚本,扫码并获取注解与目标对象之间的关系(类,方法,属性)。并且把这个对应关系保存到一个字典里面去,这个字典以头文件,是ruby脚本扫码结束后自动创建的OC文件。当我们把这OC文件导入进去目标工程,在启动后马上加载进入内存,作为全局可访问数据,然后我们就可以使用该全局数据【配置表】和 我们自己定义的规则,来达到运行期间的注解校验。当然该工程有很大缺陷是,每次编译都要扫码源代码,虽然作者做了缓存,还有就是不支持framework.在组件化遍地开花的今天,这也很尴尬!

备注:为了解决“被注解者”与“注解代码”的桥梁问题,还有一种办法是生成注解对象的类别,如当前目标是被注解者,那么久生成改其类别扩展,并导入工程预编译中。这样的“类代码插庄”,也能间接的让我们获取到类外的注解内容。当然这个也一样存在编译期间扫描代码的问题,而且如果注解多,还会增加代码量。

2.2 方案2

基于类似FB的编译可配置来模拟的,用到__attribute((used, section("__DATA,"#sectname" “)))

目前beehive组件化框架也使用了此类方案[GitHub - alibaba/BeeHive: BeeHive is a solution for iOS Application module programs, it absorbed the Spring Framework API service concept to avoid coupling between modules.]

通过参考beehive组件化框架,我们最终选择编译期配置的方案,该方案可拆分为三个部分实现:

第一步:__attribute__机制在编译期插桩

第二步,运行时 从Mach-o的section data段 取出数据

第三步,针对性解析处理

3. 项目使用指南

以NeedLogin注解为例

以宏定义形式,仅需在需要使用注解的方法前面添加

@NeedLoginOCAnnotation(GlobalModuleRouter, jumpToAccreditViewController)

swift类使用

// 检测报告 关注按钮

@NeedLoginAnnotation(CheckReportViewController, collectAction, CheckReportModule)

第一个参数为类名,第二个参数为方法名,第三个参数为模块名(swift类),OC类传OC

比如首页中跳转录入车源方法:

如上图所示,原有代码仅可支持是否登录判断,未登录拉起登录页,已登录直接跳转录入车源。添加注解后,登录判断逻辑就不需要了,不仅支持是否登录判断,还支持未登录用户登录成功后自动跳转录入车源。

4. 实现原理

第一步:__attribute__机制在编译期插桩

__attribute__是在C, C++, Objective-C语言中使用的编译指令,一般以__attribute__(xxx)的形式出现在代码中,方便开发者向编译器表达某种要求,参与控制如Static Analyzer、Name Mangling、Code Generation等过程。

关于Attribute的语法描述见官方文档 Attribute Syntax:Attribute Syntax (Using the GNU Compiler Collection (GCC))

used

Used的作用是告诉编译器,我声明的这个符号是需要保留的。被used修饰以后,意味着即使函数没有被引用,在Release下也不会被优化。如果不加这个修饰,那么Release环境链接器会去掉没有被引用的段。具体的描述可以看gun的官方文档。

section

通常情况下,编译器会将对象放置于DATA段的data或者bss节中。但是,有时我们需要将数据放置于特殊的节中,此时section可以达到目的。例如,BeeHive中就把module注册数据存在__DATA数据段里面的"BeehiveMods"section中。

section通常用于修饰全局变量。

__attribute__的更多使用示例可参考FBTweak

编译器提供了我们一种__attribute__((section("xxx段,xxx节")的方式让我们将一个指定的数据储存到我们需要的节当中。

第二步,读取section中的值

现在来了解如何将存储在特殊section中的数据读出。

其中void initProphet()使用了__attribute__((constructor))修饰,

constructor / destructor

顾名思义,构造器和析构器,加上这两个属性的函数会在分别在可执行文件(或 shared library)load 和 unload 时被调用,可以理解为在 main() 函数调用前和 return 后执行:

constructor 和 +load 都是在 main 函数执行前调用,但 +load 比 constructor 更加早一丢丢,因为 dyld(动态链接器,程序的最初起点)在加载 image(可以理解成 Mach-O 文件)时会先通知 objc runtime 去加载其中所有的类,每加载一个类时,它的 +load 随之调用,全部加载完成后,dyld 才会调用这个 image 中所有的 constructor 方法

所以 constructor 是一个干坏事的绝佳时机:

所有 Class 都已经加载完成,main 函数还未执行

无需像 +load 还得挂载在一个 Class 中

若有多个 constructor 且想控制优先级的话,可以写成 attribute((constructor(101))),里面的数字越小优先级越高,1 ~ 100 为系统保留。

void initProphet()函数的实现体里使用了_dyld_register_func_for_add_image函数,现在看看该函数的作用。

_dyld_register_func_for_add_image:这个函数是用来注册回调,当dyld链接符号时,调用此回调函数。在dyld加载镜像时,会执行注册过的回调函数;当然,我们也可以使用下面的方法注册自定义的回调函数,同时也会为所有已经加载的镜像执行回调:

通过调用TTPReadConfiguration函数,我们就可以拿到之前注册到TTPNeedLogin特殊段里面的字典参数,该函数返回字符串的数组示例:[“{\”cls\":\""#cls"\",\"sel\":\""#sel"\",\"module\":\""#module"\"}, …]。

5. 遇到问题


  1. 常量名唯一性。编译期通过attribute机制插桩是通过定义全局c++变量实现的,那么就有可能出现重名的问题,原来的想法是以类名+方法名作为变量名,但是带参的方法名有冒号(:),变量名不能存在冒号,后将类名+行号+计数 拼接成变量名, 可保持唯一性。
  2. Swift类中不能使用宏定义
    I. 再写一套Swift注解,属性包装器结合单例 判断是否aspects已hook。\\因 定义时 不执行初始化方法 放弃该方案
    II. OC类中添加swift类方法的注解 \\有问题,使用需谨慎    
    1、类名 需加模块名.    
    2、方法名 与swift中定义的名称不同,需使用生成的OC方法名,所以需要添加@objc      3、.target-action 可以被hook,自定义的其他方法hook会失败,但经销商项目中使用时暂未发现该问题
    III. 读取plist文件 \\较麻烦
    IV. 单例强行处理 \\太low
    结合上述方案,我们选择在OC类中添加swift类方法的注解
  3. aspects hook  有缺陷
    1、不能多次hook 在基类中被继承的方法
    2、hook 本方法 在block中不支持延时处理 非前插后插  (已解决)

利用NSInvocation系统类,通过原方法的方法签名、参数、调用对象,在block中再次调用原方法。

       3、仅支持hook实例方法 (已解决)

             根据对象方法的调用机制,我们知道类方法存放在其元类中,类对象的ISA指针指向元类,就可通过类对象获取其元类,通过去hook元类的实例方法的方式,实现了对类方法的hook。

       4、 类方法与实例方法重名只能被hook一个

       5、swift target-action 可以被hook,自定义的其他方法hook有可能会失败,但暂未在经销商项目中复现。

注解中,虽可以实现对类方法和实例方法的区分,但是考虑到类方法与实例方法重名只能被hook一个 ,所以注解库中优先hook实例方法,若为实现实例方法,再去hook类方法,所以使用时需注意。

注:翻阅了很多文章,具体链接也不记得了,仅以记录


推荐阅读
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • Final关键字的含义及用法详解
    本文详细介绍了Java中final关键字的含义和用法。final关键字可以修饰非抽象类、非抽象类成员方法和变量。final类不能被继承,final类中的方法默认是final的。final方法不能被子类的方法覆盖,但可以被继承。final成员变量表示常量,只能被赋值一次,赋值后值不再改变。文章还讨论了final类和final方法的应用场景,以及使用final方法的两个原因:锁定方法防止修改和提高执行效率。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • Python爬虫中使用正则表达式的方法和注意事项
    本文介绍了在Python爬虫中使用正则表达式的方法和注意事项。首先解释了爬虫的四个主要步骤,并强调了正则表达式在数据处理中的重要性。然后详细介绍了正则表达式的概念和用法,包括检索、替换和过滤文本的功能。同时提到了re模块是Python内置的用于处理正则表达式的模块,并给出了使用正则表达式时需要注意的特殊字符转义和原始字符串的用法。通过本文的学习,读者可以掌握在Python爬虫中使用正则表达式的技巧和方法。 ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
  • 本文介绍了在Java中检查字符串是否仅包含数字的方法,包括使用正则表达式的示例代码,并提供了测试案例进行验证。同时还解释了Java中的字符转义序列的使用。 ... [详细]
  • loader资源模块加载器webpack资源模块加载webpack内部(内部loader)默认只会处理javascript文件,也就是说它会把打包过程中所有遇到的 ... [详细]
  • 正则表达式及其范例
    为什么80%的码农都做不了架构师?一、前言部分控制台输入的字符串,编译成java字符串之后才送进内存,比如控制台打\, ... [详细]
  • 微信商户扫码支付 java开发 [从零开发]
    这个教程可以用作了解扫码支付的整体运行过程,已经实现了前端扫码,记录订单,回调等一套完整的微信扫码支付。相关链接:微信支 ... [详细]
  • PreparedStatement防止SQL注入
    添加数据:packagecom.hyc.study03;importcom.hyc.study02.utils.JDBCUtils;importjava.sql ... [详细]
  • 只使用’if-else’语句的’else’部分是否可以接受?有时,我觉得检查所有条件是否都是真的更容易,但是只处理“其他”情况。我想 ... [详细]
author-avatar
小白也坚强_177
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有