淘宝系App图片为什么在北京电信网络加载这么慢?
先讲讲怎么回事
不知道怎么的,大概是从19年双十一前,我在家里刷淘宝(天猫、闲鱼等)的时候,图片经常加载的特别慢,家里是北京电信的100M宽带,另外还有一张电信的手机卡,无论宽带还是4G,图片都刷的很慢,网速正常,SpeedTest测试速度都是正常的,其他App也都OK
部分家里使用电信宽带的同事也遇到了类似的问题,但都以为是家里网速的问题,所以也没有向淘宝进行反馈,只是在需要的时候,切成4G使用罢了
今年5月宽带到期,由于淘宝的问题,差点就换了联通的,但由于联通略贵(500M,166/月),电信(200M,600+/年),而且疫情期间,懒得折腾了,直接续费吧
于是就想着怎么解决一下淘宝图片的问题
不能抓包
作为一名Android码农,首先想到的办法就是开Charles/Fiddler去抓包看看,但是在挂上代理以后,会发现抓不到大部分的淘宝图片请求,于是猜测是淘宝使用了自己的网络库,或者是做了防抓包处理,所以看来不能直接通过抓包来判断问题所在
后来在反编译淘宝代码的时候,也验证了这一问题,其底层采用的并非OKHttp或者HttpURLConnection的方法去实现连接,而是使用了基于NDK实现的连接层(后面会讲到)
初步判断是DNS或IPv6问题(其实不是)
DNS:这种问题非常让人首先怀疑DNS出了问题,毕竟这种大厂,都会根据当地网络和服务器负载的实际情况,动态解析域名,选择最优的CDN
IPv6:另外就是回想起19年双十一前,貌似家里网络对IPv6支持进行了升级,会不会是淘宝的CDN对IPv6兼容有问题
于是保证试试看的心态,改了一下路由器的DNS,尝试过的DNS有:百度DNS(180.76.76.76)、阿里云DNS(223.5.5.5)、114(114.114.114.114)
然而发现并没有什么卵用,还是一样的慢……
想想也有道理,这种大厂App一般都会集成HttpDNS能力,随你怎么改DNS,都不会影响人家的解析
然后就是关掉了IPv6功能,发现还是没用,由于无意间通过Charles抓到几张图片的URL,ping了一下域名,发现其实淘宝的CDN还没有支持IPv6……
所以图片问题,和DNS、IPv6,根本没有任何关系!!!
反馈
首先是给淘宝客服反馈:
不知道其他路径能不能有效,反正淘宝App里面的反馈界面就跟摆设一样,没有任何作用!!
反馈给在阿里工作的同学:
问我要了一些信息以后,然后说给相关人员反馈一下,后来也没有动静了,据说很难推动去解决。。。
另外,我觉得,对于这种比较诡异的技术问题,还是得有一些有力的证据,可能才好推
被逼无奈,只好搞逆向
既然通过抓包、调试网络环境,以及正规渠道都无法解决问题,那我只好自己排查了
于是准备好这些工具,准备开干:apktool、dex2jar、VSCode、Android Studio、Xposed,另外就是需要一台ROOT后的Android手机,最好是Nexus或者Pixel
当然,也可以考虑使用VirtualXposed(https://github.com/android-hacker/VirtualXposed),这个工具的优点是不需要ROOT就可以使用Xposed的功能,且每次更新Hook不需要重启手机,但是部分场景不太稳定,个人还是喜欢用ROOT以后的手机去装原版的Xposed,不过需要Android 8.1及以下的系统
上述工具的使用,我就不再多说了,直接寻找突破口
利用LayoutInspector找到ImageView
在一台ROOT后的手机上,是可以利用LayoutInspector抓取任何一个App的View的
因为经常刷微淘,而且图片也比较多,加载比较慢,这里以淘宝的微淘模块为例,看看它用的类名是什么
感谢淘宝没有做更深入的类名混淆,微信的部分场景,混淆就做的非常狠,几乎没法看了
于是可以发现这个用于图片显示的类就是TNodeImageView
探究一下TNodeImageView的源码
既然已经定位图片控件,那么可以直接搜Smali文件或者使用Android Studio,在加载使用dex2jar转换而来的Jar包以后,双击Shift搜索(和平时使用Android Studio搜索项目中的类一样)
个人建议首先使用Android Studio或IDEA看Jar包里的源码,如果发现不能被反编译,或者某些App做了防dex2jar转换,再考虑去看Smali,效率高的多,不太推荐用JD-GUI,不如IDEA系列IDE方便
使用VSCode直接搜索Smali文件:
如果使用Android Studio/IDEA,需要把jar包先引入到工程当中:
这里可以发现两个方法:
第一个是
setImageUrl
:很明显是一个外部调用,用来设置图片URL的方法,断定这里可以获取到图片的URL
- 第二个是
onImageLoaded
: 其参数为一个BitmapDrawable
对象,猜测是图片加载框架的回调方法,用来设置解码后的图片
尝试修改Smali——放弃
由于找到了图片路径的首个突破口,于是想着修改一下Smali,能够让淘宝直接打印图片URL,然后直接把apk装在手机上测试
经过apktool二次打包和jarsigner的一番折腾,apk是可以装上了
但是发现,很多页面出现白屏,登录也无法使用,并且Logcat会打出一下Security相关的Log
这就说明淘宝做了签名校验,这个方法不可行……
编写Xposed插件,抓取图片URL
由于以上修改源码的办法行不通,那么只能考虑Hook的办法,目前最流行的Hook技术无非就是Xposed了
关于Xposed,我就不再多说了,自行搜索即可,不过自我推荐一下自己写的一个小工具,可以简化Xposed Hook的开发,https://github.com/yuanguozheng/coderyuan-xposed-hook,如有需要,可以自行拿去使用
以下代码即为利用coderyuan-xposed-hook实现对淘宝App「微淘」模块图片组件(TNodeImageView)的setImageUrl方法Hook
1 |
|
跑起来以后,可以在Logcat中看到下面这些URL
于是我把这些图片的URL抓出来,heic的图片,去掉了.heic,即可获取jpg或者png格式的数据,经过测试(PC浏览器、Android使用Glide等框架加载),结果发现如果我直接去访问这些图片,无论使用什么网络,速度都是正常的
下图是我的测试情况,网络是中国电信4G,可以看到左上角的速度,也都基本保存3MB/s以上(图片首次加载会比较慢。。。)
以上测试的App,使用如下代码进行图片加载
1 | override fun onBindViewHolder(holder: ImgVH, position: Int) { |
这就说明如果使用较为常规的模式去加载淘宝上的图片,其实是没问题的,也进一步验证了淘宝实际上是对网络库进行了修改,只是不知道具体做了什么事
利用Xposed进一步研究淘宝的图片加载流程
发现ImageLoader
在TNodeImageView中,可以发现一个名为imageLoader,类型为p的类成员变量,猜测为淘宝自己的图片加载框架
详细查看p
这个类,可以看到是一个abstract class
继续查看p
的实现类,可以发现一个名为j
的类
于是研究j
的源码,再发现内部一段代码,很明显是用来加载图片的功能,其内部的回调(onImageLoaded、onImageLoadFailed)、调用方式和命名,都和图片有关,其中关键点就是PhenixCreator
分析PhenixCreator
根据PhenixCreator
的包名,猜测这个框架的名字为:Phenix,暂且这么叫它
经过我对其反编译后代码的一番查看,可以发现以下两个特点:
其内部结构设计和接口调用方式,和Glide略微类似,毕竟大家都是ImageLoader,或多或少有一些共性
看上去和Glide一样,都可以比较方便的把网络层组件换掉,同时会内置的类似
HttpURLConnection
的默认网络层实现
其中,可以看到名为mImageRequest
的变量,内部调用比较频繁,而且其名字也基本能够猜出作用,那就是做图片请求
mImageRequest
的类型为b
(com.taobao.phenix.request.b
)所以可以肯定b
一定是用于在Phenix中管理图片请求
然而此时,来回翻看b
的源码,难以发现有比较明显的网络请求痕迹,所以果断尝试另外一种思路,去寻找b
的调用
这里我推荐使用Smali来搜索,但要注意相关的语法
可以发现名为com.taobao.phenix.loader.network.c
的类使用了b
,而且其包名包含network字样,猜测是和网络请求相关,果断查看
从内部打出的Log,可以发现,这个类确实和网络请求有一定关系,比如以下几行代码:
1 | htr.a("Phenix", "received cancellation.", var1); |
其中不乏有很多Trace相关的操作,应该是进行统计打点相关的操作,用来收集性能相关信息的数据内容
捕获图片网络流
仔细观察代码和Log,其中以下这段代码吸引了我的注意:
看上去是对网络请求的响应数据进行读取,其中htv.a
很有可能就是获取网络请求数据的方法,于是果断点进去分析,这里再看到下面的代码,很明显是对一种InputStream
进行操作,断定是网络流
那么htq.a
这个方法又显得格外耀眼,进去看了以后,htq
类的内部结构如下,看样子是一个对输入流InputStream
操作的工具类
1 | public class htq { |
很有可能第一a
方法,其作用就是对图片网络流的读取,这里没有比较明显的痕迹来判断a
方法的参数到底都是什么,所以只好借助Xposed来分析了
利用coderyuan-xposed-hook来Hook一下这个方法
1 |
|
运行起来以后查看Logcat,于是有了以下Log:
1 | 2020-05-25 21:10:40.284 29080-29908/com.taobao.taobao I/Xposed: YYY-htq: com.taobao.phenix.compat.mtop.b@ef09a67 |
此时com.taobao.phenix.compat.mtop.b
这个类映入眼帘,果断查找,于是发现这个类确实为一个输入流,且内部还包含一个ParcelableInputStream
类型的看似是输入流的变量a
b
类内部实际是对ParcelableInputStream
变量a
操作的封装,包含InputStream
的常规操作,如:read
、close
发现淘宝的网络框架
由于找到了ParcelableInputStream
,出于习惯地点进去看看,果断发现新大陆,这个类位于一个非常醒目的类名下:anetwork.channel.aidl
于是对这个包名底下的类都查看一番
这还有啥说的!!!赤裸裸的网络请求框架啊!
分析ANetwork网络框架
首先要把图片的URL抓出来
为了能够验证图片请求确实走到这里了,第一步就是把ANetwork每次做请求的URL打印出来,看看有没有图片的URL
ANetwork的代码混淆程度比较低,另外就是它还引用了anet.channel
包中的很多内容,看样子是一块儿的,所以如果要比较全面地理解这个库的流程,需要较为详细的阅读
我也是花了2-3天的时间去详细地看了一把这个网络库的一些代码,这里就没有什么投机取巧的办法,好好看看,基本能够理解
经过研究以后,发现anet.channel.request.Request
这个类是用来保存每次请求的基本信息的,内部还包含一个Builder
类,所以可以Hook它的build()
方法,然后把Builder
内部的URL成员变量打印出来
1 |
|
这里过滤了其他请求的URL,专门来看看图片请求的URL,下面是Logcat的输出:
1 | 2020-05-25 21:10:24.117 29080-29674/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/tps/TB1hPVmisKfxu4jSZPfXXb3dXXa.jpg_200x200q90.jpg_.heic |
果不其然,图片请求都是从这里构建的URL,也印证了上面的分析过程正确
尝试为ANetwork添加一个代理
由于ANetwork的设计,导致无法直接使用Charles这种工具进行抓包,所以本想考虑使用Xposed,强制设置代理,但是经过一番尝试,发现各类操作都没有效果,这里就不详细说了
而中途还发现其实这些请求并没有走普通的HTTP连接,其连接过程是首先使用Spdy进行连接,而HTTP连接只是作为一种降级方案来兜底,那么就基本不能按照常规的方式去调试了
并且ANetwork的Spdy底层还是基于NDK实现(如上图所示),并不像OKHttp这样的网络库使用纯Java来开发,所以看来,很难通过基本的逆向手段来添加代理,从而实现抓包了!
另外,还有一种可能就是:设置代理的地方,我还没找到……
不能抓包,打一些日志总是可以的
在ANetwork的内部,有很多打Log的代码,如下图所示:
通读ANetwork的代码,发现ALog的调用确实很多,基本能够确定ALog就是ANetwork专用的Log工具类,在很多关键位置,都使用了ALog输出了相对详细的日志信息,只是因为打正式包的时候,关掉了Logcat开关,所以无法在Logcat中打印出来
于是Hook ALog的所有静态方法,让ANetwork的日志,都能够展现在Logcat当中
1 |
|
再次运行Hook,观察ALog相关的日志,各类请求相关的信息一目了然
由于日志包含各类网络请求的信息,在翻看日志的时候,发现有以下类似内容,并且在图片加载不出来的时候,比较频繁
1 | YYY-ALog-E: awcn.SessionCenter---[Get]timeout exception---21646297---{"stackTrace":[],"suppressedExceptions":[]}---["url","https://img.alicdn.com/bao/uploaded/i1/TB1TXTMdKP2gK0jSZFoSuuuIVXa.jpg"] |
于是过滤TB1TXTMdKP2gK0jSZFoSuuuIVXa
字样的Log,专门查看该图片URL的相关信息,得到以下内容:
1 | 2020-05-26 10:28:40.376 29080-536/com.taobao.taobao I/Xposed: YYY-ALog-E: anet.UnifiedRequestTask---[traceId:Xr65l7NnDDkDAKlUmeifke5K15904601190960813129080]start---DGRD306---["bizId",null,"url","https://img.alicdn.com/bao/uploaded/i1/TB1TXTMdKP2gK0jSZFoSuuuIVXa.jpg"] |
重点来了,分析ALog日志
分析ALog的日志可以发现,其基本的请求过程是:
建立一个
UnifiedRequestTask
任务,其内部是对一系列网络请求、性能统计等操作的封装接着交给SessionCenter建立或获取可用的Spdy连接
最后调用Repeater,生成TraceId,并进行数据上报
根据上面的Log,可以发现建立Spdy连接的过程,又分为以下几个:
首先会建立长连接,超时时间为0
建立超时时间为3秒的长连接
建立短连接
然而出现这种情况的原因,看样子是长连接无法建立,导致了TimeoutException,从而走了兜底逻辑,触发建立了短连接,最终还是能把图片显示出来
由于长连接的超时时间定义为3秒,所以在Repeater的数据中,如果发生了TimeoutException,那么connWaitTime
基本保持在3000ms以上
而正常的情况,大多数应该为0,或者1000ms之内
所以一开始白图,等一会儿能看到图的时候,基本都在3s以后了,再加上一些排队策略和解码,10s以后都是有可能的……
那么为什么不直接都走短链接?做过网络性能优化的,这个问题肯定不难解释
最关键的原因还是为了加速
众所周知TCP连接的建立过程,需要经过3握手,短连接频繁建立、断开TCP连接,增加了很多额外的开销,影响客户端请求速度的同时,会额外增加服务器的压力
然而不建立固定连接的Quic,目前来说,并不成熟,使用长连接Spdy,对阿里这种公司来说,无非是目前最优的一种解决方案
在ANetwork库中,也可以看到Quic的相关实现,可以说淘宝在这方面还是紧跟时代潮流的,且几种连接机制都可以比较灵活地切换,最终还有普通的HTTP作为兜底策略,可以兼顾性能和稳定
为什么北京电信慢?
在测试过程中,我使用了电信、联通、移动的网络,其中只有北京电信的宽带、蜂窝能够出现TimeoutException,其他家的网络都是正常的,且connWaitTime值始终处于较低的水平
所以推测出以下几点原因:
北京电信网络对长连接支持较差,可能会导致无法连接的情况
淘宝的图片CDN服务(包括heic.alicdn.com,img.alicdn.com,gw.alicdn.com等域名),北京电信线路优化上可能出现了问题
淘宝App内部的连接池可能需要改进,处理一些类似的极端Case
总结
通过这次对淘宝App的逆向,不但让我对淘宝App加载图片的过程有了一定了解和认识,也让我学习了不少优秀的技术思想,这里列举一些
InstantPatcher
猜测为淘宝的热修复方案,对Java采取所有方法插桩的方式,App内每个方法第一行代码都时一个
IpChange
对象,用来做热修复完善的统计机制
图片框架Phenix、网络框架ANetwork,其内部都有完善、详细的性能及业务错误等信息的统计方法,便于进行性能评估和错误信息追踪
统一任务调度管理器
不知道猜的对不对,淘宝内部包含一个名为rxm的库,猜测是类似RxJava的实现,用来实现异步任务、UI回调、优先级调度等功能,可以实现统一的调度管理
HEIC图片
目前淘宝的内部很多图片都使用了HEIC格式,并且App内部还内置了NDK解码库,HEIC比WebP更省流,在省流方面,淘宝一直做的都比较先进,很久之前就应用了WebP,且CDN可以非常方便地支持各类格式转码、压缩等
ARM64支持
Google Play现在正在推进64位so支持,而很多App还停留在v7,甚至v5的级别,淘宝则是增加了ARMv8a的so支持,虽然so的体积几乎大了一倍,但是在如今64位手机CPU已经普及的情况下,换来的性能和体验提升,这么看来还是非常值得的
最后,感谢淘宝没有做过分的混淆和防Hook,可以让我丰富视野、学习进步!
后记
目前,我已经联系在淘宝工作的同学进行反馈,并把我抓到的日志发给了他们,据说他们内部的同学也可以复现类似的情况,也正在跟进处理中
但是截止我写这篇文章时,这个异常还是没有完全得到解决,使用北京电信网络刷淘宝时,白图的情况时好时坏,部分CDN资源还是有超时现象,希望淘宝的同学们加把劲,早日解决这一问题,让我们北京电信用户能够更愉快地剁手!哈哈哈