前言
微信朋友圈一直以来都是iOS开发人员争相模仿的界面,主要是其包含了丰富的iOS所需知识点,以及常用的功能模块。当然各个功能模块实现过程中的细节处理以及用户体验的优化,这才是我们开发者在日常开发中需要关注和加强的地方。
本文笔者将着重分析微信朋友圈实现的具体过程以及细节处理,争取把里面的所有知识点,模块虽小,但五脏俱全,其中最主要分析的是朋友圈的界面布局的细节处理以及性能优化。希望为大家提供一点思路,少走一些弯路,填补一些细坑。文章仅供大家参考,若有不妥之处,还望不吝赐教,欢迎批评指正。
微信朋友圈的基本架构是基于MVVM + RAC + ViewModel-Based Navigation来实现的,如若不懂,还请点击iOS 基于MVVM + RAC + ViewModel-Based Navigation的微信开发(一)。
微信朋友圈的界面控件布局和富文本显示内容,主要是使用YYKit来完成的,若对其不熟练的,请事先做好准备哦。
分析
前期在敲代码之前,需要着重分析一下整个微信朋友圈界面的实现方案,这可能是本篇文章的核心所在了(PS:这里特别提醒一下广大开发者,在实现某一个功能前,请务必确定一个实现方案,可能实现的方案千千万,这就需要开发者通过自身的理解来确定一个最优的方案来实现,而不是一昧毫无头绪的敲代码,造成后期又得重新迭代的悲剧!!!)。微信朋友圈的效果图如下(PS:万恶的马赛克...)。
Moment.jpeg
当然整体的界面的布局还是比较复杂的,前期看了UI还是挺让人望而却步的。首先,我们可以确定的是整体是利用UITableView来实现的,是不是大家已经隐隐约约感受到还是原来的配方,还是熟悉的味道,相同的tableView,变得只是Cell罢了。其次,笔者经过多日在GitHub上搜寻一些实现微信朋友圈的开发的Demo,以及做了大量的市场调研和内容对比,发现最具代表性的两个Demo分别是:gsdios/GSD_WeiXin和zhengwenming/WeChat,其他Demo大多数都是参考这两个Demo来做的,当然这两个Demo实现微信朋友圈的方法涉及到两个不同的方案,笔者就带大家简单分析一下各自的方案实现过程以及目前存在的弊端(PS:这里所谓的弊端,只是针对微信朋友圈而言的)。两者的界面模块划分如下(PS: ① 红色框 , ②:绿色框):
Moment_UI.jpeg
当然这两个Demo的实现朋友圈的 共同之处就是:将图上所示的红框①整体用一个UITableViewCell来展示。不同之处 就是:图上所示的绿框②的控件选取不同罢了。
UITableViewCell上布局子控件相对于大家肯定是小菜一碟,这里笔者就针对两个Demo在绿框②的控件的选取上做文章以及分析其目前存在的弊端。当然这两个方案目前都不是最最优化的方案,通过分析其中存在弊端,逐渐引申出比较令人合理的方案,当然笔者最终会给出自己的方案,但也许未必是最优的方案,更好的方案或许就存在大家的手中,笔者这里主要强调的是 知其然,知其所以然。话不多说,Let's Do It!
方案一 【gsdios/GSD_WeiXin】
该方案将绿框②的控件选取的是一个普通的UIView,当然内部显示文本(评论、回复、点赞)的子控件用的是UILabel来展示。虽然这种写起来比较通俗易懂,就是根据评论列表和点赞列表的内容,不断修改内部UILabel的frame来达到要求,但是却带来了如下的弊端:
布局复杂:考虑到绿框②内部子控件的布局的复杂性,其作者采用的是其自己写的SDAutoLayout来实现,笔者对SDAutoLayout用的也不是非常熟练,关于其布局代码的实现请留意其Demo的SDTimeLineCellCommentView.h/m文件即可,尽管其内部布局代码看起来还算简单,但是如果我们不使用SDAutoLayout,那么采用传统的frame布局,想想还是比较复杂的,比如:我们要计算出红框①(UITableViewCell)的高度,首先需要计算出绿框②内部所有子控件(UILabel)的尺寸,从而推算出绿框②的整体高度,最终方能确定红框①(UITableViewCell)的高度。笔者猜想该作者这里可能主要是为了凸显SDAutoLayout的自动布局的强大和便捷,好一个项庄舞剑,意在沛公呀。
动态创建:我们知道红框①(UITableViewCell)是支持复用的,这是毋庸置疑的,但是我们知道每一条说说(红框①)中包含的评论列表的个数是不一样且Cell高度也会不一样。这样就会涉及到当用户滚动朋友圈列表且cell复用的时候,绿框②内部的子控件的个数也是动态的,可能增多,又可能减少,这样就造成了动态增加或删除绿框②内部的子控件,想必大家都知道尽量不要在UITableViewCell中动态创建子控件,这是比较耗性能的,常规的做法都是事先在- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier一口气创建所有你要显示的控件,这样你只需要根据数据的属性来显示或隐藏某个子控件即可,这样就避免了动态创建子控件的场景。但是由于朋友圈列表的每条说说的评论列表个数是不能事先确定的,所以必然会存在绿框②动态创建子控件的悲剧。
不支持大数据:对于上面动态创建控件的问题,其实该作者在内部SDTimeLineCellCommentView.h/m也是做了优化处理的,其做法主要是将动态创建的子控件(UILabel)装进一个数组(commentLabelsArray)里面,这样可以减少一部分的动态创建子控件过程,但是还是会存在动态创建子控件,其主要逻辑就是根据你传进来的说说的评论列表的个数 (commentItemsArray.count)与commentLabelsArray.count比较罢了,如果前者小于等于后者,就不需要动态创建,只是对commentLabelsArray中的子控件做显示和隐藏处理即可;反之如果前者大于后者,这需要动态创建(commentItemsArray.count - commentLabelsArray.count)个子控件,然后又被加入到数组commentLabelsArray里面的过程。关键代码如下所示:
首先微信朋友圈的评论列表的个数是支持大数据的(PS:笔者瞎猜的…),那就必须确保绿框②能支持大数据的显示,显然随着评论列表的个数逐渐增多,以及UITableViewCell的不断复用,则绿框②的commentLabelsArray里面装的子控件也会越来越多且保持只增不减的趋势,这样该方案就显得比较的无力了。
- (void)setCommentItemsArray:(NSArray *)commentItemsArray{ _commentItemsArray = commentItemsArray; long originalLabelsCount = self.commentLabelsArray.count; long needsToAddCount = commentItemsArray.count > originalLabelsCount ? (commentItemsArray.count - originalLabelsCount) : 0; for (int i = 0; i < needsToAddCount; i++) { MLLinkLabel *label = [MLLinkLabel new]; [self addSubview:label]; [self.commentLabelsArray addObject:label]; } }
以上就是【gsdios/GSD_WeiXin】目前笔者发现其存在的些许问题以及谈谈笔者个人的一些理解。当然这个方案在针对大量的评论数据的处理上或许稍有吃力,但是如果当评论列表的个数是固定,例如:优酷视频的评论回复(如下图)。这个方案也不失为一个好的解决方案。所以说业务场景不同,实现方案不同,可见在敲代码之前,先思考后确定实现方案是多么重要。
YouKu_UI.png
方案二 【zhengwenming/WeChat】
该方案将绿框②的控件选取的是一个UITableView,也就是说Cell(红色框①)里面嵌套了一个UITableView,其内部子控件就是UITableViewCell来处理,后面的处理其实就跟我们平常处理UITableView的方法一样,创建TableView,遵守协议,实现协议方法… ,可能会不习惯的就是平常创建的TableView,我们都是将其添加在控制器的View上,这里只是添加在UITableViewCell上罢了,其他并无差异。内部实现说到底其实就是充分利用UITableView的特性,选取不同UITableViewCell来显示点赞列表和评论列表而已,相比于方案一来说,该方案主要发挥出了UITableView的特性,通过实现UITableView的协议方法就能实现评论和点赞列表的展示,且实现起来更加简单易懂,这可能是目前市场上绝大多数的做法。虽然外表看似毫无破绽,但是其中隐藏巨大弊端。之前笔者也利用这种方案,写过类似微信朋友圈的评论回复,详情请参考:iOS 实现微信朋友圈评论回复功能(二),但是其中存在的问题,笔者却没有叙述,实属抱歉,当然这里笔者将详述其存在弊端和产生的原因,以及让大家重新加深对UITableView的理解。弊端如下:
复用问题: 若想保证UITableView滚动流畅,纵享丝滑,就离不开UITableViewCell的复用机制(PS:这个复用机制想必大家应该已经滚瓜烂熟了,这里笔者就不在赘述),这也是UITableView的核心所在。首先正常情况下,我们可以确定的是红色框①这个UITableViewCell是能够Cell复用的,这个应该是毫无争议的。但是红色框①内部嵌套的绿色框②这个TableView中,其内部显示评论数据的UITableViewCell是否也是支持Cell的复用机制呢???可能大家的第一印象就是觉得是能的。但是这里笔者强调的是 绿色框②中CommentCell是不支持复用的!!!大家认为CommentCell能够复用的,都是认为其复用机制完全跟红色框①(MommentCell)的复用机制一样,都是会随着用户滑动的朋友圈列表,MommentCell 和 CommentCell离开都会完全离开屏幕,然后将完全离开屏幕的MommentCell 和 CommentCell存入缓存池,等到要显示Cell的时候又去缓存池根据reuseIdentifier去取MommentCell 和 CommentCell,如果取得到,就直接拿来用;如果取不到,就去创建等过程....,这里笔者只能说cell复用的概念倒是背的的挺熟,但是Cell复用的机制却不够理解。原因是:* 之所以红色框①这个MomentCell能够遵循Cell复用的机制,是因为首先其所处在的UITableView的尺寸大小是和屏幕尺寸大小一致,其次朋友圈列表能够滑动的前提就是保证该TableView的内容高度大于TableView的高度,即tableView.contentSize.height > tableView.frame.size.height,需要强调的是:①Cell能否产生复用取决于所处的tableView能否滚动,②并且Cell能够随着列表滚动完全离开所处的TableView的显示范围。结合这两点必要条件,很快可以推断出红色框①这个UITableViewCell是能够满足Cell复用的条件的。接着我们带着这两个必要条件来分析一下绿色框②这个TableView,首先明确的是,该TableView的高度是根据评论列表中每个评论内容(CommentCell)的高度总和(PS:tableView.height = cell0.height+cell1.height+cell2.height ...),这样就导致了该tableView的内容高度等于tableView的尺寸高度,即(tableView.frame.size.height = tableView.contentSize.height),所以评论列表是不会滚动的,这样就不满足条件①;其次,其tableView内部的CommentCell相对于所处的tableView的显示区域是完全暴露的,根本不满足条件②,所以最终真相大白,水落石出了,是不是豁然开朗,心情舒畅。 当然这里笔者友情提醒广大开发者千万不要误认为,只要Cell看不见就一定会产生复用的误区,主要是要明确该Cell相对于所处的TableView的显示区域是否看不见。(PS:知识点有木有),当然大家可以跑跑笔者写的这篇文章:iOS 实现微信朋友圈评论回复功能(二)所提供的Demo,来验证一下笔者的这一说法。最后,如果绿色框②这个TableView一旦失去了Cell的复用机制,用脚趾头想想也知道,那造成的后果务必会重蹈方案一存在的三个弊端的悲剧,这里笔者就不再赘述了,且笔者个人认为整体性能还不如方案一的。
方案三 【CoderMikeHe/WeChat】
该方案正是笔者目前使用的方案,该方案不仅很好的解决了方案一和方案二目前存在的弊端,而且使用起来极其简单方便以及性能优化上更是前两个方案无法比拟的,当然最主要的还是考察技巧性(黑魔法)。首先笔者在认定该方案之前,前期笔者是做了大量的准备工作,以及仔细琢磨了红色框①(PS:类似一条说说)这个整体的子模块组成。当然必须明确的是微信朋友圈的需求:绿色框②能够展示大量的评论数据(即:评论内容列表的个数>=100 ,虽然我们会很少看到某个人的某条说说,有100多个人的评论内容,而且微信的朋友圈信息流动性非常快,这种大数据的产生会很少发生,但是这种大数据不代表没有)。①考虑到微信朋友圈这一个硬需求,笔者着重从性能上出发,第一想到的就是利用Cell的复用机制来展示每条说说的评论内容;②考虑到前两个方案都是把红色框①当做一个整体来处理,且都来了类似的弊端以及针对评论内容大数据所带来的性能问题,以免重蹈覆辙,笔者将红色框①拆分为下图几个模块:一条说说(红色框①) = 组(段)头(绿色框②) + Cell(紫色框③) + 组(段)尾(黑色框④)。
Moment_Plan3_UI.jpeg
通过上图所示,虽然该方案在模块划分上是比较的分散,但是其总体带来的性能是非常客观的,大大保证了朋友圈列表滚动的流畅性。其中当然最最主要的原因还是归功于上图所示的组(段)头(绿色框②)、Cell(紫色框③)、组(段)尾(黑色框④)这三个控件都是可以通过使用TableView的数据源方法以及代理方法(代码如下)轻松实现View的复用机制的,而且都是平常开发中常用的方法,这样前面两个方案所存在的弊端就迎刃而解了。
/// UITableViewDelegate /// 组(段)头 - (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section; /// 组(段)尾 - (nullable UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section; /// UITableViewDataSource /// Cell - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
当然,组(段)头、组(段)尾的内部控件布局,想必对于大家已经是手到擒来的东西,这里笔者也不过多讨论,详情请参考笔者提供的Demo,自行领会。这里笔者主要想说的就是Cell(紫色框③),首先平常开发过程中,Cell的宽度一般是跟所处的tableView的宽度是一致的,但是微信朋友圈里面的这个评论Cell明显不是,这里笔者需要强调的是:重写是个好东西。这里的关键点就是在于重写自定义的UITableViewCell的- (void)setFrame:(CGRect)frame方法,关键代码如下:
/// PS:重写cell的设置尺寸的方法, 这是评论View关键 - (void)setFrame:(CGRect)frame{ frame.origin.x = MHMomentContentLeftOrRightInset+MHMomentAvatarWH+MHMomentContentInnerMargin; frame.size.width = MHMomentCommentViewWidth(); [super setFrame:frame]; }
当然对于这种方案(组(段)头+Cell+组(段)尾)的实现过程,笔者以前就写过一篇文章,来详细介绍这其中的关键点,详情请参考:iOS 实现微信朋友圈评论回复功能(一)。最后,笔者个人认为这个方案目前是实现类似微信朋友圈这种支持无限评论需求的最优雅的实施方案。
当然还有一种方案就是:微信官方团队做朋友圈开发的实现方案。如果这篇文章能够有幸被微信的开发人员看到,也请微信的开发人员分享一下微信官方的朋友圈的实现方案哦;或者如果笔者的这个方案正好和微信官方的如出一辙,那么也请为笔者疯狂打Call(权威认证)哦。最后笔者希望这篇文章能够为大家解除些许疑惑,带来些许帮助。
未完...待续...(PS:不说再见,我们后会有期)
本篇文章笔者主要是分析实现微信朋友圈的最优方案,希望能为大家在敲代码之前树立一个正确的参考,这样能够避免大家走许多弯路。当然微信朋友圈的技术要点和技术细节,虽然看似简单,但是细节处理非常重要,笔者在接下来的时间内,会陆续为其增加更多功能模块,以及将在开发WeChat朋友圈中用到的好用技术以及细节处理分享出来,希望提供大家一个参考,争取能为大家答疑解惑。当然也希望大家踊跃发言,共同交流,共同进步。
期待
文章若对您有些许帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议