Android弱网优化 —— 都要卫星互联网了,谁给我限速体验2G

2 阅读3分钟

随着互联网的发展,网速是越来越快,但是弱网环境仍然存在。除了信号不好的地方外,Wi-Fi边缘区域以及人为路由器限速也是弱网环境造成的原因。

作为一名App的开发者,我们无法控制用户的网络环境,但可以通过技术手段最大程度降低弱网带来的影响,让产品在各种网络条件下都保持“可用甚至好用”。今天我们就从多个维度,系统地探讨在弱网环境下,App应该做些什么。

数据请求层面:减少、压缩与优化

在弱网环境中,请求次数越多、数据量越大,用户等待时间就越长。首先,可以通过请求合并来减少网络往返次数。例如,将多个接口整合为一个批量接口,比如通过RxJava去合并多个请求或使用协程。其次,在传输层面可以启用gzip或brotli压缩,大幅降低数据体积,尤其是JSON数据的压缩效果非常明显。

UI层面:优雅降级与占位策略

弱网环境下最容易影响用户感知的,其实是UI。“空白”和“卡顿”是用户最无法接受的体验。因此我们需要做两件事:降级显示和占位填充。例如在加载用户头像时,可以在网络图片加载之前,先用userId的首字母配合一个灰色背景生成默认头像。这样即使图片请求失败或延迟,界面也不会出现“空白块”。在极弱网情况下,可以进一步降级:

  • 不加载高清图片,改为缩略图
  • 不自动播放视频或GIF
  • 延迟非关键资源加载 像Glide、Coil等图片加载库提供的placeholder和error图机制,本质上就是这种“体验兜底”的体现。

缓存机制:用“旧数据”对抗“慢网络”

缓存是弱网优化中最关键的一环之一。比如使用某开源项目github.com/dora4/dcach… 实现离线状态下优先加载缓存数据。在进入页面时,可以优先展示本地缓存的数据(上一次请求结果),然后再发起网络请求更新数据。更新完成后,通过DiffUtil等机制进行局部刷新,而不是整页重绘,从而保证UI流畅。

private fun submitSessions(newList: List<DoraSession>) {
    val oldList = data.toList()
    val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {

        override fun getOldListSize(): Int = oldList.size

        override fun getNewListSize(): Int = newList.size

        override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
            val old = oldList[oldPos]
            val new = newList[newPos]
            val bundle = Bundle()
            if (old.lastMsgContent != new.lastMsgContent ||
                old.lastMsgType != new.lastMsgType
            ) {
                bundle.putBoolean("msg", true)
            }
            if (old.unreadCount != new.unreadCount) {
                bundle.putBoolean("unread", true)
            }
            if (old.lastActiveTs != new.lastActiveTs) {
                bundle.putBoolean("time", true)
            }
            if (old.displayName != new.displayName) {
                bundle.putBoolean("displayName", true)
            }
            if (old.avatar != new.avatar) {
                bundle.putBoolean("avatar", true)
            }
            if (old.pin != new.pin) {
                bundle.putBoolean("pin", true)
            }
            return if (bundle.size() == 0) null else bundle
        }

        override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
            return oldList[oldPos].sessionId ==
                    newList[newPos].sessionId
        }

        override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
            val old = oldList[oldPos]
            val new = newList[newPos]
            return old.sessionId == new.sessionId &&
                    old.lastMsgId == new.lastMsgId &&
                    old.unreadCount == new.unreadCount &&
                    old.pin == new.pin &&
                    old.displayName == new.displayName &&
                    old.avatar == new.avatar &&
                    old.lastActiveTs == new.lastActiveTs
        }
    })
    recyclerView.post {
        data.clear()
        data.addAll(newList)
        diffResult.dispatchUpdatesTo(this)
    }
}

我以IM App的会话列表刷新为例,通过重写getChangePayload来实现item内部的局部刷新,比如用户修改好友备注,只改变了会话的显示名称,那我就只刷新显示名称。与之配套的payload的convert实现。

override fun convert(
    holder: BaseViewHolder,
    item: DoraSession,
    payloads: List<Any>
) {
    if (payloads.isEmpty()) {
        convert(holder, item)
        return
    }
    val bundle = Bundle()
    payloads.forEach {
        bundle.putAll(it as Bundle)
    }
    if (bundle.getBoolean("msg")) {
        holder.setText(
            R.id.tvContent,
            createChatListMsg(context, item.lastMsgType, item.lastMsgContent)
        )
    }
    if (bundle.getBoolean("unread")) {
        val badge = holder.getView<MaterialBadgeTextView>(R.id.materialBadgeTextView)
        badge.setBadgeCount(item.unreadCount, true)
    }
    if (bundle.getBoolean("time")) {
        val time =
            if (item.lastActiveTs > 0)
                DateUtils.getTimeString(
                    context,
                    Date(item.lastActiveTs),
                    true
                )
            else ""
        holder.setText(R.id.tvTime, time)
    }
    if (bundle.getBoolean("displayName")) {
        val title =
            if (!TextUtils.isEmpty(item.displayName))
                item.displayName
            else
                formatErc20(item.peerUserId)

        holder.setText(R.id.tvChatTitle, title)
    }
    if (bundle.getBoolean("avatar")) {
        val avatar =
            holder.getView<ImageView>(R.id.avatarView)
        if (item.avatar.isNotEmpty()) {
            AvatarLoader.loadCircle(avatar, item.avatar, "0x")
        }
    }
    if (bundle.getBoolean("pin")) {
        holder.setBackgroundColor(
            R.id.llContent,
            if (item.pin == 1)
                ContextCompat.getColor(context, R.color.colorPanelBg)
            else
                ContextCompat.getColor(context, R.color.white)
        )
    }
}

用户心理:适当“安抚”比技术更重要

很多时候,用户并不是不能接受慢,而是不能接受“没有反馈”。 在弱网环境下,可以通过一些简单的设计来缓解用户焦虑:

  • 显示加载动画(Skeleton、进度条)
  • 提供明确的提示文案(如“网络较慢,请稍候”)
  • 失败时给出重试按钮,而不是静默失败 这种“心理博弈”其实非常关键,它让用户知道:App还在运行,而不是卡死了。