# 28 | 怎么应对各种富文本表现需求? 你好,我是戴铭。今天,我要和你分享的主题是,在iOS开发中,如何展示富文本的内容。 在iOS开发中,富文本的展示是一个非常常见的需求。为了帮助你更好地了解如何展示富文本,我在今天这篇文章中,会结合一个项目来跟你说说面对富文本展示需求时,要怎么考虑和实现。这样,你在自己的项目中,也可以借鉴今天这样的实现思路和方法。 简单来说,富文本就是一段有属性的字符串,可以包含不同字体、不同字号、不同背景、不同颜色、不同字间距的文字,还可以设置段落、图文混排等等属性。 我以前做过一个 [RSS 阅读器](https://github.com/ming1016/GCDFetchFeed),阅读器启动后,需要抓取最新的 RSS 内容进行展示。RSS 里面的文章内容属于富文本,是用HTML标签来描述的,包含了文字样式、链接和图片。 比如,RSS阅读器中的某篇文章内容如下: ``` 涉国资流失嫌疑 东方广益6亿元入股锤子科技被调查 https://www.cnbeta.com/articles/tech/841851.htm 据虎嗅得到的独家消息,成都成华区监察委已立案调查“东方广益6亿元入股锤子科技(北京)股份有限公司”事宜,认为这个项目有国有资产流失嫌疑。成华区监察委员会成立于2018年2月,按照管理权限对全区行使公权力的公职人员依法实行监察,履行监督、调查和处置职责。

阅读全文 ]]>
ugmbbc cnBeta.COM Sat, 27 Apr 2019 09:46:45 GMT https://www.cnbeta.com/articles/tech/841851.htm
``` 文章的 HTML 代码就在上面 RSS 中的 description 标签里。解析出 RSS 中所有文章的 HTML 代码,并将它们保存到本地数据库中。 接下来,如何展示 HTML 内容呢?当时,我的第一反应就是使用 WebView 控件来展示。 ## WebView 使用 WebView 显示文章只需要创建一个 UIWebView 对象,进行一些基本滚动相关的设置,然后读取 HTML 字符串就可以了,具体实现代码如下: ``` self.wbView = [[UIWebView alloc] init]; self.wbView.delegate = self; [self.view addSubview:self.wbView]; [self.wbView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.right.bottom.equalTo(self.view); }]; self.wbView.scalesPageToFit = YES; // 确保网页的显示尺寸和屏幕大小相同 self.wbView.scrollView.directionalLockEnabled = YES; // 只在一个方向滚动 self.wbView.scrollView.showsHorizontalScrollIndicator = NO; // 不显示左右滑动 [self.wbView setOpaque:NO]; // 默认是透明的 // 读取文章 html 字符串进行展示 [self.wbView loadHTMLString:articleString baseURL:nil]; ``` 和 UIWebView 的 loadRequest 相比,UIWebView 通过 loadHTMLString 直接读取 HTML 代码,省去了网络请求的时间,展示的速度非常快。不过,HTML 里的图片资源还是需要通过网络请求来获取。所以,如果能够在文章展示之前就缓存下图片,那么无需等待,就能够快速完整地展示丰富的文章内容了。 那么,我应该使用什么方案来缓存文章中的图片呢? 在 Cocoa 层使用 NSURLProtocol 可以拦截所有 HTTP 的请求,因此我可以利用 NSURLProtocol 来缓存文章中的图片。 接下来,我再来和你说说,**如何用我写的一个 Web 页面预加载库** [**STMURLCache**](https://github.com/ming1016/GCDFetchFeed/blob/master/GCDFetchFeed/GCDFetchFeed/STMURLCache.m)**来预缓存 HTML 里的图片。**这个库你也可以应用到自己项目中。 **首先**,我需要从数据库中取出所有未缓存图片的文章内容 HTML。实现代码如下: ``` [[[[[SMDB shareInstance] selectAllUnCachedFeedItems] subscribeOn:[RACScheduler schedulerWithPriority:RACSchedulerPriorityDefault]] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSMutableArray *x) { // 在数据库中获取所有未缓存的文章数据 x NSMutableArray *urls = [NSMutableArray array]; if (x.count > 0) { self.needCacheCount = x.count; for (SMFeedItemModel *aModel in x) { // 将文章数据中的正文内容都存在 urls 数组中 [urls addObject:aModel.des]; } } ... }]; ``` 如上面代码所示,在数据库中获取到所有未缓存文章的数据后,遍历所有数据,提取文章数据中的正文 HTML 内容保存到一个新的数组 urls 中。 **然后**,使用 STMURLCache 开始依次预下载文章中的图片进行缓存。实现代码如下: ``` [[STMURLCache create:^(STMURLCacheMk *mk) { mk.whiteUserAgent(@"gcdfetchfeed").diskCapacity(1000 * 1024 * 1024); }] preloadByWebViewWithHtmls:[NSArray arrayWithArray:urls]].delegate = self; ``` STMURLCache 使用 preloadByWebViewWithHtmls 方法去预缓存所有图片,在 STMURLCache 初始化时,会设置 UserAgent 白名单,目的是避免额外缓存了其他不相关 UIWebView 的图片。 缓存图片的核心技术还是 NSURLProtocol,STMURLCache 最终也是使用 NSURLProtocol 来缓存图片的。NSURLProtocol 是一个抽象类,专门用来处理特定协议的 URL 数据加载。你可以使用自定义 URL 处理的方式,来重新定义系统 URL 加载。STMURLCache 缓存图片的具体实现代码,你可以在 [STMURLProtocol](https://github.com/ming1016/GCDFetchFeed/blob/master/GCDFetchFeed/GCDFetchFeed/STMURLProtocol.m)这个类里查看。 STMURLProtocol 会在所有网络请求的入口 canInitWithRequest 方法中加上过滤条件,比如 STMURLCache 在初始化时设置 UserAgent 白名单,过滤代码如下: ``` // User-Agent来过滤 if (sModel.whiteUserAgent.length > 0) { // 在 HTTP header 里取出 User Agent NSString *uAgent = [request.allHTTPHeaderFields objectForKey:@"User-Agent"]; if (uAgent) { // 不在白名单中返回 NO,不会进行缓存 if (![uAgent hasSuffix:sModel.whiteUserAgent]) { return NO; } } else { return NO; } } ``` UserAgent 白名单过滤会通过 request 的 allHTTPHeaderFields 获取到当前网络请求的 UserAgent,然后和已经设置的 UserAgent 白名单做比较:如果在白名单中就进行缓存;否则,就不会缓存。 STMURLProtocol 还可以根据域名进行过滤,这样可以灵活、精确地控制缓存范围。如果你设置了域名白名单,那么只有在白名单里的域名下的网络请求才会执行缓存,过滤代码如下: ``` //对于域名白名单的过滤 if (sModel.whiteListsHost.count > 0) { id isExist = [sModel.whiteListsHost objectForKey:request.URL.host]; // 如果当前请求的域名不在白名单中也会返回 NO if (!isExist) { return NO; } } ``` 如代码所示,当前网络请求的域名可以通过 request.URL.host 属性获取到,获取到网络请求的域名后,再去看域名白名单里是否有,如果有就缓存,没有就返回 NO,不进行缓存操作。 在 canInitWithRequest 方法中满足缓存条件后,开始缓存的方法是 startLoading。startLoading 方法会判断已缓存和未缓存的情况,如果没有缓存会发起网络请求,将请求到的数据保存在本地。如果有缓存,则会直接从本地读取缓存,实现代码如下: ``` // 从缓存里读取数据 NSData *data = [NSData dataWithContentsOfFile:self.filePath]; NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:[otherInfo objectForKey:@"MIMEType"] expectedContentLength:data.length textEncodingName:[otherInfo objectForKey:@"textEncodingName"]]; [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; // 使用 NSURLProtocolClient 的 URLProtocol:didLoadData 方法加载本地数据 [self.client URLProtocol:self didLoadData:data]; [self.client URLProtocolDidFinishLoading:self]; ``` 如代码所示,STMURLProtocol 先通过缓存的路径获取到缓存的数据,再使用 NSURLProtocolClient 的 URLProtocol:didLoadData 方法加载本地缓存数据,以减少网络请求。 显示文章内容时使用 NSURLProtocol,对于那些已经缓存过图片的文章就不用发起图片的网络请求,显示的速度跟本地加载显示速度一样快。 虽然通过 URLProtocol 重新定义系统 URL 加载的方式,来直接读取预缓存提升了加载速度,但在长列表的 Cell 上展示富文本,就需要性能更高、内存占用更小的方法。那么接下来,我们再看看除了 UIWebView 还有没有什么方法可以展示富文本呢? 当然还有了。 在长列表这种场景下,如果不用 HTML 来描述富文本的话,想要使用原生 iOS 代码来描述富文本的话,你还可以使用苹果官方的[TextKit](https://developer.apple.com/documentation/appkit/textkit)和 [YYText](https://github.com/ibireme/YYText)来展示。 其中,YYText 不仅兼容 UILabel 和 UITextView,在异步文字布局和渲染上的性能也非常好。所以接下来,我们就一起看看 YYText是如何展示富文本的吧。 ## YYText 集成 YYText 到你的App非常简单,只需要在 Podfile 中添加 pod ‘YYText’ 就可以了。下面代码展示了如何展示图文混排的富文本: ``` NSMutableAttributedString *text = [NSMutableAttributedString new]; UIFont *font = [UIFont systemFontOfSize:16]; NSMutableAttributedString *attachment = nil; // 嵌入 UIImage UIImage *image = [UIImage imageNamed:@"dribbble64_imageio"]; attachment = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:image.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString: attachment]; // 嵌入 UIView UISwitch *switcher = [UISwitch new]; [switcher sizeToFit]; attachment = [NSMutableAttributedString yy_attachmentStringWithContent:switcher contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString: attachment]; // 嵌入 CALayer CASharpLayer *layer = [CASharpLayer layer]; layer.path = ... attachment = [NSMutableAttributedString yy_attachmentStringWithContent:layer contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString: attachment]; ``` 如代码所示,YYText 对于富文本的图文混排使用的是自定义的 NSMutableAttributedString 分类,自定义分类不光简化了 NSMutableAttributedString,还增加了功能,除了图片外,可以嵌入 UIView 和 CALayer。 通过上面 YYText 描述富文本的代码,你会发现原生代码描述富文本跟 HTML 比,既复杂又啰嗦。HTML 代码更易读、更容易维护,所以除了长列表外,我建议你都使用 HTML 来描述富文本。 对于 UIWebView 内存占用高的问题,你可以考虑使用 HTML 代码转原生代码的思路解决。比如,你可以参考我以前做的将 HTML 代码转原生代码的示例项目 [HTN](https://github.com/ming1016/HTN)里的解决思路。 ## 小结 今天我跟你介绍了如何通过 UIWebView 和 YYText 来展示富文本。 UIWebView 展示的是使用 HTML 描述的富文本。HTML 是描述富文本最简单和最常用的方式,相对于 YYText 或 TextKit 那样描述富文本的方式来说,更加简洁和标准。不过,UIWebView 的缺点也比较明显,同时创建多个 UIWebView 实例,对于内存的占用会非常大。 所以,我对于富文本展示的建议是,如果是列表展示富文本建议使用 TextKit 或者 YYText,其他情况可以选择使用 UIWebView 来展示富文本。 ## 课后作业 使用 [STMURLCache](https://github.com/ming1016/GCDFetchFeed/blob/master/GCDFetchFeed/GCDFetchFeed/STMURLCache.m)预加载你工程中的一个 Web 页面,看看打开速度提升了多少,预加载成功后,在弱网环境和无网络的环境都可以试试。 感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。