You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
12 KiB
Markdown

2 years ago
# 28 | 怎么应对各种富文本表现需求?
你好我是戴铭。今天我要和你分享的主题是在iOS开发中如何展示富文本的内容。
在iOS开发中富文本的展示是一个非常常见的需求。为了帮助你更好地了解如何展示富文本我在今天这篇文章中会结合一个项目来跟你说说面对富文本展示需求时要怎么考虑和实现。这样你在自己的项目中也可以借鉴今天这样的实现思路和方法。
简单来说,富文本就是一段有属性的字符串,可以包含不同字体、不同字号、不同背景、不同颜色、不同字间距的文字,还可以设置段落、图文混排等等属性。
我以前做过一个 [RSS 阅读器](https://github.com/ming1016/GCDFetchFeed),阅读器启动后,需要抓取最新的 RSS 内容进行展示。RSS 里面的文章内容属于富文本是用HTML标签来描述的包含了文字样式、链接和图片。
比如RSS阅读器中的某篇文章内容如下
```
<item>
<title>涉国资流失嫌疑 东方广益6亿元入股锤子科技被调查</title>
<link>https://www.cnbeta.com/articles/tech/841851.htm</link>
<description>
<![CDATA[
<p><strong>据虎嗅得到的独家消息成都成华区监察委已立案调查“东方广益6亿元入股锤子科技北京股份有限公司”事宜认为这个项目有国有资产流失嫌疑。</strong>成华区监察委员会成立于2018年2月按照管理权限对全区行使公权力的公职人员依法实行监察履行监督、调查和处置职责。</p> <a href="https://www.cnbeta.com/articles/tech/841851.htm" target="_blank"><strong>阅读全文</strong></a>
]]>
</description>
<author>ugmbbc</author>
<source>cnBeta.COM</source>
<pubDate>Sat, 27 Apr 2019 09:46:45 GMT</pubDate>
<guid>https://www.cnbeta.com/articles/tech/841851.htm</guid>
</item>
```
文章的 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 的图片。
缓存图片的核心技术还是 NSURLProtocolSTMURLCache 最终也是使用 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 页面,看看打开速度提升了多少,预加载成功后,在弱网环境和无网络的环境都可以试试。
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。