极客时间已完结课程限时免费阅读

28 | 怎么应对各种富文本表现需求?

28 | 怎么应对各种富文本表现需求?-极客时间

28 | 怎么应对各种富文本表现需求?

讲述:冯永吉

时长08:47大小8.02M

你好,我是戴铭。今天,我要和你分享的主题是,在 iOS 开发中,如何展示富文本的内容。
在 iOS 开发中,富文本的展示是一个非常常见的需求。为了帮助你更好地了解如何展示富文本,我在今天这篇文章中,会结合一个项目来跟你说说面对富文本展示需求时,要怎么考虑和实现。这样,你在自己的项目中,也可以借鉴今天这样的实现思路和方法。
简单来说,富文本就是一段有属性的字符串,可以包含不同字体、不同字号、不同背景、不同颜色、不同字间距的文字,还可以设置段落、图文混排等等属性。
我以前做过一个 RSS 阅读器,阅读器启动后,需要抓取最新的 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来预缓存 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这个类里查看。
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 代码来描述富文本的话,你还可以使用苹果官方的TextKitYYText来展示。
其中,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里的解决思路。

小结

今天我跟你介绍了如何通过 UIWebView 和 YYText 来展示富文本。
UIWebView 展示的是使用 HTML 描述的富文本。HTML 是描述富文本最简单和最常用的方式,相对于 YYText 或 TextKit 那样描述富文本的方式来说,更加简洁和标准。不过,UIWebView 的缺点也比较明显,同时创建多个 UIWebView 实例,对于内存的占用会非常大。
所以,我对于富文本展示的建议是,如果是列表展示富文本建议使用 TextKit 或者 YYText,其他情况可以选择使用 UIWebView 来展示富文本。

课后作业

使用 STMURLCache预加载你工程中的一个 Web 页面,看看打开速度提升了多少,预加载成功后,在弱网环境和无网络的环境都可以试试。
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 2

提建议

上一篇
27 | 如何用 Flexbox 思路开发?跟自动布局比,Flexbox 好在哪?
下一篇
29 | 如何在 iOS 中进行面向测试驱动开发和面向行为驱动开发?
unpreview
 写留言

精选留言(15)

  • 2019-05-14
    WKWebView不好实现这种效果吗?UIWebView已经被苹果淘汰了啊

    作者回复: WKWebView 思路类似,使用WKURLSchemeHandler就行

    14
  • 2019-05-20
    wk的NSURLProtol支持不太好,用了网上的一些方案,会导致一些奇怪的问题…。很蛋疼。现在需求里面的ajax请求都不好去拦截

    作者回复: wk 的话,可以了解下 -[WKWebViewConfiguration setURLSchemeHandler:forURLScheme:]

    4
  • Happy_Home
    2019-05-14
    老师,想问一下,你的那个阅读器项目貌似也导入了DTCoreText,可是整个项目中好像并没有运用到,DTCoreText好像也是可以直接将HTML字符串转化为富文本,是出于什么原因没有使用而选择UIWebView的呢
    3
  • Ace
    2019-05-14
    YYKit的想法和方案确实不错,不过曾经在实践的时候,遇到某些不是很常见的语言里面的字符部分渲染不出来。。。不知道大家有没有遇到过,这个问题一直没有解决,后面就放弃使用YYkit了。
    3
  • Ant
    2019-05-15
    同问为啥不用 WKWebView

    作者回复: 可以用 WKWebView,思路类似,实现上用 WKURLSchemeHandler

    2
  • Geek_e1d503
    2019-05-15
    能否有一堂苹果审核的课,梳理常见被拒原因?
    2
  • 谭鹏
    2019-05-15
    YYKit 里面的 封装runloop 和GCD的部分 非常好用
    2
  • Tristan
    2019-05-15
    NSMutableAttributedString * attrStr = [[NSMutableAttributedString alloc] initWithData:[htmlString dataUsingEncoding:NSUnicodeStringEncoding] options:@{ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType } documentAttributes:nil error:nil];
    共 2 条评论
    1
  • 吹啊吹吹
    2019-05-14
    铭哥,github上STMURLCache的引用的文件有点多啊,需要全部使用?
    1
  • Sun0010
    2019-08-09
    - (NSCachedURLResponse *)localCacheResponeWithRequest:(NSURLRequest *)request if (isExist == nil) { [self.responseDic setValue:[NSNumber numberWithBool:TRUE] forKey:request.URL.absoluteString]; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error) { cachedResponse = nil; } else { NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"%f",[date timeIntervalSince1970]],@"time",response.MIMEType,@"MIMEType",response.textEncodingName,@"textEncodingName", nil]; BOOL resultO = [dic writeToFile:otherInfoPath atomically:YES]; BOOL result = [data writeToFile:filePath atomically:YES]; if (resultO == NO || result == NO) { } else { } cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data]; } }]; [task resume]; return cachedResponse; } return nil; 因为 [session dataTaskWithRequest:request completionHandler:^]是异步请求,所以 cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data]; 其实是没什么效果?
    展开
  • findU
    2019-07-04
    老师,WKURLSchemeHandler是再11.0之后才支持的API,而且如果用WK使用NSURLProtocol做静态资源缓存的话需要处理苹果的私有API(存在一定的被拒风险)。
  • tomcat
    2019-06-06
    一个自定义的NSURLProtocol处理一种事情,如果想拦截一个网络请求,然后经过层层自定义NSURLProtocol,功耗和效率有比较大的影响吗?
  • xilie
    2019-05-29
    长列表 FPS 性能优化的问题,看到都 9102 年了,还好多文章介绍计算高度并缓存的方案,Self-Sizing 还没普及吗?大家都还手动算的吗?性能更好?
    共 2 条评论
    1
  • xilie
    2019-05-29
    看了下 Github YYText 的代码是 3 年前的,几百个没关的 issues,没维护了吗?这个库还适合现在用吗?
    共 1 条评论
  • 2019-05-14
    我想问下关于富文本里包含<img scr='http:xxx'> 这种该怎么处理呢,尤其是在长列表里