JChat Swift 演进过程 - 打造一款通用的 IM UI 库

内容简介:

近年来随着APP应用社交化的发展,越来越多的应用开始接入即时通讯 SDK ,以便快速实现社交功能。同时开发者希望有一款通用的 IM UI 来避免重复开发,提高开发效率。本次分享将会结合极光推送公司JChat产品的开发经验,介绍如何优雅地实现一款通用的 IM UI 库,并谈谈开发过程中遇到的坑及相应的解决方法,以及如何减少重复开发和增加代码的可扩展性。

演讲 PPT 点这下载

最近半年的时间里,我从 SDK 开发转到 IM UI 库的开发(其实就是一个完整 IM APP),也完全过度到使用 Swift 进行开发。直到某天,领导对我说:“你去做个关于我们极光 IM 的演讲吧”,当时我就懵逼了,看来唯有谈谈我最近半年的工作心得了,我作为一位执行力比较强的小跟班,领导的话我肯定是服从安排,所以很直接就把主题定为:

如何使用 极光 IM JMessage

嗯...目测这样直接打广告,显得水平就不太高,主办方也无情地拒绝了,虽然我们是同一家公司的,但他们还是很坚守原则的,所以只能稍微把演讲主题改了一下:

如何打造一款通用的 IM UI 库

UI 库与 APP 的差别

开发者 VS 用户

在座的各位,估计大部分都是 iOS 开发者,我们当中,可能较多的人都是从事 APP 开发,估计也有部分是从事 SDK 开发的同学。我们都知道,APP 主要面向的用户,用户更加注重的是应用的使用和功能,而SDK 面向的则是开发者,开发者关注的则是 SDK 具有哪些功能和这些功能是如何去实现和使用的。同样,UI 库其实也可以说成是 SDK,它只是针对界面层的 SDK,它面向同样是开发者,有着和 SDK 类似的特点。

重复造轮子

“Stop Trying to Reinvent the Wheel”

因为我所在的部门本身就是开发 IM SDK 的,去写这么一个 UI 库的主要目的还是避免开发者重复开发轮子,毕竟时间可贵,珍惜生命,少写重复代码。在我们实际开发中,我们不应该重复造轮子,听好几个朋友说过,他们公司不允许使用任何的第三方库,这个不知道是出于什么原因,但个人感觉这是一种浪费生命的行为,对于那些优秀的开源框架,比如说像 AFNetWorking,当我们需要使用相关的功能时,我们完全有理由去拿来直接使用,而不是花大量的时间去开发新的轮子,软件是有生命周期的,可能待你把所以有轮子造好,你的软件就已经可以和市场 say goodbye 了,并且轮子造好时,还需要花费大量的人力和时间去进行测试和验收。

作为一个开发者,在工作上面压力很多时候都不会轻,在我们有限的开发生涯中,应该如何有效利用时间来做一些更有价值的事情,而且不是浪费在造轮子上。显然,罗马不是一天建成的,也不是一个人建成的。我们需要学会把自己和别人写的代码组织起来,高效地利用,并以此为基础构建软件。如何优雅地实现一款通用的组件,在方便自己工作的同时,给其它开发者带来方便,这就是我今天想讲的主题,下面都会以 IM UI 库为例进行演讲。

可兼容性

作为一款通用的 IM UI 库,首先兼容性是必不可少的,它不是单纯的一个 APP,它应该更具有通用性,兼容各类型的 IM SDK,而不单单是针对自己公司的产品,最理想的姿势当然是支持所有类型的 IM SDK,但理想都是美好的,现实却总是会时不时打击下我们。这里就先不管能不能支持所有的 IM SDK,这是一个 target,前方路的还很漫长,我们尚需努力。下面将从 JChat 的架构设计来你介绍整个 UI 库的兼容性实现和解耦过程。

JChat 架构设计

旧 JChat 消息处理

图片.png

在接手 JChat Swift 开发之前,有一个年久失修的 OC 版本的 JChat,它在消息处理层上,是直接使用 SDK里面的 JMSGMessage 作为整个应用的消息体对象来使用(这里说明下,这里的 SDK 指的是我们极光 IM SDK,下面不重复说明),这样做,也不是说不可以,多么简单明了,但是,如果某一天,领导说:“这个 IM SDK 满足不了我们当前业务,我们需要更换成 xxxx IM SDK,下周出新版本”。

笑着活下去

估计如果是新来接手这个项目的人,肯定懵逼了,我想整个应用的业务逻辑层都需要去改,这其中到底有多苦逼,只有自己去真正去体验一把,试过才能知道其中有多艰辛。希望位都不会遇到这种神项目,如果真的碰到了,我也只能对你说一句:“兄弟,笑着活下去吧”。

程序员为何为难程序员

同时也希望所有人尽量不要写这种代码,说不定某天刚好与接手你项目的同事或前同事相遇街角,狭路相逢,说不定你就需要躺着出来。

在就里只是和大家开个玩笑,但并不是不可能的,好了,下面回到正题。

JChat Swift 消息处理

图片.png

在 JChat Swift 里面,不再使用这种高耦合的方式,而是在上层再封装一层与 IM SDK 无关的 JCMessage,只保留消息展示所需的信息,在应用的业务逻辑层里面,都只依赖于 JCMessage,这样不管你使用的极光的 IM 也好,还是环信的 IM 也好,或者其它的 IM SDK,只需要去修改从 xxxMessage -> JCMessage 的解析方法就可以了,其它的业务逻辑就基本不需要去改动了。

同时,为了提供更好扩展性,我们应该提供一个 JCMessageType 协议:

protocol JCMessageType: class {    
    var msgId: String { get }
    var content: JCMessageContentType { get }
    var options: JCMessageOptions { get }
    var targetType: MessageTargetType { get }
    // ...
}
图片.png

这样不管是 JCMessage 还是 XMessage, 只需要实现 JCMessageType 协议:

class JCMessage: NSObject, JCMessageType {
    init(content: JCMessageContentType) {
        self.content = content
        self.options = JCMessageOptions(with: content)
        super.init()
    }
    open var msgId = ""
    open var targetType: MessageTargetType = .single
}

那么在原来的逻辑上都不需要改动,开发者还可以自定义一些字段或者做一些其它的扩展,使用的自由度更大。

这里虽然是 IM UI 库的实现为,但其实在其它地方上也是同理的,比如使用某些第三方闭源包时,在上层提供一层稳定的 api,使上层的业务逻辑保持稳定,当 SDK Api 或者内部实现发生变动时,我们只需要在底层的实现去做适配就可以了,上层业务层就不会受影响,把受影响范围控制在最小。

消息类型的扩展

在做 IM 应用的时候,变动最多的莫过于各种类型的消息添加了,比如今天只需要最简单的文本消息、语音消息和图片消息,过两天就需要你添加片名消息、阅后即焚消息等。所以在 IM UI 库中,如何设计各种消息体的实现就很重要了。

protocol JCMessageContentType: class  {    
    // 消息体展示的大小
    func sizeThatFits(_ size: CGSize) -> CGSize   
    // 消息类型
    static var viewType: JCMessageContentViewType.Type { get }
}

protocol JCMessageContentViewType: class {
    init()
    // 渲染消息
    func apply(_ message: JCMessageType)
}

消息的展示,其实只需要知道消息内容和类型就可以绘制出来,所以在这里定义了 JCMessageContentType 协议和 JCMessageContentViewType 协议,消息 Content 实现 JCMessageContentType 时需要实现 sizeThatFits 方法来返回 content 的 size,来确定它在界面上显示的大小,同时需要定义它的 ContentViewType,就是它的类型。消息的展示 View 实现 JCMessageContentViewType 时,需要实现 apply 方法,通过 apply 方法来把 message 的信息渲染到界面上。

在 ChatViewLayout(MessageCell 布局文件) 中,通过 JCMessageContentType 的 sizeThatFits 来获取 MessageCell 的大小,在 MessageCell 中,则是通过 JCMessageContentViewType 的 apply 来设置展示的内容,不管你是什么类型的消息,只要你符合协议的要求,ChatView 就可以把 Message 渲染出来,这样就可以降低 ChatView 与 Message Type 的耦合,使用者就可以更快更方便地实现各种类型的消息,并且不需要原来的代码进行改动。

API 设计

最小化原则

  1. 尽可能少的接口来完成任务

  2. 尽可能少的访问权限

ChatView

下面说下整个 UI 库最复杂的界面 ChatView 的 API。

ChatView.png
public func insert(_ newMessage: JCMessageType, at index: Int)
public func insert(contentsOf newMessages: Array<JCMessageType>, at index: Int)

public func append(_ newMessage: JCMessageType)
public func append(contentsOf newMessages: Array<JCMessageType>)
    
public func update(_ newMessage: JCMessageType, at index: Int)
    
public func removeAll() 
public func remove(at index: Int)
public func remove(contentOf indexs: Array<Int>)

基于 UI 库的特点,相较于 app 开发,需要更着重地考虑 API 的设计。你标记为 public 的内容将是使用者能看到的内容。提供什么样的 API 在很大程度上决定了其他的开发者会如何使用该 UI 库。

在 API 设计的时候,从原则上来说,我们一开始可以提供尽可能少的接口来完成必要的任务,这有利于控制整个 UI 库的复杂程度。 在 ChatView 中我们只提供必须的添加、删除和修改消息的接口,只需要向 ChatView 传递正确 JCMessageType,ChatView 就会负责在界面上渲染出来,使用者不需要再去关心 ChatView 的显示过程,只需要保证传递正确 JCMessageType 序列就可以了。最少的接口也减少了开发者的学习成本,减少不必须的歧义,如果后期需要,开发者可以对其进行二次开发,添加所需的公共方法,或者把原有的一些私有方法设置成公有。

OC 与 Swift 命名兼容

OC 方法名兼容 Swift 调用

JChat 性能优化

缓存

在 JChat Swift 实现中,为了提高性能,很多地方都添加了缓存,就像缓存计算出来的 Message Cell 的 size、图片加载资源加载等,这里以 JChat 主题管理功能为例,详细说下。

JChat 主题管理 UML

JChat 的主题管理功能是通过 bundle 来管理图片,不同的主题皮肤的图片资源放在对应的 bundle 里面,共同的资源放在默认的 bundle 中,当监听到主题切换时,只需要切换图片访问路径并刷新界面就可以了。

关于 JChat 主题管理功能的实现的详细可以参考:

再谈 Swift 换肤功能

在 JChat 中,聊天的时候,较多界面上都有进行频繁的刷新,就如聊天列表或消息列表,这里就会有大频率的重复访问本地的图片的,特别是当用户长时间没有登录,积累了大量离线消息时,下次登录时,会一次性收到大量的离线消息,在上层刷新频繁就会非常大了,一些应用里面的默认图片的访问量可能就会比较大,我们通过文件的方式来加载本地图片时,就会存在性能的问题,所以在访问图片资源的时候,如果该图片如果已经缓存在内存中时,我们就从缓存中读,如果 缓存中没有,则从硬盘里面读取,并把该图片缓存到内存中,这样的话,资源图片实质上都只加载一次,而不需要多次去加载。需要注意的是,因为图片一直缓存在内存中时,就需要监听系统是否有内存警告,如果系统发出内存警告时,就需要手动去清空缓存,避免应用 crash。

其它

  1. 离屏渲染(Offscreen-Rendered)
  2. 图层混合(Blended Layers)
  3. 复杂界面不使用 autolayout
  4. ...

结束语

简单的小结下,虽然整个演讲都以 IM UI 为例,但实际上,在其它方面的开发也是类似套路的,以不变应万变,万变不离其宗,程序开发,最重要的是思路。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,803评论 25 707
  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一种新的协议。它实...
    香橙柚子阅读 23,798评论 8 183
  • 书开了 页跑了 逃逸的墨点追上了每缕阳光 连接思考的眼和所有的光明 只有你的名字 在吸引注意
    心种阅读 149评论 0 0
  • 还能想起你第一天上学路上的所遇所见吗 还是只记得那份激动紧张了。 这本书,最先吸引我的是作者名和其中给人稚嫩淳朴让...
    阿佛阅读 2,161评论 0 0
  • 偶然,办公室里谈到送花给女朋友。一个男同事说,他曾经送花过。那一年,没有互联网,没有微信包月订花,没有淘宝。他打了...
    若有一天阅读 476评论 0 0