Giảm thiểu tác động của bản cập nhật định kỳ

Các yêu cầu mà ứng dụng của bạn gửi tới mạng là nguyên nhân chính làm tiêu hao pin vì các yêu cầu này bật đài di động hoặc đài Wi-Fi tiêu tốn điện năng. Ngoài công suất cần thiết để gửi và nhận các gói, những bộ đàm này sẽ tiêu tốn thêm năng lượng chỉ với việc bật và duy trì trạng thái khoá. Một hành động đơn giản như yêu cầu mạng cứ 15 giây một lần có thể khiến đài phát trên thiết bị di động luôn bật và nhanh chóng tiêu hao pin.

Có 3 loại nội dung cập nhật định kỳ chung:

  • Do người dùng yêu cầu. Thực hiện cập nhật dựa trên một số hành vi của người dùng, chẳng hạn như cử chỉ kéo để làm mới.
  • Do ứng dụng khởi tạo. Thực hiện cập nhật định kỳ.
  • Do máy chủ khởi tạo. Thực hiện cập nhật để phản hồi thông báo từ máy chủ.

Chủ đề này xem xét từng thành phần và thảo luận thêm về những cách tối ưu hoá để giảm hiện tượng tiêu hao pin.

Tối ưu hoá các yêu cầu do người dùng đưa ra

Các yêu cầu do người dùng đưa ra thường xuất hiện để phản hồi một số hành vi của người dùng. Ví dụ: ứng dụng dùng để đọc các tin bài mới nhất có thể cho phép người dùng thực hiện cử chỉ kéo để làm mới để kiểm tra các bài viết mới. Bạn có thể sử dụng các kỹ thuật sau để phản hồi các yêu cầu do người dùng đưa ra, đồng thời tối ưu hoá việc sử dụng mạng.

Hạn chế yêu cầu của người dùng

Bạn nên bỏ qua một số yêu cầu do người dùng đưa ra nếu không cần thiết, chẳng hạn như nhiều cử chỉ kéo để làm mới trong một khoảng thời gian ngắn để kiểm tra dữ liệu mới trong khi dữ liệu hiện tại vẫn còn mới. Việc thực hiện theo từng yêu cầu có thể lãng phí một lượng pin đáng kể khi khiến đài ở trạng thái bật. Một phương pháp hiệu quả hơn là điều tiết các yêu cầu do người dùng đưa ra để chỉ có thể thực hiện một yêu cầu trong một khoảng thời gian, giảm tần suất sử dụng đài phát.

Sử dụng bộ nhớ đệm

Bằng cách lưu dữ liệu của ứng dụng vào bộ nhớ đệm, bạn sẽ tạo một bản sao cục bộ của thông tin mà ứng dụng cần tham chiếu. Sau đó, ứng dụng của bạn có thể truy cập nhiều lần vào cùng một bản sao cục bộ của thông tin mà không cần phải mở kết nối mạng để đưa ra yêu cầu mới.

Bạn nên lưu dữ liệu vào bộ nhớ đệm càng nhiều càng tốt, bao gồm cả các tài nguyên tĩnh và nội dung tải xuống theo yêu cầu, chẳng hạn như hình ảnh có kích thước đầy đủ. Bạn có thể sử dụng các tiêu đề bộ nhớ đệm HTTP để đảm bảo rằng chiến lược lưu vào bộ nhớ đệm không làm ứng dụng của bạn hiển thị dữ liệu lỗi thời. Để biết thêm thông tin về cách lưu các phản hồi mạng vào bộ nhớ đệm, hãy xem phần Tránh tải tệp xuống thừa.

Trên Android 11 trở lên, ứng dụng của bạn có thể dùng chính các tập dữ liệu lớn mà các ứng dụng khác dùng cho trường hợp sử dụng như học máy và phát nội dung nghe nhìn. Khi cần truy cập vào một tập dữ liệu dùng chung, trước tiên, ứng dụng của bạn có thể kiểm tra phiên bản đã lưu vào bộ nhớ đệm trước khi tìm cách tải bản sao mới xuống. Để tìm hiểu thêm về các tập dữ liệu dùng chung, hãy xem phần Truy cập vào tập dữ liệu dùng chung.

Sử dụng băng thông lớn hơn để tải nhiều dữ liệu xuống ít thường xuyên hơn

Khi kết nối qua sóng vô tuyến không dây, băng thông cao hơn thường đi kèm với chi phí pin cao hơn, nghĩa là 5G thường tiêu thụ nhiều năng lượng hơn LTE, đồng thời cũng đắt hơn 3G.

Tức là mặc dù trạng thái phát thanh cơ bản thay đổi tuỳ theo công nghệ vô tuyến, nhưng nhìn chung, tác động tương đối của pin của thời gian kết thúc thay đổi trạng thái sẽ lớn hơn đối với đài phát băng thông cao hơn. Để biết thêm thông tin về thời gian đếm ngược, hãy xem bài viết Máy trạng thái vô tuyến.

Đồng thời, băng thông cao hơn có nghĩa là bạn có thể tìm nạp trước linh hoạt hơn, tải nhiều dữ liệu xuống hơn cùng một lúc. Có thể ít trực quan hơn, vì chi phí pin tại thời điểm cuối tương đối cao hơn, nên việc duy trì đài phát trong thời gian dài hơn trong mỗi phiên chuyển cũng sẽ hiệu quả hơn để giảm tần suất cập nhật.

Ví dụ: nếu một đài LTE có băng thông gấp đôi và chi phí năng lượng gấp đôi so với 3G, thì bạn nên tải dữ liệu xuống nhiều gấp 4 lần trong mỗi phiên – hoặc có thể lên tới 10 MB. Khi tải lượng dữ liệu này xuống, bạn cần cân nhắc tác động của việc tìm nạp trước đối với bộ nhớ cục bộ còn trống và thường xuyên xoá bộ nhớ đệm tìm nạp trước.

Bạn có thể dùng ConnectivityManager để đăng ký trình nghe cho mạng mặc định và TelephonyManager để đăng ký PhoneStateListener nhằm xác định loại kết nối thiết bị hiện tại. Sau khi biết loại kết nối, bạn có thể sửa đổi quy trình tìm nạp trước cho phù hợp:

Kotlin

val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager

private var hasWifi = false
private var hasCellular = false
private var cellModifier: Float = 1f

private val networkCallback = object : ConnectivityManager.NetworkCallback() {
    // Network capabilities have changed for the network
    override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
    ) {
        super.onCapabilitiesChanged(network, networkCapabilities)
        hasCellular = networkCapabilities
    .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
        hasWifi = networkCapabilities
    .hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
    }
}

private val phoneStateListener = object : PhoneStateListener() {
override fun onPreciseDataConnectionStateChanged(
    dataConnectionState: PreciseDataConnectionState
) {
  cellModifier = when (dataConnectionState.networkType) {
      TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f
      TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1/2f
      else -> 1f

  }
}

private class NetworkState {
    private var defaultNetwork: Network? = null
    private var defaultCapabilities: NetworkCapabilities? = null
    fun setDefaultNetwork(network: Network?, caps: NetworkCapabilities?) = synchronized(this) {
        defaultNetwork = network
        defaultCapabilities = caps
    }
    val isDefaultNetworkWifi
        get() = synchronized(this) {
            defaultCapabilities?.hasTransport(TRANSPORT_WIFI) ?: false
        }
    val isDefaultNetworkCellular
        get() = synchronized(this) {
            defaultCapabilities?.hasTransport(TRANSPORT_CELLULAR) ?: false
        }
    val isDefaultNetworkUnmetered
        get() = synchronized(this) {
            defaultCapabilities?.hasCapability(NET_CAPABILITY_NOT_METERED) ?: false
        }
    var cellNetworkType: Int = TelephonyManager.NETWORK_TYPE_UNKNOWN
        get() = synchronized(this) { field }
        set(t) = synchronized(this) { field = t }
    private val cellModifier: Float
        get() = synchronized(this) {
            when (cellNetworkType) {
                TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f
                TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1 / 2f
                else -> 1f
            }
        }
    val prefetchCacheSize: Int
        get() = when {
            isDefaultNetworkWifi -> MAX_PREFETCH_CACHE
            isDefaultNetworkCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt()
            else -> DEFAULT_PREFETCH_CACHE
        }
}
private val networkState = NetworkState()
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
    // Network capabilities have changed for the network
    override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
    ) {
        networkState.setDefaultNetwork(network, networkCapabilities)
    }

    override fun onLost(network: Network?) {
        networkState.setDefaultNetwork(null, null)
    }
}

private val telephonyCallback = object : TelephonyCallback(), TelephonyCallback.PreciseDataConnectionStateListener {
    override fun onPreciseDataConnectionStateChanged(dataConnectionState: PreciseDataConnectionState) {
        networkState.cellNetworkType = dataConnectionState.networkType
    }
}

connectivityManager.registerDefaultNetworkCallback(networkCallback)
telephonyManager.registerTelephonyCallback(telephonyCallback)


private val prefetchCacheSize: Int
get() {
    return when {
        hasWifi -> MAX_PREFETCH_CACHE
        hasCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt()
        else -> DEFAULT_PREFETCH_CACHE
    }
}

}

Java

ConnectivityManager cm =
 (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
TelephonyManager tm =
  (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

private boolean hasWifi = false;
private boolean hasCellular = false;
private float cellModifier = 1f;

private ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onCapabilitiesChanged(
    @NonNull Network network,
    @NonNull NetworkCapabilities networkCapabilities
) {
        super.onCapabilitiesChanged(network, networkCapabilities);
        hasCellular = networkCapabilities
    .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
        hasWifi = networkCapabilities
    .hasTransport(NetworkCapabilities.TRANSPORT_WIFI);
}
};

private PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override
public void onPreciseDataConnectionStateChanged(
    @NonNull PreciseDataConnectionState dataConnectionState
    ) {
    switch (dataConnectionState.getNetworkType()) {
        case (TelephonyManager.NETWORK_TYPE_LTE |
            TelephonyManager.NETWORK_TYPE_HSPAP):
            cellModifier = 4;
            Break;
        case (TelephonyManager.NETWORK_TYPE_EDGE |
            TelephonyManager.NETWORK_TYPE_GPRS):
            cellModifier = 1/2.0f;
            Break;
        default:
            cellModifier = 1;
            Break;
    }
}
};

cm.registerDefaultNetworkCallback(networkCallback);
tm.listen(
phoneStateListener,
PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE
);

public int getPrefetchCacheSize() {
if (hasWifi) {
    return MAX_PREFETCH_SIZE;
}
if (hasCellular) {
    return (int) (DEFAULT_PREFETCH_SIZE * cellModifier);
    }
return DEFAULT_PREFETCH_SIZE;
}

Tối ưu hoá các yêu cầu do ứng dụng khởi tạo

Các yêu cầu do ứng dụng khởi tạo thường diễn ra theo lịch biểu, chẳng hạn như một ứng dụng gửi nhật ký hoặc số liệu phân tích đến một dịch vụ phụ trợ. Khi xử lý các yêu cầu do ứng dụng khởi tạo, hãy cân nhắc mức độ ưu tiên của các yêu cầu đó, liệu chúng có thể được phân theo lô với nhau hay không và liệu chúng có thể bị trì hoãn cho đến khi thiết bị đang sạc hoặc kết nối với mạng không đo lượng dữ liệu hay không. Bạn có thể tối ưu hoá các yêu cầu này bằng cách lên lịch cẩn thận và sử dụng các thư viện như WorkManager.

Yêu cầu mạng theo lô

Trên thiết bị di động, quá trình bật đài, tạo kết nối và giữ cho đài phát ở trạng thái bật sẽ tiêu tốn một lượng lớn năng lượng. Do đó, việc xử lý từng yêu cầu riêng lẻ vào những thời điểm ngẫu nhiên có thể làm tiêu hao pin một cách đáng kể và làm giảm thời lượng pin. Một phương pháp hiệu quả hơn là đưa một tập hợp các yêu cầu mạng vào hàng đợi và xử lý chúng cùng nhau. Điều này cho phép hệ thống thanh toán chi phí điện năng khi bật đài chỉ một lần mà vẫn nhận được tất cả dữ liệu mà ứng dụng yêu cầu.

Sử dụng WorkManager

Bạn có thể sử dụng thư viện WorkManager để thực hiện công việc theo lịch biểu hiệu quả, trong đó cân nhắc xem có đáp ứng các điều kiện cụ thể hay không, chẳng hạn như khả năng hoạt động của mạng và trạng thái nguồn điện. Ví dụ: giả sử bạn có một lớp con Worker có tên là DownloadHeadlinesWorker để truy xuất các tiêu đề tin tức mới nhất. Bạn có thể lên lịch chạy worker này mỗi giờ, miễn là thiết bị kết nối với mạng không đo lượng dữ liệu và pin của thiết bị không yếu, kèm theo chiến lược thử lại tuỳ chỉnh nếu có vấn đề khi truy xuất dữ liệu, như minh hoạ dưới đây:

Kotlin

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)
    .setRequiresBatteryNotLow(true)
    .build()
val request =
    PeriodicWorkRequestBuilder<DownloadHeadlinesWorker>(1, TimeUnit.HOURS)
        .setConstraints(constraints)
        .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES)
        .build()
WorkManager.getInstance(context).enqueue(request)

Java

Constraints constraints = new Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED)
        .setRequiresBatteryNotLow(true)
        .build();
WorkRequest request = new PeriodicWorkRequest.Builder(DownloadHeadlinesWorker.class, 1, TimeUnit.HOURS)
        .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES)
        .build();
WorkManager.getInstance(this).enqueue(request);

Ngoài WorkManager, nền tảng Android còn cung cấp một số công cụ khác để giúp bạn tạo lịch biểu hiệu quả để hoàn tất các tác vụ nối mạng, chẳng hạn như thăm dò ý kiến. Để tìm hiểu thêm về cách sử dụng các công cụ này, hãy xem Hướng dẫn xử lý ở chế độ nền.

Tối ưu hoá các yêu cầu do máy chủ khởi tạo

Các yêu cầu do máy chủ khởi tạo thường xảy ra để phản hồi một thông báo của máy chủ. Ví dụ: một ứng dụng dùng để đọc các tin bài mới nhất có thể nhận được thông báo về một loạt bài viết mới phù hợp với lựa chọn ưu tiên của người dùng về hoạt động cá nhân hoá. Sau đó, ứng dụng này sẽ tải xuống.

Gửi bản cập nhật máy chủ bằng Giải pháp gửi thông báo qua đám mây của Firebase

Giải pháp gửi thông báo qua đám mây của Firebase (FCM) là một cơ chế gọn nhẹ dùng để truyền dữ liệu từ máy chủ đến một phiên bản ứng dụng cụ thể. Bằng cách sử dụng FCM, máy chủ có thể thông báo cho ứng dụng đang chạy trên một thiết bị cụ thể rằng ứng dụng đó có dữ liệu mới.

So với việc thăm dò ý kiến (trong đó ứng dụng của bạn phải thường xuyên ping máy chủ để truy vấn dữ liệu mới), mô hình dựa trên sự kiện này chỉ cho phép ứng dụng của bạn tạo một kết nối mới khi biết có dữ liệu cần tải xuống. Mô hình này giảm thiểu các kết nối không cần thiết và giảm độ trễ khi cập nhật thông tin trong ứng dụng.

FCM được triển khai bằng kết nối TCP/IP liên tục. Việc này giúp giảm thiểu số lượng kết nối liên tục và cho phép nền tảng tối ưu hoá băng thông, đồng thời giảm thiểu tác động liên quan đến thời lượng pin.