写点什么

一年撸完百万行代码,企业微信的全新鸿蒙 NEXT 客户端架构演进之路

作者:JackJiang
  • 2025-04-16
    江苏
  • 本文字数:9218 字

    阅读完需:约 30 分钟

一年撸完百万行代码,企业微信的全新鸿蒙NEXT客户端架构演进之路

本文由企业微信客户端团队黄玮分享,原题“在流沙上筑城:企微鸿蒙开发演进”,下文进行了排版优化和内容修订。


1、引言

当企业微信团队在 2024 年启动鸿蒙 Next 版开发时,我们面对的是双重难题:

  • 1)在 WXG 小团队模式下,如何快速将数百万行级企业应用移植到全新操作系统?

  • 2)在鸿蒙 API 还是 Preview 的初期,如何保持业务代码的稳定,在 API 快速更新的浪潮中岿然不动?

DataList 框架给出了破局答案(即通过三重机制构建数字负熵流):

  • 1)结构化熵减:将业务逻辑渲染到 UI 的过程抽象为数据流,使鸿蒙与 Android 共享同一套数据驱动的开发机制;

  • 2)动态熵减:通过抽象出来的 UI 数据层屏蔽鸿蒙 API 的变化,让业务代码历经三个版本的 UI 层大改而不受影响;

  • 3)认知熵减:将跨平台差异封装为一系列通用组件,降低开发者心智负荷,可以专注于业务开发而不用关心技术变更。

本文将要分享的是企业微信的鸿蒙 Next 客户端架构的演进过程,面对代码移植和 API 不稳定的挑战,提出了 DataList 框架解决方案。通过结构化、动态和认知三重熵减机制,将业务逻辑与 UI 解耦,实现数据驱动开发。采用 MVDM 分层架构(业务实体层、逻辑层、UI 数据层、表示层),屏蔽系统差异,确保业务代码稳定。


2、企业微信客户端框架进化史

罗马不是一天建成的,我们在开发框架方面,也经历了 发现问题、探索方案 、优化改进 的过程。

野蛮生长(2019 年前):

  • 1)背景:团队缺乏统一规范,开发风格各异;

  • 2)问题:相同功能重复实现,维护成本高。

初步探索(2019-2022):

  • 1)背景:急需统一开发范式,提高开发效率;

  • 2)实现:EasyList 框架,提出"一切皆列表"理念,封装模板代码,让开发者专注于业务开发;

  • 3)问题:未严格隔离业务与 UI,退化为 MVC 模式;抽象能力不足,组件复用率极低。

渐入佳境(2022-2024):

  • 1)创新:实现了基于数据驱动/分层隔离的 DataList 框架;

  • 2)价值:框架提供抽象能力,降低开发认知负担;让每一个组件都具备复用能力,极大提高了复用率,助力通用组件从个位数突破至 50+。

3、企业微信客户端框架整体设计

3.1 整体架构设计

DataList 是一套基于数据驱动的分层隔离框架,整体架构图如下图所示。

▲ 图 1:DataList MVVM 架构图

接下来将从数据流向、分层架构的角度分别对这张图进行讲解。

3.2 数据流向设计

从数据流向的角度,DataList 框架可以简单分为 Data/List 两部分:

  • 1)List:业务逻辑部分,简单来说就是业务数据如何转换为 UI 数据;

  • 2)Data:数据驱动部分,UI 数据如何渲染为实际的 UI/如何驱动 UI 刷新。

▲ 图 2:DataList 数据流向图

3.3 MVDM 环形分层设计

DataList 通过将业务数据到 UI 数据的转换逻辑独立出来,系统形成了清晰的边界层次:

  • 1)业务实体层(Repo):负责请求数据,拿到业务数据(保持稳定);

  • 2)业务逻辑层(ViewModel):处理业务逻辑,负责业务数据到 UI 数据的转换(保持稳定);

  • 3)UI 数据层(CellData/ViewData):对 UI 层的抽象(内部适应变化,对外接口稳定);

  • 4)表示层(Cell):处理具体 UI 渲染(拥抱变化,适配平台新特性)。

相当于 MVVM(Model-View-ViewModel)变成了 MVDM(Model-View-Data-ViewModel)。

箭头代表依赖指向:

▲ 图 3:DataList 环形分层图

这里介绍下 UI 数据层。

将整个控件数据化,即为 ViewData:

export class TextData extends BaseData {

  text?: string | Resource

  fontColor?: ResourceColor

  fontSize?: number | string | Resource

  fontWeight?: number | FontWeight | string

将多个 ViewData 组合起来,成为一个组件 CellData:

//由 Image+Text 组成

export class ImgTextCellData extends BaseCellData {

  builder: WrappedBuilder<[]> = wrapBuilder(ImgTextCellBuilder)

  root: RowData

  img?: ImgData //对应 Image 控件

  text?: TextData //对应 Text 控件

}


由于 CellData 内不含任何业务代码,所以不受限于业务,天然可以复用。下图是组件复用统计(现有 58 个组件,数千次复用)。

▲ 图 4:通用组件复用统计

这样分层的好处:

  • 1)方便 UI 大规模复用;

  • 2)跨平台代码一致性;

  • 3)隔离业务与 UI,UI 层变动不影响业务逻辑。

3.4 无可删减:DataList 开发示例

完美的达成,不在于无可增添,而在于无可删减。 ——《风沙星辰》 安托万·德·圣-埃克苏佩里

梳理一下,开发一个业务需求,哪些部分是无可删减的?

其实就是业务相关的部分:

  • 1)数据请求;

  • 2)业务数据转为 UI(UI 数据)。

这些都是必须由开发者填写的逻辑,这些步骤框架最多只能简化,不能代劳。

比如:我们开发一个极简版本的人员列表,看下对应步骤。

数据请求:

//Repo 对应 Model 层


class DemoContactRepo():IListRepository<DemoContactReq,DemoContactRsp> {


override fun requestData( req: DemoContactReq,//请求参数 callback: (rsp: DemoContactRsp) -> Unit,//结果回调 errorCallback: (errorCode: Int, errorMsg: Any?) -> Unit//错误回调 ) { //请求数据,返回 ContactService.getContact(req){contacts-> callback(contacts) } }


}


数据转换:

//继承自单数据源列表基类,泛型指明请求与返回的业务数据类型


class DemoContactViewModel: SingleListViewModel<DemoContactReq, DemoContactRsp>() {


/** * 业务数据转为 UI 数据 */ overridefun transferData(data: DemoContactRsp): List<ICellData> { returndata.contacts.map { ImgPhotoTextImgCellData( //通用组件 dataId = it.id, photo = PhotoData(url = it.avatar),//一个图片控件 leftText = TextData(text = it.name))//一个文本控件 } } /** * 拉取数据所用的仓库(对应 Model 层) */ overridefun initRepository(): IListRepository<DemoContactReq, DemoContactRsp> { return DemoContactRepo() } /** * 初次或刷新页面时的请求参数 */ overridefun refreshParam(arguments: Bundle?): DemoContactReq { return DemoContactReq(0,20) }


}


算上注释,「总计 39 行」,一个极简版联系人列表就开发完成了。

▲ 图 5:DataList 联系人 Demo

如果是一个本地静态页面,可以去掉网络请求部分,直接堆砌通用组件(CellData)即可,完整代码只要 40 行。

//继承自本地静态列表基类,无数据请求


class DemoAttendanceViewModel:LocalSingleListViewModel() {


//... //&#128295; 乐高式组件拼装 overridefun transformCellDataList(): List<ICellData> { return listOf( attendanceCellData("打卡人员","员工 A").section(1), attendanceCellData("规则名称","打卡规则 abc").section(1), attendanceCellData("规则类型","固定上下班").section(2), attendanceCellData("打卡时间","周一至周五,09:00-10:00").section(2), attendanceCellData("打卡方式","手机+智慧考勤机").section(3), attendanceCellData("打卡位置","天府三街 198 号").section(3), attendanceCellData("打卡 Wi-Fi", "未设置").section(3), attendanceCellData("打卡设备", "").section(3), TextCellData(TextData.tips("位置和 Wi-Fi 满足任意一项即可打卡")).noneDivider(), attendanceCellData("加班规则","以加班申请为准").section(4), attendanceCellData("更多设置","").section(5), ButtonCellData(ButtonData("删除规则", buttonStyle = R.style.button_l_white, textColor = R.color.day_night_color_chrome_red.getColor())).section(6)) } //对通用 Cell 的简单封装 privatefun attendanceCellData(title:String,desc:String):ImgPhotoTextImgCellData{ return ImgPhotoTextImgCellData(/*设置属性*/) }


}

▲ 图 6:DataList 静态列表 Demo

3.5 MVDM 架构的延迟决策实践

如果想设计一个便于推进各项工作的系统,其策略就是要在设计中尽可能长时间地保留尽可能多的可选项。 ——《整洁架构之道》

通过 MVDM 分层架构,我们构建了业务逻辑与 UI 渲染的解耦机制。但真正的考验来自鸿蒙 Next 开发——当底层 API 如流沙般变动时,如何保持上层建筑的稳定?

通过 UI 数据层的隔离,MVDM 的 UI 层历经三个大版本的架构演进,业务层仍保持稳定:


  • 1)妥协版:快速启动业务开发;

  • 2)适配版:拥抱动态属性能力;

  • 3)优化版:突破性能瓶颈。

这三次蜕变完美诠释了"流沙筑城"的技术哲学:在持续变化的基础设施上,通过架构设计构建确定性。接下来我们将深入每个阶段的演变历程。

4、第一版:系统限制下的妥协

4.1 目标:快速启动

由于我们所有页面都基于 DataList 开发,需要尽快实现数据绑定能力,让业务开发可以启动。

4.2 实现思路

鸿蒙和 Compose 一样,UI 组件是函数而不是类,没办法像 Android 那样,拿到控件的对象进行赋值。


@Component


export struct DemoPage{


build(){ Text("Hello World!") //这是一个函数,没法拿到它的对象,也就没法进行动态赋值 }


}


如果要实现数据与 UI 的绑定,只能在这里对所有属性进行遍历调用.。

4.3 技术方案

在现有 API 的基础上,我们只能实现这个方案。

▲ 图 7:数据绑定第一版

直接把所有属性列出来,全部调一遍,如果 data 里对应属性没有赋值,就相当于用 null 调用了一次。

4.4 实践问题

这个方案有很多问题:

  • 1)即使我在 Data 里只设置了一个属性,也需要执行一遍所有函数;

  • 2)某些属性函数,用 null 调用和不调用,表现是不一样的,这种属性无法列出;

  • 3)太丑,不优雅。

我们迫切需要一个能动态设置属性的方案,因此我向华为官方提出了需求。

▲ 图 8:向华为提需求

这个需求交付之后,就有了第二版。


5、第二版:动态属性下的数据绑定

5.1 接入动态属性设置能力

之前提的需求,华为给的解决方案是 AttributeModifer。

这是官网的介绍:

▲ 图 9:Modifier 能力介绍

5.2 技术方案

接入 AttributeModifer 后,UI 层的写法如下:

@Component


export struct WwText {


@ObjectLink data: TextData


@State modifier: TextModifier = new TextModifier(new TextData())


aboutToAppear(): void {


this.modifier.data = this.data


}


build() {


Text(this.data.text) .attributeModifier(this.modifier) //通过 modifier 更新属性,不必再调其他函数


}


}

这里更新的原理大致如下图:

▲ 图 10:第二版更新机制

TextData 被 @Observed 注解之后,实际上是被动态代理了:

  • 1)代理类观察到属性变化;

  • 2)从记录的 set 里找到观察者;

  • 3)调用观察者的更新函数(实际流程比较复杂,很多调用);

  • 4)这个更新函数里面就会执行 Modifier 里面的 applyNormalAttribute 函数,最后将属性动态设置到控件上。

WwText 编译后的 ts 代码如下:


//WWText.ts


export class WwText extends ViewPU {


//... initialRender() { this.observeComponentCreation2((elmtId, isInitialRender) => { //这里就是会刷新的部分 Text.create(this.data.text); Text.attributeModifier.bind(this)(ObservedObject.GetRawObject(this.modifier)); }, Text); Text.pop(); }


}

5.3 实践问题

实际使用中发现,这套方案有两方面很显著的问题。

1)问题 1:代码膨胀:

在实际应用这些 Ww 系列封装组件的场景,可以看到编译后的代码膨胀的非常明显,两行编译后变成了二十行。

▲ 图 11:ets 源码/ts 产物

一个通用组件,编译后从 4k 变成了 75k。

▲ 图 12:编译后体积变化

问题 2:性能消耗:

这个写法的性能也非常差,主要是三个方面。

1)冗余刷新:

在 applyAttribute 这里,如果 TextData 里面设置了 10 个属性,但是本次只更新了一个属性,那么在触发更新之后,仍然会 10 个属性都重新设置一遍。


export class TextModifier extends BaseModifier<TextAttribute> {


//...


applyAttribute(instance: TextAttribute, data: TextData) {


super.applyAttribute(instance, data) if (data.fontColor || data.fontColor == 0) { instance.fontColor(data.fontColor) } if (data.textAlign) { instance.textAlign(data.textAlign) } //...


}


}

2)状态管理:

现在鸿蒙这套状态管理机制,在 DataList 数据绑定的场景下性能不足。查了一下鸿蒙状态管理机制的源码,状态变量是通过动态代理来感知属性变化的,具体一点就是通过 SubscribableHandler 来代理属性的 set、get 等操作,源码如下。

class SubscribableHandler{


get(target,property,receiver){ //... switch(property){ default: const result = Reflect.get(target,property,receiver)//反射获取属性 if(/*...*/){ let isTracked = this.isPropertyTracked(target, propertyStr); this.readCbFunc_.call(this.obSelf_, receiver, propertyStr, isTracked); } } }


}


经过测试:这个 get 函数的耗时为万次 9ms。而我们的 Modifier 里面恰好有很多 if,需要拿值来判断。

简单算一下,一个页面 10 个 cell,每个 cell5 个 Text,每个 Text23 个属性+45 个基础属性:


一次刷新 get 次数 = 10X5X(23+45) = 3400 次

3400/10000X9 = 3ms


也就是说,没有执行任何具体逻辑,只是取值判断,就消耗了「3ms」。而鸿蒙 120 帧率的情况,一帧的渲染时间也只有 8.3ms。

3)节点增多:

对原生控件进行包装后(Text ==> WwText),View 树里会增加一个节点(橙色)。如果某些情况图方便给外层组件又设置了属性,还会再额外增加一个渲染节点(红色)。

比如下面这个组件:


Column(){


WwText({data:this.data1}).width("100%") WwText({data:this.data2})


}


对应的 View 树如下:

▲ 图 13:节点增多示意

节点从两个变成了五个,而鸿蒙的渲染性能优化就是要求节点越少越好。

6、第三版:基于自定义状态管理的性能优化

6.1 目标:性能优化

第三版的目标就是解决第二版的诸多问题,进行性能优化。

6.2 实现思路

针对这些问题,分析的思路如下:

▲ 图 14:第三版问题分析

6.3 技术方案

1)去掉控件包装:

前面提到使用包装控件有两个弊端:

  • 1)编译后的代码增加,体积增大;

  • 2)增加节点,消耗性能。

因此,我们决定去掉包装,使用原生控件。

那么有两个问题:

  • 1)原本的控件基础逻辑放哪里(比如 WwPhoto 里加载图片的逻辑);

  • 2)之前提到,我们用 AttributeModifier 时,控件的属性函数我们可以动态调用,但是构造函数不行,那如何更新构造函数?

这两个问题都可以用 AttributeUpdater 来解决,它是 AttributeModifier 的子类。

划重点:

▲ 图 15: AttributeUpdater 说明-划重点

去掉包装类之后,原本放到包装类里面的基础逻辑,可以放到对应的 Updater 里面。

例如:

  • 1)WwText ==> Text + TextUpdater;

  • 2)WwPhoto ==> Image + PhotoUpdater。

2)自定义状态管理:

升级为 Updater 之后,如果对应的 Data 仍然是状态变量,那么我们去 get 的时候消耗依旧。 这里先解释一下,为什么我们的 Data 要加 @Observed 注解。

按官方的用法,只有多层嵌套监听的场景才需要 @Observed 注解

其实这里是因为我们的所有业务逻辑都在 ViewModel 里面,而不是按照官方方案放在 Page 里。就会存在修改无法被感知的问题,如下图所示。

▲ 图 16:为何要加 @Observed

说回正题,既然要去掉这个官方的状态管理,那么就有两处改动:

  • 1)去掉 Data 上的 @Observed 注解;

  • 2)在 View 里面不再加状态注解。

那么,如何驱动 UI 刷新?

正好,AttributeUpdater 里面可以直接拿到 attribute 对象,可以通过这个对象直接设置属性,那么问题就回到了如何感知 Data 属性的变更。

正常情况首先想到的就是 TypeScript 的动态代理,即 Proxy,鸿蒙的状态管理就是这么做的,其实现基于前文提到的 SubscribableHandler,里面用了反射,性能不足。想要不反射,要么就字符串匹配,依次调用对应函数,既然如此,不如彻底一点,直接使用静态代理。

export class BaseData


//view 的实例,由 Update 赋值和清理


ins?:INS


//用于刷新构造函数


updateConstructorFunc?: () =>void


private _width?: Length


private _height?: Length


//...


set width(width: Length|undefined) {


this._width = width this.ins?.width(width) //设置属性时直接设置到 view 上


}


get width():Length|undefined{


returnthis._width


}


//...


最后,配套 Updater 的实现如下:


export class BaseUpdater> extends AttributeUpdater<T, C> {


data?: DATA


constructor(data?: DATA) {


super() this.data = data


}


//用于批量刷新所有已设置的属性,上屏或 reuse 时触发


updateData(data?: DATA, instance?: T): BaseUpdater<DATA, T, C> {


//... this.setUpdateFunc(this.data, ins) if (ins) { this.applyAttribute(ins, this.data) this.refreshConstructor() } returnthis


}


//设置属性


applyAttribute(instance: CommonAttribute, data: BaseData) {


if (data.width || data.width == 0) { instance.width(data.width) } if (data.height || data.height == 0) { instance.height(data.height) } //...


}


}

第三版的改动总结如下:

▲ 图 17:第三版改动总结

这些改动之后,通用组件内部 UI 层的实现也需修改:


@Component


export struct ImgTextCell {


@Consume@Watch("updateData") cellData: ImgTextCellData


rootUpdater = new RowUpdater()


imgUpdater = new ImageUpdater()


textUpdater = new TextUpdater()


aboutToAppear() {


this.updateData()


}


aboutToReuse() {


this.updateData()


}


build() {


Row() { Image(ImageUpdater.EMPTY).attributeModifier(this.imgUpdater) Text().attributeModifier(this.textUpdater) }.attributeModifier(this.rootUpdater)


}


//data 与 updater 绑定


private updateData() {


this.rootUpdater.updateData(this.cellData.root) this.imgUpdater.updateData(this.cellData.img) this.textUpdater.updateData(this.cellData.text)


}


}


虽然 Cell 内部实现变化很大,但是对业务方来说,CellData 和 Data 的对外使用方法没有变化。

Data 与 Updater 为何要分开。

其实这里的 Cell 写法看起来还是有优化空间的,比如你可能会想到,为何不把 Data 和 Updater 结合到一起,比如:

export class BaseData extends BaseUpdater{


//...


}


然后 Cell 的写法就可以简化成:


@Component


export struct ImgTextCell {


@Consume cellData: ImgTextCellData


build() {


Row() { Image(ImageUpdater.EMPTY).attributeModifier(this.cellData.img) Text().attributeModifier(this.cellData.text) }.attributeModifier(this.cellData.root)


}


}

分两种情况讨论一下:

  • 1)修改 Data 内部的值:这两种写法,都是通过 AttributeUpdater 内部的 attribute 对象进行更新,都是改那个更新哪个,没毛病;

  • 2)增/删/改 Data 对象本身。

▲ 图 18:修改 Data 本身的两种情况

6.3 升级效果

1)体积降低:

以 PhotoTextCell 为例,升级之后代码编译后的体积明显降低了,仅为升级前的 9.3%。

可以再对比下编译后的内容。

ets 源码:

build() {


Row() {


Image("").attributeModifier(this.imgUpdater) Text().attributeModifier(this.textUpdater)


}.attributeModifier(this.rootUpdater)


}


ts 产物:


initialRender() {


this.observeComponentCreation2((elmtId, isInitialRender) => { Row.create(); Row.attributeModifier.bind(this)(this.rootUpdater); }, Row); this.observeComponentCreation2((elmtId, isInitialRender) => { Image.create(""); Image.attributeModifier.bind(this)(this.imgUpdater); }, Image); this.observeComponentCreation2((elmtId, isInitialRender) => { Text.create(); Text.attributeModifier.bind(this)(this.textUpdater); }, Text); Text.pop(); Row.pop();


}


可以看到编译产物少了很多层嵌套,代码结构清爽多了,我们的 hap 当时改完之后体积直接少了「十几 M」。

2)性能提升:

升级之后性能也有明显提升:

  • 1)通用组件 PhotoTextCell 的复用耗时从 4.3ms 降低到 0.9ms;

  • 2)首页的会话列表,复用的帧率由卡顿的 32 帧提升到丝滑的 118 帧。

由于鸿蒙的动态帧率机制,118 其实就是滑动时满帧。

▲ 图 19:升级前后帧率对比

7、本文小结

在鸿蒙生态快速迭代的"流沙"环境下,DataList 框架通过三重熵减机制构建了确定性开发范式,鸿蒙 DataList 的三次技术演进本质是一场对抗 API 不确定性的架构实践。

简单总结一下:

1)第一版(妥协版):基于 API 遍历属性实现基础数据绑定,虽快速启动业务开发但存在冗余调用与性能隐患;

2)第二版(适配版):引入 AttributeModifier 动态属性机制,可进行属性的动态更新,却因状态管理机制本身的性能消耗和控件包装导致代码膨胀与性能劣化;

3)第三版(优化版):创新采用自定义状态管理,剥离包装层直接操作原生控件,结合 AttributeUpdater 实现静态代理与精准属性更新,使通用组件编译体积缩减至 9.3%、复用耗时降低 79%,帧率从 32 帧跃升至 118 帧。(本文已同步发布于:http://www.52im.net/thread-4812-1-1.html

三次架构升级始终贯彻 MVDM 分层理念,通过 UI 数据层的隔离,实现业务逻辑零修改适配 UI 层巨变。包含这三次主要的升级在内,过去一年 DataList 的 UI 层经历了十多次改动(包括 API 变化与对鸿蒙了解更深入而进行的性能优化)。这些变更揭示了"流沙筑城"的核心逻辑:「表层拥抱变化,中层消化冲击,核心业务层保持稳定」。

UI 数据层在此场景中负责消化技术变化带来的冲击,允许团队:

  • 1)通过接口抽象延迟具体实现决策;

  • 2)在知识完备后通过实现替换进行渐进式优化;

  • 3)保持核心业务代码的语义稳定性。

这些最终让企业微信鸿蒙团队于 2024 年底完成了企业微信鸿蒙 NEXT 第一版「100 万行,600+页面」的开发,并成功发布。

至此,关于企业微信鸿蒙 NEXT 开发架构演进讲解完毕。

8、相关资料

[1] 微信纯血鸿蒙版正式发布,295 天走完微信 14 年技术之路!

[2] 鸿蒙 NEXT 如何保证应用安全:详解鸿蒙 NEXT 数字签名和证书机制

[3] 开源 IM 聊天程序 HarmonyChat:基于鸿蒙 NEXT 的 WebSocket 协议

[4] 大型 IM 工程重构实践:企业微信 Android 端的重构之路

[5] 企业微信的 IM 架构设计揭秘:消息模型、万人群、已读回执、消息撤回等

[6] 企业微信针对百万级组织架构的客户端性能优化实践

[7] 企业微信客户端中组织架构数据的同步更新方案优化实战

[8] 微信团队分享:微信支付代码重构带来的移动端软件架构上的思考

[9] 微信团队原创分享:微信客户端 SQLite 数据库损坏修复实践

[10] 从客户端的角度来谈谈移动端 IM 的消息可靠性和送达机制

[11] 爱奇艺技术分享:爱奇艺 Android 客户端启动速度优化实践总结

[12] 伪即时通讯:分享滴滴出行 iOS 客户端的演进过程

[13] 移动端 IM 实践:Android 版微信如何大幅提升交互性能(一)

[14] 百度公共 IM 系统的 Andriod 端 IM SDK 组件架构设计与技术实现

[15] 首次公开,最新手机 QQ 客户端架构的技术演进实践

[16] IM 开发干货分享:有赞移动端 IM 的组件化 SDK 架构设计实践

[17] 马蜂窝旅游网的 IM 客户端架构演进和实践总结

[18] 蘑菇街基于 Electron 开发 IM 客户端的技术实践

[19] IM 开发干货分享:我是如何解决大量离线消息导致客户端卡顿的


用户头像

JackJiang

关注

还未添加个人签名 2019-08-26 加入

开源IM框架MobileIMSDK、BeautyEye的作者。

评论

发布
暂无评论
一年撸完百万行代码,企业微信的全新鸿蒙NEXT客户端架构演进之路_网络编程_JackJiang_InfoQ写作社区