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