淘宝系App图片为什么在北京电信网络加载这么慢?

先讲讲怎么回事

不知道怎么的,大概是从19年双十一前,我在家里刷淘宝(天猫、闲鱼等)的时候,图片经常加载的特别慢,家里是北京电信的100M宽带,另外还有一张电信的手机卡,无论宽带还是4G,图片都刷的很慢,网速正常,SpeedTest测试速度都是正常的,其他App也都OK

taobao-image

部分家里使用电信宽带的同事也遇到了类似的问题,但都以为是家里网速的问题,所以也没有向淘宝进行反馈,只是在需要的时候,切成4G使用罢了

今年5月宽带到期,由于淘宝的问题,差点就换了联通的,但由于联通略贵(500M,166/月),电信(200M,600+/年),而且疫情期间,懒得折腾了,直接续费吧

于是就想着怎么解决一下淘宝图片的问题

不能抓包

作为一名Android码农,首先想到的办法就是开Charles/Fiddler去抓包看看,但是在挂上代理以后,会发现抓不到大部分的淘宝图片请求,于是猜测是淘宝使用了自己的网络库,或者是做了防抓包处理,所以看来不能直接通过抓包来判断问题所在

charles

后来在反编译淘宝代码的时候,也验证了这一问题,其底层采用的并非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……

alicdn

所以图片问题,和DNS、IPv6,根本没有任何关系!!!

反馈

  • 首先是给淘宝客服反馈:

    不知道其他路径能不能有效,反正淘宝App里面的反馈界面就跟摆设一样,没有任何作用!!

  • 反馈给在阿里工作的同学:

    问我要了一些信息以后,然后说给相关人员反馈一下,后来也没有动静了,据说很难推动去解决。。。

另外,我觉得,对于这种比较诡异的技术问题,还是得有一些有力的证据,可能才好推

被逼无奈,只好搞逆向

既然通过抓包、调试网络环境,以及正规渠道都无法解决问题,那我只好自己排查了

于是准备好这些工具,准备开干:apktooldex2jarVSCodeAndroid StudioXposed,另外就是需要一台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的

layoutinspector

因为经常刷微淘,而且图片也比较多,加载比较慢,这里以淘宝的微淘模块为例,看看它用的类名是什么

weitao

感谢淘宝没有做更深入的类名混淆,微信的部分场景,混淆就做的非常狠,几乎没法看了

于是可以发现这个用于图片显示的类就是TNodeImageView

探究一下TNodeImageView的源码

既然已经定位图片控件,那么可以直接搜Smali文件或者使用Android Studio,在加载使用dex2jar转换而来的Jar包以后,双击Shift搜索(和平时使用Android Studio搜索项目中的类一样)

个人建议首先使用Android Studio或IDEA看Jar包里的源码,如果发现不能被反编译,或者某些App做了防dex2jar转换,再考虑去看Smali,效率高的多,不太推荐用JD-GUI,不如IDEA系列IDE方便

使用VSCode直接搜索Smali文件:

smali

如果使用Android Studio/IDEA,需要把jar包先引入到工程当中:

class

这里可以发现两个方法:

  • 第一个是setImageUrl

    很明显是一个外部调用,用来设置图片URL的方法,断定这里可以获取到图片的URL

seturl

  • 第二个是onImageLoaded: 其参数为一个BitmapDrawable对象,猜测是图片加载框架的回调方法,用来设置解码后的图片

onimageloaded

尝试修改Smali——放弃

由于找到了图片路径的首个突破口,于是想着修改一下Smali,能够让淘宝直接打印图片URL,然后直接把apk装在手机上测试

经过apktool二次打包和jarsigner的一番折腾,apk是可以装上了

但是发现,很多页面出现白屏,登录也无法使用,并且Logcat会打出一下Security相关的Log

signerror

这就说明淘宝做了签名校验,这个方法不可行……

编写Xposed插件,抓取图片URL

由于以上修改源码的办法行不通,那么只能考虑Hook的办法,目前最流行的Hook技术无非就是Xposed了

关于Xposed,我就不再多说了,自行搜索即可,不过自我推荐一下自己写的一个小工具,可以简化Xposed Hook的开发,https://github.com/yuanguozheng/coderyuan-xposed-hook,如有需要,可以自行拿去使用

以下代码即为利用coderyuan-xposed-hook实现对淘宝App「微淘」模块图片组件(TNodeImageView)的setImageUrl方法Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@XPHook(
pkgName = "com.taobao.taobao",
applicationName = "com.taobao.tao.TaobaoApplication",
isUseMultiDex = true
)
class TaobaoHookImpl {

@HookMethod(
className = "com.taobao.tao.flexbox.layoutmanager.view.TNodeImageView",
methodName = "setImageUrl",
isHookBefore = false,
paramsCls = [String::class]
)
fun hookTNodeImageView(param: XC_MethodHook.MethodHookParam?) {
param?.args?.forEach {
if (it != null && it is String) {
log("TNodeUrl", it.toString())
}
}
}
}

跑起来以后,可以在Logcat中看到下面这些URL

tnodeurl

于是我把这些图片的URL抓出来,heic的图片,去掉了.heic,即可获取jpg或者png格式的数据,经过测试(PC浏览器、Android使用Glide等框架加载),结果发现如果我直接去访问这些图片,无论使用什么网络,速度都是正常的

下图是我的测试情况,网络是中国电信4G,可以看到左上角的速度,也都基本保存3MB/s以上(图片首次加载会比较慢。。。)

alicdn-images

以上测试的App,使用如下代码进行图片加载

1
2
3
4
5
6
7
override fun onBindViewHolder(holder: ImgVH, position: Int) {
Glide.with(this@MainActivity)
.load(jsonArray[position])
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE) // 强制不使用缓存
.into(holder.imageView)
}

这就说明如果使用较为常规的模式去加载淘宝上的图片,其实是没问题的,也进一步验证了淘宝实际上是对网络库进行了修改,只是不知道具体做了什么事

利用Xposed进一步研究淘宝的图片加载流程

发现ImageLoader

在TNodeImageView中,可以发现一个名为imageLoader,类型为p的类成员变量,猜测为淘宝自己的图片加载框架

imageloader-1

imageloader-2

详细查看p这个类,可以看到是一个abstract class

继续查看p的实现类,可以发现一个名为j的类

imageloader-3

于是研究j的源码,再发现内部一段代码,很明显是用来加载图片的功能,其内部的回调(onImageLoaded、onImageLoadFailed)、调用方式和命名,都和图片有关,其中关键点就是PhenixCreator

phenixcreator

分析PhenixCreator

根据PhenixCreator的包名,猜测这个框架的名字为:Phenix,暂且这么叫它

经过我对其反编译后代码的一番查看,可以发现以下两个特点:

  • 其内部结构设计和接口调用方式,和Glide略微类似,毕竟大家都是ImageLoader,或多或少有一些共性

  • 看上去和Glide一样,都可以比较方便的把网络层组件换掉,同时会内置的类似HttpURLConnection的默认网络层实现

其中,可以看到名为mImageRequest的变量,内部调用比较频繁,而且其名字也基本能够猜出作用,那就是做图片请求

mimagerequest

mImageRequest的类型为bcom.taobao.phenix.request.b)所以可以肯定b一定是用于在Phenix中管理图片请求

然而此时,来回翻看b的源码,难以发现有比较明显的网络请求痕迹,所以果断尝试另外一种思路,去寻找b的调用

这里我推荐使用Smali来搜索,但要注意相关的语法

image-request

可以发现名为com.taobao.phenix.loader.network.c的类使用了b,而且其包名包含network字样,猜测是和网络请求相关,果断查看

从内部打出的Log,可以发现,这个类确实和网络请求有一定关系,比如以下几行代码:

1
2
3
htr.a("Phenix", "received cancellation.", var1);
htr.a("Phenix", "Network Read Started.", var9);
htr.a("Phenix", "Network Connect Started.", var8);

其中不乏有很多Trace相关的操作,应该是进行统计打点相关的操作,用来收集性能相关信息的数据内容

捕获图片网络流

仔细观察代码和Log,其中以下这段代码吸引了我的注意:

read-response

看上去是对网络请求的响应数据进行读取,其中htv.a很有可能就是获取网络请求数据的方法,于是果断点进去分析,这里再看到下面的代码,很明显是对一种InputStream进行操作,断定是网络流

htv

那么htq.a这个方法又显得格外耀眼,进去看了以后,htq类的内部结构如下,看样子是一个对输入流InputStream操作的工具类

1
2
3
4
5
6
7
8
9
10
public class htq {
public static htv a(InputStream var0, a var1, int[] var2) throws Exception {
...
}

public static void a(InputStream var0, a var1, huk var2) throws Exception {
...
// 内部包含很多对Stream的操作,以及OOM的检测及异常抛出
}
}

很有可能第一a方法,其作用就是对图片网络流的读取,这里没有比较明显的痕迹来判断a方法的参数到底都是什么,所以只好借助Xposed来分析了

利用coderyuan-xposed-hook来Hook一下这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@HookMethod(
className = "tb.htq",
methodName = "a",
isHookBefore = false,
paramsClsStr = ["java.io.InputStream", "com.taobao.tcommon.core.a", "tb.huk"]
)
fun hookHTQ(param: XC_MethodHook.MethodHookParam?) {
// 遍历该方法的参数,由于是静态方法,所以不要考虑首个参数是自己所在类的实例对象
param?.args?.forEach {
if (it != null) {
log("YYY-htq", it.toString())
}
}
}

运行起来以后查看Logcat,于是有了以下Log:

1
2
3
2020-05-25 21:10:40.284 29080-29908/com.taobao.taobao I/Xposed: YYY-htq: com.taobao.phenix.compat.mtop.b@ef09a67
2020-05-25 21:10:40.284 29080-29908/com.taobao.taobao I/Xposed: YYY-htq: tb.hth@96d165b
2020-05-25 21:10:40.284 29080-29908/com.taobao.taobao I/Xposed: YYY-htq: tb.huk@e275014

此时com.taobao.phenix.compat.mtop.b这个类映入眼帘,果断查找,于是发现这个类确实为一个输入流,且内部还包含一个ParcelableInputStream类型的看似是输入流的变量a

stream

b类内部实际是对ParcelableInputStream变量a操作的封装,包含InputStream的常规操作,如:readclose

发现淘宝的网络框架

由于找到了ParcelableInputStream,出于习惯地点进去看看,果断发现新大陆,这个类位于一个非常醒目的类名下:anetwork.channel.aidl

ParcelableInputStream

于是对这个包名底下的类都查看一番

ANetwork

这还有啥说的!!!赤裸裸的网络请求框架啊!

分析ANetwork网络框架

首先要把图片的URL抓出来

为了能够验证图片请求确实走到这里了,第一步就是把ANetwork每次做请求的URL打印出来,看看有没有图片的URL

ANetwork的代码混淆程度比较低,另外就是它还引用了anet.channel包中的很多内容,看样子是一块儿的,所以如果要比较全面地理解这个库的流程,需要较为详细的阅读

我也是花了2-3天的时间去详细地看了一把这个网络库的一些代码,这里就没有什么投机取巧的办法,好好看看,基本能够理解

经过研究以后,发现anet.channel.request.Request这个类是用来保存每次请求的基本信息的,内部还包含一个Builder类,所以可以Hook它的build()方法,然后把Builder内部的URL成员变量打印出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@HookMethod(
className = "anet.channel.request.Request\$Builder",
methodName = "build",
isHookBefore = true
)
fun hookRequest(param: XC_MethodHook.MethodHookParam?) {
val instance = param?.thisObject ?: return
val oriUrlProp = instance::class.memberProperties.find { it.name == "originUrl" }
oriUrlProp?.isAccessible = true
val formatUrlProp = instance::class.memberProperties.find { it.name == "formattedUrl" }
formatUrlProp?.isAccessible = true
val ori = oriUrlProp?.getter?.call(instance)?.toString()
val format = formatUrlProp?.getter?.call(instance)?.toString()

val targetUrl = if (!format.isNullOrEmpty()) {
format
} else if (!ori.isNullOrEmpty()) {
ori
} else {
null
}
if (targetUrl?.contains("heic.alicdn.com") == true) {
log("YYY-Request-Url", targetUrl)
}
}

这里过滤了其他请求的URL,专门来看看图片请求的URL,下面是Logcat的输出:

1
2
3
4
5
6
7
8
9
10
11
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
2020-05-25 21:10:26.589 29080-29909/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/tps/i2/O1CN011EAZ5L1gJ0OScCpUv_!!0-juitemmedia.jpg_200x200q90.jpg_.heic
2020-05-25 21:10:26.743 29080-29898/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/imgextra/i1/128/TB24Xy8l5OYBuNjSsD4XXbSkFXa_!!128-0-luban.jpg_200x200q90.jpg_.heic
2020-05-25 21:10:27.219 29080-29676/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/tps/i2/O1CN011EAZ5L1gJ0OScCpUv_!!0-juitemmedia.jpg_200x200q90.jpg_.heic
2020-05-25 21:10:27.370 29080-29908/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/imgextra/i4/170/O1CN01XXyKpn1D7tt9CAYT3_!!170-0-lubanu.jpg_200x200q90.jpg_.heic
2020-05-25 21:10:27.803 29080-29674/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/imgextra/i1/128/TB24Xy8l5OYBuNjSsD4XXbSkFXa_!!128-0-luban.jpg_200x200q90.jpg_.heic
2020-05-25 21:10:28.186 29080-29912/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/imgextra/i2/34/O1CN01bMPrqD1C7c4FGfAwy_!!34-0-lubanu.jpg_960x960q90.jpg_.heic
2020-05-25 21:10:28.388 29080-29674/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/imgextra/i4/170/O1CN01XXyKpn1D7tt9CAYT3_!!170-0-lubanu.jpg_200x200q90.jpg_.heic
2020-05-25 21:10:28.978 29080-29676/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/imgextra/i2/34/O1CN01bMPrqD1C7c4FGfAwy_!!34-0-lubanu.jpg_960x960q90.jpg_.heic
2020-05-25 21:10:39.375 29080-29898/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/imgextra/i2/164/O1CN015yuRLo1D59YXcfmvo_!!164-0-lubanu.jpg_960x960q90.jpg_.heic
2020-05-25 21:10:39.380 29080-29674/com.taobao.taobao I/Xposed: YYY-Request-Url: https://heic.alicdn.com/imgextra/i2/164/O1CN015yuRLo1D59YXcfmvo_!!164-0-lubanu.jpg_960x960q90.jpg_.heic

果不其然,图片请求都是从这里构建的URL,也印证了上面的分析过程正确

尝试为ANetwork添加一个代理

由于ANetwork的设计,导致无法直接使用Charles这种工具进行抓包,所以本想考虑使用Xposed,强制设置代理,但是经过一番尝试,发现各类操作都没有效果,这里就不详细说了

而中途还发现其实这些请求并没有走普通的HTTP连接,其连接过程是首先使用Spdy进行连接,而HTTP连接只是作为一种降级方案来兜底,那么就基本不能按照常规的方式去调试了

spdy

并且ANetwork的Spdy底层还是基于NDK实现(如上图所示),并不像OKHttp这样的网络库使用纯Java来开发,所以看来,很难通过基本的逆向手段来添加代理,从而实现抓包了!

另外,还有一种可能就是:设置代理的地方,我还没找到……

不能抓包,打一些日志总是可以的

在ANetwork的内部,有很多打Log的代码,如下图所示:

alog

通读ANetwork的代码,发现ALog的调用确实很多,基本能够确定ALog就是ANetwork专用的Log工具类,在很多关键位置,都使用了ALog输出了相对详细的日志信息,只是因为打正式包的时候,关掉了Logcat开关,所以无法在Logcat中打印出来

于是Hook ALog的所有静态方法,让ANetwork的日志,都能够展现在Logcat当中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@HookMethod(
className = "anet.channel.util.ALog",
methodName = "e",
paramsClsStr = ["java.lang.String", "java.lang.String", "java.lang.String", "java.lang.Throwable", "java.lang.Object[]"],
isHookBefore = false
)
// 由于方法比较多,所以这里只列举其中一个,即ALog.e的Hook
fun hookLogcat2(param: XC_MethodHook.MethodHookParam?) {
val logStr = getLogParamsStr(param?.args) ?: return
log("YYY-ALog-E", logStr)
}

/**
* 获取Log参数,返回拼接后的字符串
*/
fun getLogParamsStr(objs: Array<Any>?, separator: String = "---"): String? {
objs ?: return null
return StringBuilder().apply {
objs.forEachIndexed { i, item ->
if (item is String) {
this.append(item)
} else {
try {
this.append(gson.toJson(item))
} catch (e: Exception) {
}
}
if (i != objs.size - 1) {
this.append(separator)
}
}
}.toString()
}

再次运行Hook,观察ALog相关的日志,各类请求相关的信息一目了然

alog-log

由于日志包含各类网络请求的信息,在翻看日志的时候,发现有以下类似内容,并且在图片加载不出来的时候,比较频繁

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
2
3
4
5
6
7
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"]
2020-05-26 10:28:40.378 29080-484/com.taobao.taobao I/Xposed: YYY-ALog-D: awcn.SessionCenter---getInternal---21646297---["u","https://img.alicdn.com/bao/uploaded/i1/TB1TXTMdKP2gK0jSZFoSuuuIVXa.jpg","sessionType","LongLink","timeout",0]
2020-05-26 10:28:40.385 29080-881/com.taobao.taobao I/Xposed: YYY-ALog-D: awcn.SessionCenter---getInternal---21646297---["u","https://img.alicdn.com/bao/uploaded/i1/TB1TXTMdKP2gK0jSZFoSuuuIVXa.jpg","sessionType","LongLink","timeout",3000]
2020-05-26 10:28:43.391 29080-881/com.taobao.taobao I/Xposed: YYY-ALog-E: awcn.SessionCenter---[Get]timeout exception---21646297---{"stackTrace":[],"suppressedExceptions":[]}---["url","https://img.alicdn.com/bao/uploaded/i1/TB1TXTMdKP2gK0jSZFoSuuuIVXa.jpg"]
2020-05-26 10:28:43.391 29080-881/com.taobao.taobao I/Xposed: YYY-ALog-D: awcn.SessionCenter---getInternal---21646297---["u","https://img.alicdn.com/bao/uploaded/i1/TB1TXTMdKP2gK0jSZFoSuuuIVXa.jpg","sessionType","ShortLink","timeout",0]
2020-05-26 10:28:44.377 29080-29994/com.taobao.taobao I/Xposed: YYY-ALog-E: anet.Repeater---[traceId:Xr65l7NnDDkDAKlUmeifke5K15904601190960813129080]end, [RequestStatistic]ret=1,statusCode=200,msg=SUCCESS,bizId=null,host=img.alicdn.com,ip=103.15.99.106,port=443,protocolType=https,retryTime=0,retryCostTime=0,processTime=3528,connWaitTime=3007,cacheTime=0,sendDataTime=191,firstDataTime=149,recDataTime=134,lastProcessTime=0,oneWayTime=4003,callbackTime=0,serverRT=0,sendSize=0,recDataSize=220008,originalDataSize=220008,extra={"firstIp":"103.15.99.106"},isReqSync=false,isReqMain=false,url=https://img.alicdn.com/bao/uploaded/i1/TB1TXTMdKP2gK0jSZFoSuuuIVXa.jpg---DGRD306---[]
2020-05-26 10:28:44.430 29080-475/com.taobao.taobao I/Xposed: YYY-ALog-E: analysis.FullTraceAnalysis---FullTraceStatistic|Xr65l7NnDDkDAKlUmeifke5K15904601190960813129080|https://img.alicdn.com/bao/uploaded/i1/TB1TXTMdKP2gK0jSZFoSuuuIVXa.jpg|img.alicdn.com|picture|null|wifi|https|0|1||1|0|0|0|0|1590412176629|1590411949467|1590412200433|3|com.taobao.tao.TBMainActivity|1590460108261|0||1590460119095|1590460119096|1590460120366|1590460120368|1590460123884|1590460123896|1590460124371|1590460124371|1590460124371|1590460124371|1590460124238|1590460124424|1590460124425|1590460124426|0|0|220008|220008|0|191|149|134|42|null|0|0|{}|200|---null---[]

重点来了,分析ALog日志

分析ALog的日志可以发现,其基本的请求过程是:

  • 建立一个UnifiedRequestTask任务,其内部是对一系列网络请求、性能统计等操作的封装

  • 接着交给SessionCenter建立或获取可用的Spdy连接

  • 最后调用Repeater,生成TraceId,并进行数据上报

根据上面的Log,可以发现建立Spdy连接的过程,又分为以下几个:

  1. 首先会建立长连接,超时时间为0

  2. 建立超时时间为3秒的长连接

  3. 建立短连接

然而出现这种情况的原因,看样子是长连接无法建立,导致了TimeoutException,从而走了兜底逻辑,触发建立了短连接,最终还是能把图片显示出来

由于长连接的超时时间定义为3秒,所以在Repeater的数据中,如果发生了TimeoutException,那么connWaitTime基本保持在3000ms以上

connwait

而正常的情况,大多数应该为0,或者1000ms之内

所以一开始白图,等一会儿能看到图的时候,基本都在3s以后了,再加上一些排队策略和解码,10s以后都是有可能的……

那么为什么不直接都走短链接?做过网络性能优化的,这个问题肯定不难解释

最关键的原因还是为了加速

众所周知TCP连接的建立过程,需要经过3握手,短连接频繁建立、断开TCP连接,增加了很多额外的开销,影响客户端请求速度的同时,会额外增加服务器的压力

然而不建立固定连接的Quic,目前来说,并不成熟,使用长连接Spdy,对阿里这种公司来说,无非是目前最优的一种解决方案

在ANetwork库中,也可以看到Quic的相关实现,可以说淘宝在这方面还是紧跟时代潮流的,且几种连接机制都可以比较灵活地切换,最终还有普通的HTTP作为兜底策略,可以兼顾性能和稳定

为什么北京电信慢?

在测试过程中,我使用了电信、联通、移动的网络,其中只有北京电信的宽带、蜂窝能够出现TimeoutException,其他家的网络都是正常的,且connWaitTime值始终处于较低的水平

所以推测出以下几点原因:

  1. 北京电信网络对长连接支持较差,可能会导致无法连接的情况

  2. 淘宝的图片CDN服务(包括heic.alicdn.com,img.alicdn.com,gw.alicdn.com等域名),北京电信线路优化上可能出现了问题

  3. 淘宝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资源还是有超时现象,希望淘宝的同学们加把劲,早日解决这一问题,让我们北京电信用户能够更愉快地剁手!哈哈哈