当前位置: 首页 > 科技观察

抖音平台多产品代码隔离技术的实践与探索

时间:2023-03-16 19:25:28 科技观察

作者|郭宇鹏序言在软件架构领域,框架的作用类似于基础设施服务,是为了实现而形成的组件规范一个行业标准。简单理解,框架就是制定了一套规范或规则,开发者在规范或规则下工作。本文通过分析框架实体ServiceKit/Adapter来窥探其底层结构和架构设计。背景描述随着抖音业务的发展,为保证项目整体演进迭代计划的高效运行,系统建设已提上日程,Codebase(通称产品)集成是项目之一.本项目主要为开发者提供底层复用能力,提升研发团队效率,致力于帮助开发者轻松高效地开发和管理代码。在Codebase整合过程中,技术团队在各业??务线方向探索差异化;在演进过程中,业务线之间的耦合度越来越强,开发者迫切需要一种差异化代码隔离的解决方案。下图展示了抖音和抖音至尊版模块的区别。回顾痛点,在以往的开发中,开发者普遍采用宏隔离(isLite或isPad)来区分不同产品之间的差异,但这种方式严重破坏了整个抖音项目的架构。以下是几个维度的分析。研发效率:lint需要支持不同的宏变量。有重复的棉绒。单个组件很难区分项目来控制二进制发布频率。二进制文件需要经常更新。宏会造成很多二进制混入,影响编译效率。如果使用单个文件作为编译缓存单元,宏隔离也会降低编译缓存命中率。可扩展性:可扩展性差,缺乏动态能力和插件能力,增加新功能和修改原有功能会导致类实现的代码急剧膨胀。圈复杂度:宏隔离导致代码碎片化,修改重构成本高。组件粒度:不能支持项目间的不同业务由组件独立组成,背离了高内聚低耦合的原则。我们的目标和愿景是打造一套符合抖音工程架构体系,高效、通用、便捷的框架规范,让开发者可以在规范的规则下进行编码。架构设计启蒙蓝图启蒙设计是开始做事之前的抽象意识,如下图所示,在多个产品的开发环境中,可以高效复用公共代码,优雅隔离差异化代码。为了帮助新同学快速上手架构框架,笔者在构建Swift这个框架的过程中,根据近期经历的几个项目的经验总结出一套系统的脑图。让我与您分享框架系统化的全景。全景思维框架里面的内容很多,这里还是想提一下,说不定能在哪个阶段给你启发;建议从树的根节点开始,有选择地了解;如果想大概了解一下,Enter3层左右就可以了,如果想了解更多,请到叶子节点(为了不影响阅读体验,比较详细的节点都被截掉了)。基于上述框架系统化的思路,整章会先介绍一些设计思路,然后再深入到性能等相关技术细节。由于篇幅有限,我们将对我们认为比较重要的技术点进行简化说明。适配器模式的设计思维在设计模式中,适配器模式(adapterpattern)有时也被称为包装风格或包装。将类的接口转换为用户所期望的。适配通过将类自己的接口包装在现有类中,使因接口不兼容而无法协同工作的类能够协同工作。——百科Adapter模式开发同学不需要关心每个模块的复杂程度,业务逻辑,选择类对象还是实例对象,各自单元如何初始化等等,只需要根据自己的任务调度做在包装好的适配器上,类似万能充电器(90后同学时代的产物:>),不用关注电池是华为的还是OPPO的,即插即用。注册与发现服务注册-服务发现思想服务演进下面三张图简述了web服务时代从传统服务到微服务时代的历史(传统服务->并发服务->分布式微服务),有兴趣的可以了解更多,这里就不过多介绍了。微服务微服务是一种以业务功能为中心的服务设计理念。每个服务都有独立运行的业务功能,不受语言限制的API对外开放。应用程序由一个或多个微服务组成。——维基百科,微服务简单了解微服务后,从服务的角度来看,可以将多个Target产品按照每个业务模块划分为多个Adapter服务,与多个适配器协议结合,实现一对一-许多影响。.我们深入介绍了内部设计思路。在使用阶段,一个主类可以向多个适配器类发送消息;在注册过程中,一个适配器类可以绑定多个适配器协议,满足两种场景:一种是多个产品必须实现的接口,可以放在一个公共协议上,第二种是一个接口单个产品必须实现的是放在一个独立的协议上。公共协议+独立协议可以组合起来,由同一个带上下文的适配器类实现。说到微服务,我们就不得不了解接下来的两个概念,服务注册和发现。Serviceregistration服务注册:就是向一个公共组件注册提供某种服务的模块信息。(下面的示例代码更容易理解)//服务注册ServiceKit.register(AModuleServer);服务发现服务发现:是指使用一个注册中心记录分布式系统中所有服务的信息,以便其他服务可以快速找到这些已注册的服务;可以自动发现新的和删除的服务。(下面的示例代码更容易理解)//服务发现ServiceKit.get(AModuleServer);AdvancedBlueBox:抖音TargetBlackBox:抖音SpeedVersionTargetaXXXDOUYINAdapter:是XXXDOUYINAdapterImpl的服务实例。XXXDOUYINAdapterImpl:是订阅者,发布者是持有XXXDOUYINAdapterImpl实例的XXXDOUYINAdapter的主类。<>XXXDOUYINAdapter:面向协议编程,对Protocol接口进行抽象,分离出各自差异代码和通用代码的接口。上图进一步总结了整个项目背景(抖音和抖音至尊版的两套代码,有重复也有不同,如何继续分享重复的代码,将差异化的代码隔离到各自的Target中products,不再耦合),我们要做的过程(通过adapter模式进行任务调度,面向协议编程,将共享和差异化的代码抽象成接口形式,在每个Target中实现各自的协议Impl),到达结果(通过方便的脚手架和辅助工具,用户可以低成本学习和理解,操作简单)。关系图工程视角从抖音现有工程架构的角度理解设计。流程实战接下来我们进行一次流程化实战演练。代码实战中,订阅类在App内存中创建一个实例,订阅者的生命周期由所有关联的发布者决定,比如多个controller将埋点逻辑汇总到一个processor,或者比如一个父controller对应到多个子控制设备。技术细节了解了上面的设计图后,我们再简单分析一下内部的技术细节。在编译插件的一般思路下,注册会放在App启动阶段,但是这样做很容易拖慢App的启动速度。为了在不影响启动速度的情况下尽早注册,需要根据编译器特性实现:__attribute__((section("name"))),通过attribute命令,在运行时将其写在.data段中编译,然后运行的时候读出来。下图介绍了编译注解的简单过程。代码示例__attribute((used,section(_DY_SEGMENT","_DY_MSG_ASSOCIATE_SUBSCRIBER_SECTION)))static_dy_message_pair_DY_MSG_UNIQUE_VAR=\{\&_DY_MSG_ASSOCIATE_PROTOCOL_METHOD(INDEX),\&_DY_MSG_ASSOCIATE_LOGIC_METHOD,\};利用上述编译注解的能力,搭配协议反射,就能达到使用时,get协议进一步读取.data段中存储的内存地址进行加载。此功能也称为延迟加载。支持切面的核心思想如下(伪代码)。代码块模型在注册阶段暴露,可以在块中做类似AB的逻辑方面。isABTest=YES;注册{if(isABTest){returnObjectA.new;}else{returnObjectB.new;}}循环引用是为了防止在block中使用subscriber和publisher或者主类和adapter之间的关联下面造成循环引用,adapter底层使用NSProxy来实现。如下面这种情况,内存没有释放的问题就不用关心了。场景1@implementationDYAudioViewForDOUYINRegisterAdapters(DYFeedInteractionControllerPrivateProtocol,DYFeedContaineAudioAdapter){if(GET_AB_TEST_CASE(enableAutoPlay)){returnnil;}else{return[[DYAudioViewForDOUYINalloc]init]:}}s[voidselfweakTarget]refresh:^{[[selfweakTarget]refresh];[自停];}];}-(void)stop{//做点什么....}@end场景2@implementationDYFeedContainerGetAdapters(DYFeedContaineAudioAdapter,DYFeedContaineVideoAdapter,DYFeedModuleConfig)-(void)stopPlay{idadapter=[selfDYFeedContaineVideoAdapter];[[selfDYFeedContaineVideoAdapter]stopVideo:^{[adapterrefreshView];}];self.myBlockadView=^();};}@endbindingassociation绑定关联分为强关联和弱关联两部分。强关联:每个适配器都与主类强绑定关联,使得适配器的生命周期可以跟随主类自动释放,在使用适配器对象时可以保持内存处于最佳状态。弱关联:主类与适配器弱关联,这样在隔离的子类中,可以通过Key(self=adapter)获取主类,达到反向通信的效果。在多语言适配Swift环境下,无法在注册阶段使用友好的属性编译指令自定义段能力。如果要使用高性能的惰性注册功能,则必须另辟蹊径。将注册代码块直接放到MachO文件中的代码区,通过继承协议SwiftAdapter,实现层实现+(id)lazyRegister类方法,运行时的Api映射出A类对象,并调用服务发现阶段的A类方法代码,可以解决“懒注册”问题;然后改造底层框架,保证内部控件只初始化一次,用户视角不需要关心。E.g.classModuleADouYinLiteAdapter:NSObject,SwiftAdapterProtocol{classfunclazyRegister()->NSObjectProtocol{returnModuleADouYinLiteAdapter.init()}}便捷脚手架为各个语言环境下的服务发现和注册接口创建脚手架,方便使用。Objective-C宏接口由宏封装。//服务注册RegisterAdapters(ModuleDouYinLiteAdapter){returnModuleDouYinLiteAdapter.new;}//服务发现GetAdapters(ModuleDouYinLiteAdapter)SwiftProtocolExtensionSwift环境不能友好的使用宏封装,这时候我们可以通过扩展Protocol来达到封装的效果。//服务注册类funclazyRegister()->NSObjectProtocol,ModuleDouYinLiteAdapterProtocol{returnModuleDouYinLiteAdapter.init()}//服务发现Protocol.getAdapter(self,ModuleDouYinLiteAdapterProtocol.self)使用透视OC编码通用接口差异代码场景服务注册服务发现Swift编码独有接口差分代码场景服务注册预抽象协议接口,懒注册,支持切面。支持在各Adapter实现层获取WeakTarget(主类)。服务发现辅助工具就像很多人喜欢玩的网络游戏地下城与勇士(DNF)。辅助工具“连发”(顾名思义就是连发,可以联想到传统单发步枪和自动步枪的区别)不仅可以让玩家节省大量的按键成本,而且节奏组合中的罢工得到了增强。同理,我们推荐使用Xcode自定义模板工具进行编程,让用户减少敲代码的时间成本,开发时更专注于编码逻辑。为了让开发者更规范的使用,我们在代码静态检查阶段对代码进行拦截和修正,并结合目前的情况罗列了几个Badcase。场景示例中只隔离了分支判断逻辑,没有实现代码隔离。这样会把判断逻辑带到主类中,增加包体积。//例如错误示例-(void)masterFunction{if([selfDYFeedAModuleLiteAdapter]){//精简代码}else{//抖音或其他目标代码}}//-----------------------------------------------------------------------//例如正确示例-(void)masterFunction{[[selfDYFeedAModuleAdapter]runFunction];}//各自Target实现runFunction协议方法//在抖音-(void)runFunction{//代码}//inLite-(void)runFunction{//code}场景二同一个产品中,一个协议被多个类实现(Debug环境编译阶段会通过断言进行第一次拦截)。//例如错误示例(抖音targer)@interfaceAModuleAdapter@interfaceBModuleAdapter//例如正确示例(抖音targer)@interfaceAModuleAdapter@interfaceBModuleAdapter场景3不同产品中的Adapter方法离线可能返回空值。如果要使用Adapter做一些逻辑编码,需要提前判断是否为空。//例如错误示例-(DYAModuleFeedType)getType{return[[selfDYModuleAdapter]checkType];}//例如正确的例子-(DYAModuleFeedType)getType{return[selfDYModuleAdapter]?[[selfDYModuleAdapter]checkType]:/*底层逻辑*/;}生态建设至此,多产品适配器框架实体Adapter已在多个平台业务线批量使用抖音,覆盖了大部分OC业务场景,同时也构建了Swift场景能力,框架父ServiceKit20+Apps已经接入。写在最后,踏踏实实做下去对于核心框架,我们可能只写一行代码,但是会有几百万甚至几千万行代码从中穿过,所以一定要慎重考虑。