iOS 的 UITextView 显示含有网络图片的 NSAttributedString 时,怎么样接管网络图片的下载过程?

2014-11-26 18:59:00 +08:00
 q84629462
NSString *html = @"<img src=\"http://cdn.v2ex.com/site/logo@2x.png\"/>";
NSDictionary *options = @{NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType};
NSAttributedString *string = [[NSAttributedString alloc] initWithData:[html dataUsingEncoding:NSUnicodeStringEncoding] options:options documentAttributes:nil error:&error];
//textView是UITextView
[text setAttributedText:string];
想在上面这一个过程中,实现:
把html中的img替换成小的缩略图显示,并使用TMCache缓存缩略图,在UITextView中点击缩略图后,显示原图。
搜索了两三天,依然找不到图片的下载是在NSAttributedString/UITextView这两个玩意的哪个方法里,倒是实现了点击后识别是否图片。
31965 次点击
所在节点    iDev
80 条回复
PrideChung
2014-11-26 23:12:36 +08:00
@jox LZ的目的只是想干涉图片加载的URL,这种情况下我认为没必要从Parse HTML到文字排版整个过程重做一次,而且这么干的开销也不低。
q84629462
2014-11-26 23:16:26 +08:00
@jox 分割html再解析,关键在哪里截断html字符串,会有截断html标签的可能,这也是个难题。。。
lazy loading的话,怎么获取在UITextView屏幕中显示的图片呢?
jox
2014-11-26 23:23:39 +08:00
@PrideChung lz似乎是想写v2ex的客户端?这样看来v2ex的API返回的数据也是html咯?

v2ex的帖子内容似乎只支持图片这一种html tag,那parse的话就很简单了,借助已有的工具根本不用自己做什么,拼凑一下就能用了,如果只是简单的图文混排的话就更容易了,使用text kit的话连定制都用不到,直接用就行了,我觉得完全没难度啊。

不过也可能是因为我不了解你说的这个urlprotocol的缘故,总之我的话就是parse html,然后使用text kit来排版和渲染html,html里如果有img标签的话就直接下载,然后裁剪和渲染,这样子。
jox
2014-11-26 23:29:37 +08:00
@q84629462 lazy loading手动实现还是很困难的,你要借助UIScrollView,获取当前SCrollVIew的bounds的值,假设你只使用一个text view来渲染文本,那么你就要判断text view的哪部分在当前scroll view 的bounds里,然后只渲染这部分的内容,我猜你是要写v2ex的iOS客户端是吗?如果是的话,你可以不用自己实现,使用uitableview或者collection view就行,那个自带lazy loading的机制。

虽然parse html借助工具很简单,不过你大概得了解一下词法分析和语法分析的概念,使用工具parse html得到的是一个树状的数据结构,人们叫这种数据结构为DOM(Document Object Model),你需要遍历这个数据结构然后将其转化成你需要的另外一种数据结构,在你遇到的这个问题里,你需要将DOM转化成NSAttributedString,然后再使用text kit进行排版和渲染
jox
2014-11-27 00:38:32 +08:00
“分割html再解析,关键在哪里截断html字符串,会有截断html标签的可能,这也是个难题。。。”

我又看了一下你的回复,上面我引用的这个,额,这不是难题。。。。现在的难题是你需要了解一些常识性的东西。。。

假设你得到了这样的一个字符串:

dude check this <img src="/images/huge_rack"> out!!!

你可以认为这个字符串是一段html,经过parser处理之后你得到的数据结构可能是这样的:

(tree, (
(text, "dude check this "),
(img, nil, ((src, "/images/huge_rack"))),
(text, " out!!!")
))

这个数据结构有三个子节点,第一个节点的类型是text,值是字符串"dude check this "
第二个节点的类型是img,值为空,包含一个类型为src的属性,属性的值为字符串"/images/huge_rack"

第三个节点的类型又是text,以此类推,如果你需要处理的数据都是这种结构的话,一个循环就够了,否则你就得采用深度优先的方式递归地遍历这个数据结构。

你遍历这个数据结构,并根据每个节点的类型执行相应的动作将其转化成NSAttributedString,然后交给text kit来负责排版,你负责下载图片(如果有的话)。text kit排版完之后在某个时间点会根据用户的操作会把排好版的字形渲染到屏幕上,这期间或者之后如果你从网上下载到了图片,你就要问text kit哪些地方它帮你为附件留了空,你把下载并裁剪好的图片插入或者绘制到这些空里。

额,我只能帮你到这里了,按照我说的方法的话就是这么做。
q84629462
2014-11-27 03:15:32 +08:00
@jox 回复太频繁被禁言1800秒,囧。。。
回顾一下当时提到截断时脑中想到的例子:<div>很多childnode或者很多文字</div>
这种单一node但是里面又有很长的内容,截断必然会把div拆开,这就涉及到截断后要闭合div的情况
当然这只是极端情况,我想问题会比较极端。。。
之前提到正则,我不是说用正则解析html,而是说把img src用正则替换成缩略图的src,当然这是下策
从15楼我的测试情况来看,用本地图片作为占位符也无法生效。虽然这是最快且工作量最少的解决办法,先占位,自己实现图片下载再填充到占位符。
倒是可以考虑用正则替换掉html里的img标签(同时匹配到src里的图片网址),明天试试这个方法。
最麻烦的自定义解析html并排版只能作为最后手段了。。。
q84629462
2014-11-27 03:27:39 +08:00
也就是说把html代码里的img标签记录位置并删除,变成只有文字和文字样式的nsattributedstring,再处理图片
有关15楼的测试@jox @PrideChung @gonghao 还有更靠谱的测试方法吗,我始终觉得苹果不会这么坑,在nsattributedstring生成后,还没插入到uitextview显示前就已经开始读取nsattributedstring里的图片
jox
2014-11-27 07:24:53 +08:00
--
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithData:[html dataUsingEncoding:NSUnicodeStringEncoding] options:options documentAttributes:nil error:&error];

[string enumerateAttribute:NSAttachmentAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value) {
NSTextAttachment *ment = value;
ment.image = [UIImage imageNamed:@"noavatar"];
}
}];
--

这是15L的代码,如果这里的变量html指向的数据是html字符串,那么哪来的NSTextAttachment属性?你怎么知道那个enumerate block里的value的类型一定是NSTextAttachment呢?你用UIImage的imageNamed这个方法,当然会读取图片了,但是你根本不知道ment这个指针指向的对象的类型是什么,所以你不能保证ment一定有个image属性,事实上在这里你什么都不知道,完全就是在胡搞

你要这样构造包含附件的富文本

--

NSMutableAttributedString *richText = [[NSMutableAttributedString alloc] initWithString:@"rich text with image attachment: "];
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
attachment.image = image; // suppose image exists
attachment.bounds = CGRectZero; // configure size of attachment
[richText appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];

--

然后你可以直接把这个richText丢给text view,让text view使用默认的配置来渲染这个包含附件的富文本,你也可以使用text kit或者core text来渲染。text kit会自动帮你进行排版,你只需要创建text kit的几个核心对象并把它们组合起来,然后使用richText作为text storage,剩下的都交给text kit来做。我建议你再好好看看我回复你的内容,如果你发现你并没有真正看明白我在说什么,说明你需要看文档了。

你认为的最麻烦的办法在我看来是最简单的办法,iOS平台图文混排大家都是这么做的,你检索一下iOS图文混排就知道了,当然,你也可以使用web view来渲染html,你可以使用一个html wrapper,然后使用css来控制排版,这样的话就是web kit在帮你做parse,typesetting,loading image, render这四个步骤的工作了,可是你确定要在你的应用里使用一个浏览器来渲染一小段html文本吗?如果你需要在多个地方显示html文本,比如在一个table view里,你怎么确定web view的高度是多少呢?

我看了你的另一个问有没有提供缩略图服务的帖子,我真是无语了。你知道你在要求什么吗?你在要求别人帮你从另一个地方下载图片,然后再帮你生成一个缩略图版本的图片传给你,然后猜猜what's the best part?for free!!如果是你你会接受这种要求吗?假设真有人傻到愿意提供这种服务,并且牛逼到能够很好地提供这种服务,你得到的这个缩略图需要多长时间你考虑过没?本来是Server->Client->resize的一个过程被你搞成了Server->Server->resize->Client,常识啊兄弟,你真的是一名程序员吗?你真的清楚自己在干什么吗?
q84629462
2014-11-27 16:06:43 +08:00
@jox @jox
15楼我没有附上nsstring *html的内容,但放在本帖里讨论必然是包含<img>标签的,所以创建NSAttributedString后是必然包含NSTextAttachment的。
[NSAttributedString enumerateAttribute:NSAttachmentAttributeName] 这方法的作用是循环NSAttributedString里的NSTextAttachment对象,也就是说那个block里的value就是NSTextAttachment对象
还有可以指定循环其它对象,例如
NSLinkAttributeName(NSAttributedString里的<a>标签)
详情请看NSAttributedString.h的定义
15楼的测试我可能没有描述清楚,本意是测试NSAttributedString+UIView这种显示富文本组合,是在何时加载NSAttributedString里面包含的网络图片(即<img src="http://a.com/a.jpg">)
这个测试有两种结果:
1、如果是UITextView负责加载图片,那我就可以通过[NSAttributedString enumerateAttribute:NSAttachmentAttributeName]这个方法替换掉网络图片,改为显示APP自带的图片资源作为占位符。
2、创建NSAttributedString后就加载图片,那第一个结果的替换NSTextAttachment为占位符就 行不通了
15楼的测试我自己得出的是第二个结果,依据是在15楼的代码执行顺序下,在UITextView里显示的尺寸不是noavatar这个图片的尺寸,而是被替换掉的图片尺寸,这代表NSAttributedString已经 [同步] 读取过图片了,已经获得了这些图片的尺寸后,才去执行[NSAttributedString enumerateAttribute:NSAttachmentAttributeName]这一步。所以被替换成的noavatar已经被拉扯变形了。
=============
至于另外一贴咨询有没有缩略图服务,难道要手机APP读取原图后再缩放?太耗流量太耗能,有什么好无语的?!
而且图片并不在我服务器上,而是第三方网站的图片,所以有生成第三方缩略图这个需求不是很正常吗?我只是不写产生这个需求的原因而直接到v2ex问问有没有网站提供这种服务而已,这又有什么好无语的呢。
我很清楚在干啥啊,就是想给手机省流量和节能而已,而且又可以提高APP的响应速度。
q84629462
2014-11-27 16:17:00 +08:00
@jox 我只是觉得自定义解析html和排版对我来说比较难掌握才把你的提议放在最终手段,而不是反感你的意见
由于昨晚被禁言了,所以被禁言时提交的内容没有发表出来,这里说一下:
我做的是某个论坛的APP,一边做一边学,所以帖子里的图片并不在我的服务器上
我的Android APP开发也是由此入门的并且APP已经做好了,因为只是学习阶段的产物,所以不会在v2ex放出来。
jox
2014-11-27 17:25:48 +08:00
@q84629462 网络图片下载下来之后都要自己裁剪的,会有哪个第三方的网站给你提供免费的裁剪服务啊?除非你请求的API返回的图片都是放在他们家的服务器上,并且有明确的接口提供缩略图,否则你在只有一个url的情况下只能自己做裁剪,难不成你还要parse每个图片的url看看这个url指向的网站是否提供缩略图服务?这不是搞笑呢吗?人家凭什么提供API帮你裁剪图片啊?你又不是拿着图片去别人的网站在线操作,你是想直接发送一个图片的url和指定你希望得到的尺寸,你就想让别人帮你裁剪图片再发给你?做梦呢?这些我觉得是常识,你怎么会希望有人提供这种服务呢?


如果你不使用text kit来进行排版的话,直接把html丢给text view或者web view,这些图片什么时候被加载你是没办法控制的,也许你可以像上面那个哥们说的,使用url protocol对每个发送的请求进行检查,但是你都说了是论坛的app,那么这个论坛的api返回的html里的图片就有可能来自世界上任意一台电脑,你的程序拿到的只是一个url而已,你甚至不知道这个url指向的是否是有效的资源,那么你除了尝试从这个url下载资源以外你还能做什么?你只能在图片成功下载下来之后再进行裁剪,再作为数据返回给之前发送请求的对象。绕了一圈之后既不会节省流量,也不会使你的应用响应速度变快。而且还要面对因为使用text view和web view渲染图片带来的性能问题,text view渲染html的速度非常慢,论坛返回的数据是一个列表,即使你使用table view也会至少有两三个web view存在于内存中,如果连续有几个帖子的内容很短,可能会有五六个web view存在于内存中,如果某个帖子的内容字数稍微多一点的话,再配合图片,你的应用的内存占用会很高,而且渲染的速度也不会很理想,很有可能会导致滑动时候的卡顿,这些你都要解决。

在我看来这样做很麻烦
ld0891
2014-11-28 10:27:07 +08:00
@jox 有的时候Regex还是很方便,比如我曾经做过的一个Discuz客户端,里面的帖子列表就是用正则Parse的,因为格式固定。
并且Regex是原生支持,比hpple用XPath解析快,试过一样的功能4s用Regex比hpple流畅。
貌似stackoverflow上有个很搞笑的问答就是说用Regex解析HTML的弊病,但也要看具体场合,一行Regex就能迅速解决的事情就没必要用hpple一个tag一个tag地找,特别是Discuz这种tag混乱必须要用到XPath高级语法才能正确Parse的网页。
jox
2014-11-28 10:57:08 +08:00
@ld0891 你的意思是tag混乱的网页也要使用regex来匹配吗?有些discuz的tag是嵌套的,并且block element嵌套在inline element这样的情况也有,这样的场合使用regex会很麻烦,有些语法regex根本没法匹配,regex不是用来匹配html这样的语言的,除非你确定你要匹配的html使用的tag种类非常简单并且没有嵌套且一定是语法准确的html,大多数情况下你都需要使用功能完善的parser来parse html。

NSRegularExpression是如何实现的我不清楚,hpple只是一个轻量级的libxml的wrapper,libxml是用C写的,libxml解析xml和html的速度非常快,我只用regex来匹配一般的字符串,所以不清楚regex如果用来解析它能够解析的html速度会快多少,使用hpple我觉得性能完全不是问题。
ld0891
2014-11-28 15:51:58 +08:00
@jox tag复杂只是原因之一,比如我提到的内容规则也适合Regex。

tag复杂是因为用XPath写起来太麻烦,往往要牵扯到多个child,ancestor之类的条件判断,写出来不论调试还是维护都很难。

用Regex不需要考虑tag的问题,因为Regex不是以tag做定位的。内容规则是有利条件罢了,并且大部分需要爬取的内容都是以列表呈现的,所以规则的情况比较多。tag复杂简单也决定不了用什么,综合考虑比较好。
jox
2014-11-28 16:54:26 +08:00
@ld0891 我没明白你的意思,对于html这样的语言,regex的适用范围是很有限的,而且我认为试图使用regex来匹配结构复杂的html是非常容易出错的,我不清楚你要parse的html是怎样的一种情况,regex只能匹配结构简单的html,以discuz代码为例,我用过的discuz可以处理这样的递归语法:

--

discuz_content: text discuz_code
text: "plain string"
discuz_code: [discuz_tag]discuz_content[/discuz_tag]
discuz_tag: quote
discuz_tag: font
... discuz_tag list ...
discuz_tag: img

--

经过discuz的处理,可以出现任意递归深度的html,这种情况你是没办法使用regex来处理的,regex只能处理保证不会出现递归结构的html。另外你一直在强调xpath的问题,这点也让我感到很困惑。如果需要将html整个转化成NSAttributedString,我一般只需要用到少数几个很简单的xpath,用"//body"得到了dom树之后遍历一下就是了,如果碰到一些tag会使用固定的结构,也都是一些很简单的xpath,我似乎还从来没遇到过需要从一长串html中寻找某个特殊的tag以获得特定的数据的情况,我能想到的似乎只有爬取所有的超链接,或者爬取某个页面的某个特殊tag的内容,这些都很简单啊,html/css怎么定位xpath就怎么定位呗,一般这样的tag都有id属性,很好找啊。
q84629462
2014-11-28 22:23:37 +08:00
@jox hpple怎么获得<a>的href属性呢?
<a>的TFHppleElement tagName是text,attributes是空的
ld0891
2014-11-28 22:46:04 +08:00
@jox 不太清楚你所说的html递归是什么意思。。。我一般的应用是爬图片链接和文本内容。
举个例子吧,下面这行是我曾写过的一个XPath,用来爬文本Discuz发帖内容的。
//div[@class='pcb']//td[@class='t_f']//text()[not(ancestor::blockquote)][not(ancestor::ignore_js_op)]
就是因为text()的父系节点全部都是重名,并且所有的属性和值也都是重复,没有办法简单的指定@XXX='abc'来Parse。
特别是ignore_js_op中记得是Discuz的图片缩略功能,因为引入了js又是一大堆乱七八糟的tag重名。。。
只能用XPath比较复杂的语法,Axes轴的概念, http://www.w3school.com.cn/xpath/xpath_axes.asp

这样调试就很麻烦,因为光看一个节点没有用,全HTML的父系都得兼顾。。。
ld0891
2014-11-28 22:47:13 +08:00
@q84629462 hpple有个方法叫objectForKey,专门获取属性值的。具体h文件里有。
q84629462
2014-11-28 23:02:12 +08:00
回顾了http://www.raywenderlich.com/14172/how-to-parse-html-on-ios
知道了[element objectForKey:@"href"]去href
原来是我的html文本里的a是这样的
<a href="javascript:;" dataitem="name_付之一笑" >@付之一笑</a>
看来是tfhpple无法识别这种类型的link啊
q84629462
2014-11-28 23:05:55 +08:00
@ld0891 某个论坛已经将帖子数据json化了,所以我不是用hpple来匹配网页中的数据,而是需要显示json里的帖子内容文本到uitableview里,一层回复就是一个tableviewcell

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/149498

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX