美文网首页
iOS SDK开发二三事

iOS SDK开发二三事

作者: 一意孤行的程序猿 | 来源:发表于2020-04-23 14:43 被阅读0次

    引子

    • 无意间,看到5年前,Android大佬子勰写的,关于SDK开发方面的文章(SDK那些事(总纲)), 不由得唤起自己开发iOS SDK的回忆;本文简单总结下自己开发SDK方面的经验;

    • SDK(Software Development Kit)可以最大程度实现代码和功能的复用,为业务开发提供一个非常好的支持;这里的业务可以是内部业务,也可以是外部业务

    • 简单来说,所谓SDK开发,本质是服务提供;不仅需要写好代码,还要完善代码之外的事情,任重道远

    一、准备

    1、清晰解决的问题和要求

    ​ 一般而言,新起一个SDK必然有其深刻的业务背景;研发同学对SDK要解决的问题和SDK的特殊要求,了解地越详细越好;常见的要求有:

    • 禁止采集用户信息【安全方面】
    • 必须对SDK使用者鉴权【安全方面】
    • 核心代码必须混淆【安全方面】
    • 不可以有调试日志,不可以监控上报【安全方面】
    • 持久化的敏感数据要加密;【安全方面】
    • SDK大小不可以超过XXKB:【其他】
    • .......

    2、选择合适的开发语言

    • 大多数情况下,选择Objective-C开发就对了,不仅能接入Swift开发的项目,还能接入Objective-C开发的项目;
    • 当然并非绝对,具体根据业务情况决定;

    3、选择合适的技术方案

    • 网络请求使用第三方框架,还是直接利用iOS的API;
    • JSON转Model、Model序列化和反序列选择哪个第三方框架(性能,框架大小方面考虑)
    • 持久化存储选择SQLite、NSUserDefaults 或是 Keychain;
    • UI布局使用 frame、autolayout(Masonry框架) or flexbox(YogaKit框架)
    • MVVM or MVC 架构模式选择;
    • ...

    4、确立基本代码规范

    ​ SDK可能长期维护 或 多人开发,确立好基本代码规范,能保障SDK的代码质量;这些规范本质上是一些共识和约束,如:

    • 命名规范:属性、变量、方法等均采用小写字母开头的驼峰命名;类名使用大写字母开头的驼峰命名
    • 注释要求:对外暴露的类、方便和变量要有注释,解释其功能;关键代码要有注释;
    • 简洁要求:无用的代码选择直接删除,不要注释;无用的资源及时清除;
    • ....

    二、SDK的主体设计

    1、多模块设计

    • SDK中可能包含不同的模块功能,而不同的业务方需要的模块可能不同;对SDK中模块进行拆分,保证业务方尽可能引入的是他们需要的代码;

    • 一般使用Cocoapods创建Pod库的,在podspec中定义好模块,为每个模块清晰定义好包含的代码和资源,以及外部依赖(静态库 or 静态库);这样可以将模块之间实现代码和资源的物理隔离;

    • 关于Cocoapods创建Pod库更多细节可以参考Cocoapods使用小记,至于是公有Pod还是私有Pod根据实际情况定;

      创建公有Pod库或者私有Pod库原理是一样的;不一样的是:两者的版本索引查询方式不一样,公有库的podspec由CocoaPods/Specs管理,而内部私有使用的pod库需要公司内部建立一个仓库来管理podspec

    2、SDK目录层级

    • Pod库中,代码放在Classes目录下,图片资源放在Assets目录下;

    • Classes目录按模块划分第一级目录,如AModuleBModuleCModule等,其中每个模块Code再划分二级目录,如ModuleService、View、Controller、Model、API等;具体的代码文件存放在这些二级目录中;其中ModuleService中代码是要对外暴露的,其他预期外部不可见;

    • 资源方面,也按模块细节;主要的资源是图片资源,在Assets目录下新加AModule.xcassetsBModule.xcassetsCModule.xcassets等;

    3、公开接口设计

    • SDK使用者们关注接口是够好用,设计好接口,让你的SDK使用体验加分;

    • 接口功能尽量职责单一,接口需要的参数不要太多;如果参数多,可以使用Model将业务参数封装下;提供Model的default实现;接口名,参数名使用驼峰命名,最好见名知义;

    • 接口中每个参数类型要明确,不要出现idNSDictionary这样的类型;避免业务随意传参,增加SDK内部对参数校验难度;也减少业务方对参数的困惑;

    • 接口内第一件事情是要做参数校验,不符合预期情况,Debug模式直接Assert,及早把问题抛出;Release模式要记录到日志并上报,提前返回,避免后续出现迷惑问题,增加排查问题成本;

    4、代理和状态码

    • 除了暴露接口,SDK还可能暴露代理(Delegate)方法 和 状态码;
    • 代理(Delegate)是需要业务方根据需要实现的,代理(Delegate)中可选实现方法加上@optional关键字,没有的话默认要实现;协议和协议中具体方法的作用要增加上注释;
    • 状态码设计上,需要注意两点:
      • SDK中定义的状态码使用枚举(NS_ENUM)定义,对应的每个值增加注释;
      • 如果SDK依赖了其他SDK,这些SDK的状态码最好不要透传给业务方,中间增加转化;比如微信SDK中WXErrCode不要透传给业务方,封装一下,对外暴露的是我们的状态码;

    5、SDK的版本号

    • SDK都要有明确的版本号,一般版本号分三段:主版本、特性版本、修正版本,如5.6.1;其中主版本号用于大版本的发布,特性版本主要用于更新迭代,修正版本号主要用于bug修复。
    • SDK内部的埋点、监控,网络请求等都要携带SDK的版本号,这些版本号对定位SDK问题非常重要;
    • SDK对外一般是二进制的形式提供,发布的SDK需要带上版本号信息;至少要保证三个地方的SDK版本信息是一致的;git仓库的tag版本号podspec中的version代码中的版本号;可以实现个脚本,在编译时候,统一修改这三处的版本号信息;

    6、SDK安全需求

    • 目前遇到的,有关SDK安全方面要求有:
      • 核心代码的混淆;包括字符串常量、类名和方法名方面的混淆;
      • 敏感数据需要加密存储,禁止明文传输等
      • 对SDK使用者鉴权的要求,"非法"App不得使用,非法调用者必Crash;
    • 此外,有不常见的”安全诉求“:App内不得有任何调试信息,埋点、采集用户数据等行为;不得使用带来安全隐患的API,比如打开WKWebview跨域开关这个就不被允许;
    • 代码混淆后,混淆前后的符号间映射一定要保留,否则线上问题堆栈信息中,出现的SDK混淆后的符号会让定位问题非常迷惑;可以做成转化工具,导入堆栈符号信息,输出混淆前正常的符号信息;

    三、对业务的支持

    1、提供SDK文档

    • SDK开发好后,对外还要提供相关文档,包括但不限于:SDK功能介绍SDK接入办法SDK具体使用SDK 更新日志记录 以及 SDK问题记录
    • 基于专业角度,建议提供SDK概况文档,包括但不限于:接入的业务情况,优化记录,SDK的依赖库信息,二进制 和 资源大小信息等;有些业务对SDK大小敏感,有些关注SDK的稳定性...;
    • 业务方在接入SDK前,是从接触SDK文档开始,好的文档能帮助SDK更好地被接受,后续更好开展工作;

    2、提供Demo

    • 只有文档还不够的,一定要提供Demo;Demo有两个好处:
      • 帮助业务方结合文档更好了解SDK的接入和使用;
      • 帮助测试及时验证SDK升级后带来的影响;
    • Demo中不仅有SDK引入办法、使用办法;还可以写一些简单UI,帮助展示SDK功能;每次SDK升级,都通过Demo自动出包,提供给测试人员和产品去验证(功能验证和设备兼容性验证);
    • SDK的自动化测试这块,暂未尝试;但在是Demo中,定义对SDK接口的单元测试是必要的;单元测试要关注:非法传参case,非主线程调用case。

    3、规范SDK开发流程

    • 开发SDK和开发完整项目一样,要有需求评审、技术评审、排期,开发,自测,提测、测试验收等环节 ;不同功能在不同feature分支上开发,每个feature功能验证通过后可以合入主干分支;合入主干后,由主干分支发布版本;

    • 发布的SDK使用二进制的形式;SDK使用二进制形式,不仅能提示项目项目编译速度,也能保护好源码;如果业务方需要SDK源码,需要向SDK负责人申请权限;

    4、帮助业务排查问题

    • SDK开发者经常会收到业务方帮助排查问题的诉求;毕竟,SDK对业务方是一个黑盒,很多业务上涉及到SDK问题,需要SDK开发者帮助排查,耗时耗力;
    • 为了更好帮助业务排查问题,推出三件套:SDK核心链路监控埋点 + 重要日志信息持久化并上报 + 调试日志信息可视化;前两者是固化在于SDK中的;而调试日志信息是交给业务方,由他们灵活处理;
    • 业务方拿到调试日志信息,可以将其输出到控制台,也可以输出到App的日志可视化工具中;鉴于业务方可能没有,可以提供一个轻量级的日志可视化工具。
    • 必须注意的是,SDK中的调试日志信息禁止带到线上,SDK的Release版本不可以带这些信息。

    5、沟通Plus

    • 重要内容要有记录:SDK会被多个宿主App接入,不同的App环境不同,SDK可能遇到很多问题,积极帮助解决后,记录下来,作为后续宿主App使用SDK的重要参考;
    • 建立SDK和业务沟通机制;及时同步SDK最新信息;SDK的bugfix版本,要及时同步,并帮助业务升级,尽量减少损失;
    • 为SDK增加代码Reviewer:SDK重大升级,最好involve主要业务方的研发进行技术评审和Code Review;其实这对业务方是个很高的要求,需要业务方至少有一个人对SDK有比较全面的了解;

    四、SDK实现中的注意事项

    1、注意多线程使用

    • 不要在主线程执行耗时操作,可以将他们交给子线程;
    • 控制好并发数量,GCD并发队列并不会去管理最大并发数,无限制提交任务给并发队列,会给性能带来问题。可以适当控制并发数量,防止线程爆炸;具体可见 iOS实录16:GCD小结之控制最大并发数
    • 遇到必须要在主线程执行的任务;先判断当前是否在主线程,不在的话,可以通过GCD将任务放到主线程队列执行;此外,还可以加断言,Debug下,非主线程执行执行抛异常,Crash,帮助及时发现问题;
    • 尽可能使用轻量级的锁,可以使用信号量;自旋锁性能非常好,但是有优先级反转的问题,谨慎使用;

    2、使用缓存要克制

    • 无论是内存缓存和磁盘缓存都要有清除策略(可以LRU)和使用大小限制
    • 如果内存缓存没有清除策略和使用大小限制,会导致内存使用无限制增长,最后可能会导致OOM问题;此外,当收到内存警告时候,内存缓存要及时清除,否则可能引起OOM问题,直接破坏用户体验;
    • 如果磁盘缓存没有清除策略和使用大小限制,会导致磁盘空间滥用,对App整体体验都不好,而且后续清理成本比较高;
    • 持久化在磁盘文件中数据,不要在App启动时去读取,可以懒加载;不要对数据量和文件读取性能报侥幸,随着SDK的迭代,那些数据可能不断变大,也可能在低端机器上文件读取性能比较差,偶现几十ms甚至几百ms的耗时,直接拖累启动速度;

    3、内存使用要注意

    • 尽量优化内存使用,四个原则:减少大块内存使用、降低内存峰值、避免内存泄露和处理内存警告;
    • 具体到内存使用中技巧:
      • 合理使用autorealsepool,降低内存峰值,避免 OOM
      • 复用大内存对象,如UITableViewCell对象;懒加载大的内存对象
      • NSCache 代替 NSMutableDictionary,使用NSPurgableData 代替NSData
      • weak strong dance 来解决 Block 中的循环引用,代理(delegate)使用weak修饰;
      • CoreFoundation对象、CoreGraphics对象、还有C/C++的内存分配需要管理好,有malloc()和calloc()就要有free;
      • ....
    • 了解内存方面知识可以看:iOS内存二三事

    4、使用单例要注意

    • 有些SDK通过单例对象提供服务;因为,单例对象只有一个对象,不仅可以节约开销,还可以保证App中多业务操作SDK中同一个服务对象;
    • 但是定义单例要注意;平常App开发中,写单例不怎么严谨,提供个让外部访问的类方法,如+ (instancetype)sharedInstance,内部使用dispatch_once保证alloc和init只执行一次,这种是粗发式单例,并不能保证绝对单例;
    • 在SDK中,一定要保证外部访问到的是单例对象;除了提供让外部访问的类方法,还要重写+(instancetype)allocWithZone-(instancetype)copyWithZone-(instancetype)mutableCopyWithZone方法,保证永远都只会分配一次内存空间,实现真正的单例;
    • iOS中,一个对象有且只能有一个代理;如果你的单例对象有需要业务方实现的代理方法,根据实际情况判断,是否需要实现多代理

    5、注意宏定义和条件编译

    • 编译器前端(如Clang)在编译源码时,首先要做预处理(preprocessor),如头文件引入,宏替换,注释处理,条件编译(#ifdef) 等;
    • 一般地,SDK中会使用到条件编译;在源码情况下,可以根据不同的条件,编译不同的源码;但是SDK使用二进制的形式对外提供的话,在二进制化时就已经根据XX条件编译好了;因此,二进制的SDK不会能跟随宿主App编译条件变化了;
    • 使用宏可以提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。但是如果宏对应实现,需要根据条件编译来区分不同的行为;在二进制编译时候,宏定位的行为会被确定下来;
    • 宏定义有个小细节,用do{...}while(0)构造的宏定义不会受到大括号、分号等的影响,非常建议使用。

    五、SDK实现中的小技巧

    1、weak symbol

    • 简介:symbol默认都是strong的,但是可以增加 __attribute__ ((weak))属性将其变成weak symbol;weak symbol在链接时候比较特殊:
      • strong symbol必须有实现,否则会报错;
      • 不可以存在两个同名的strong symbol
      • strong symbol可以覆盖weak symbol的实现
    • SDK可以用weak symbol提供默认实现,然后业务中利用strong symbol把业务实现注入进来,以此来实现依赖注入

    weak symbol 不做标准方案推荐; 遇到要临时适配某些业务的特殊case,时间紧急情况下,可以"剑走偏锋";

    2、预定义符号

    • 简介:有些公开功能使用宏定义的函数形式,可以在函数中带上__FILE____LINE____FUNCTION__这些C语言中预定义符;这样可以在发生问题时,更好找到使用宏函数的位置,demo如下:

      #define AddFunc(a,b) 
      do { \
          addFuncImp(a,b, __FILE__,__LINE__,__FUNCTION__); \
      } while(0)
      
    • 有人问过:为什么不使用[NSThread callStackSymbols]获取当前线程堆栈信息,岂不是更好;不使用有3方面考虑:

      • 只是为了获取调用代码位置;了解到SDK调用位置,排查问题能高效地多;
      • [NSThread callStackSymbols]捕获堆栈信息在符号裁剪情况下,主模块中的是内存地址信息,而不是符号信息;
      • __FILE____LINE____FUNCTION__的成本更低,性能更好;

    3、section()函数

    • 简介:section()函数是Clang提供的,可以读写二进制段;实际应用中,在编译阶段将一些确定的常量写入数据段(__DATA段),并在运行期根据需要读取出来;可以利用此能力实现延迟加载;

    • 在阿里的iOS的BeeHive有类似的使用,如下:

      #define BeeHiveMod(name) \
      char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
      
      #define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
      

      used关键词是告诉编译器,Release下不优化,必须保留这个符号;否则Release下。链接器会优化掉没有引用的符号;

    4、pre-main和after_main

    • 简介:App启动耗时一般统计pre-main阶段(T1),和main()函数之后到didFinishLaunchingWithOptions方法执行完这段(T2)阶段;SDK中可以利用__attribute__ ((constructor))__attribute__((destructor))这两个函数属性在pre-mainafter_main时机做一些事情;

    • 使用这两个属性定义函数示范如下:

      __attribute__((constructor)) void before_main_xxxx()
      {
          //can do something
      }
      __attribute__((destructor)) void after_main_xxxx()
      {
          //can do something
      }
      
    • 需要说明的是:在pre-mainafter_main时机,千万不要做耗时操作;在SDK(二进制形式)中使用比较隐蔽,一般情况下,业务方很难想到或注意到;如果在pre-main时机做了耗时的事情,宿主App启动体验就不太好了;

      dyld加载过程分四步:Load dylibs imageRebase/Bind imageObjc setupinitializers;其中+load()__attribute__((constructor))之前,他们都在initializers阶段内完成;initializers之后就是main函数执行了;

    5、Method Swizzling

    • 简介:Method Swizzling是Objective-C中运行时特性之一,本质是在运行时交换方法实现(IMP);SDK有时候需要Method Swizzling利用hook一些系统(Objective-C)方法;
    • 需要Method Swizzling的话,推荐使用RSSwizzle,他是线程安全的Method Swizzling方案,优势是:不需要在+load()中实现方法交换 而且是 线程安全的;

    推荐👇:

    • 020 持续更新,精品小圈子每日都有新内容,干货浓度极高。

    • 结实人脉、讨论技术 你想要的这里都有!

    • 抢先入群,跑赢同龄人!(入群无需任何费用)

    • (直接搜索群号:789143298,快速入群)
    • 点击此处,与iOS开发大牛一起交流学习

    申请即送:

    • BAT大厂面试题、独家面试工具包,

    • 资料免费领取,包括 数据结构、底层进阶、图形视觉、音视频、架构设计、逆向安防、RxSwift、flutter,

    参考文章

    如何编写一个(不)受欢迎的 iOS SDK

    听阿里云工程师谈谈如何开发一个优秀的SDK

    作者:南华Coder
    链接:https://juejin.im/post/5e9adea16fb9a03c364f216e

    相关文章

      网友评论

          本文标题:iOS SDK开发二三事

          本文链接:https://www.haomeiwen.com/subject/awcaihtx.html