Gỡ lỗi Hồ sơ cơ sở

Tài liệu này trình bày các phương pháp hay nhất để giúp bạn chẩn đoán vấn đề và đảm bảo Hồ sơ cơ sở hoạt động đúng cách nhằm mang lại nhiều lợi ích nhất.

Vấn đề về bản dựng

Nếu đã sao chép ví dụ về Hồ sơ cơ sở trong ứng dụng mẫu Now in Android, bạn có thể gặp thất bại trong kiểm thử (khi thực hiện tác vụ trong Hồ sơ cơ sở) cho biết rằng hoạt động kiểm thử không chạy được trên một trình mô phỏng:

./gradlew assembleDemoRelease
Starting a Gradle Daemon (subsequent builds will be faster)
Calculating task graph as no configuration cache is available for tasks: assembleDemoRelease
Type-safe project accessors is an incubating feature.

> Task :benchmarks:pixel6Api33DemoNonMinifiedReleaseAndroidTest
Starting 14 tests on pixel6Api33

com.google.samples.apps.nowinandroid.foryou.ScrollForYouFeedBenchmark > scrollFeedCompilationNone[pixel6Api33] FAILED
        java.lang.AssertionError: ERRORS (not suppressed): EMULATOR
        WARNINGS (suppressed):
        ...

Lỗi này xảy ra vì ứng dụng Now in Android dùng một thiết bị do Gradle quản lý để tạo Hồ sơ cơ sở. Lỗi này có thể xảy ra vì bạn thường không được chạy phép đo điểm chuẩn hiệu suất trên một trình mô phỏng. Tuy nhiên, vì không thu thập các chỉ số về hiệu suất khi tạo Hồ sơ cơ sở nên để thuận tiện, bạn có thể chạy bộ sưu tập Hồ sơ cơ sở trên các trình mô phỏng. Để sử dụng Hồ sơ cơ sở với một trình mô phỏng, hãy tạo và cài đặt bản dựng qua dòng lệnh, đồng thời đặt một đối số để bật các quy tắc của Hồ sơ cơ sở:

installDemoRelease -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile

Ngoài ra, bạn có thể tạo một cấu hình chạy tuỳ chỉnh trong Android Studio để bật Hồ sơ cơ sở trên các trình mô phỏng bằng cách chọn Run > Edit Configurations (Chạy > Chỉnh sửa cấu hình):

Thêm một cấu hình chạy tuỳ chỉnh để tạo Hồ sơ cơ sở trong ứng dụng Now in Android
Hình 1. Thêm cấu hình chạy tuỳ chỉnh để tạo Hồ sơ cơ sở trong ứng dụng Now in Android.

Vấn đề về cài đặt

Hãy kiểm tra để đảm bảo rằng APK hoặc AAB mà bạn đang tạo là của một biến thể bản dựng có chứa Hồ sơ cơ sở. Để kiểm tra, cách dễ nhất là mở APK trong Android Studio bằng cách chọn Build > Analyze APK (Bản dựng > Phân tích APK), mở APK của bạn rồi tìm hồ sơ trong tệp /assets/dexopt/baseline.prof:

Kiểm tra Hồ sơ cơ sở bằng Trình xem APK trong Android Studio
Hình 2. Kiểm tra Hồ sơ cơ sở bằng Trình xem APK trong Android Studio.

Hồ sơ cơ sở cần được biên dịch trên thiết bị đang chạy ứng dụng. Đối với cả lượt cài đặt qua cửa hàng ứng dụng và ứng dụng được cài đặt bằng PackageInstaller, hoạt động biên dịch trên thiết bị đều diễn ra trong quá trình cài đặt ứng dụng. Tuy nhiên, khi cài đặt ứng dụng không qua cửa hàng ứng dụng từ Android Studio hoặc qua các công cụ dòng lệnh, thư viện ProfileInstaller Jetpack chịu trách nhiệm xếp các hồ sơ cần biên dịch vào hàng đợi trong quá trình tối ưu hoá DEX tiếp theo ở chế độ nền. Trong những trường hợp đó, nếu muốn đảm bảo Hồ sơ cơ sở đang được sử dụng, bạn có thể phải buộc thực hiện quy trình biên dịch Hồ sơ cơ sở. ProfileVerifier cho phép bạn truy vấn trạng thái cài đặt và biên dịch hồ sơ, như trong ví dụ sau:

Kotlin

private const val TAG = "MainActivity"

class MainActivity : ComponentActivity() {
  ...
  override fun onResume() {
    super.onResume()
    lifecycleScope.launch {
      logCompilationStatus()
    }
  }

  private suspend fun logCompilationStatus() {
     withContext(Dispatchers.IO) {
        val status = ProfileVerifier.getCompilationStatusAsync().await()
        when (status.profileInstallResultCode) {
            RESULT_CODE_NO_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Baseline Profile not found")
            RESULT_CODE_COMPILED_WITH_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Compiled with profile")
            RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
                Log.d(TAG, "ProfileInstaller: App was installed through Play store")
            RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST ->
                Log.d(TAG, "ProfileInstaller: PackageName not found")
            RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ ->
                Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read")
            RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE ->
                Log.d(TAG, "ProfileInstaller: Can't write cache file")
            RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            else ->
                Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued")
        }
    }
}

Java

public class MainActivity extends ComponentActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onResume() {
        super.onResume();

        logCompilationStatus();
    }

    private void logCompilationStatus() {
         ListeningExecutorService service = MoreExecutors.listeningDecorator(
                Executors.newSingleThreadExecutor());
        ListenableFuture<ProfileVerifier.CompilationStatus> future =
                ProfileVerifier.getCompilationStatusAsync();
        Futures.addCallback(future, new FutureCallback<>() {
            @Override
            public void onSuccess(CompilationStatus result) {
                int resultCode = result.getProfileInstallResultCode();
                if (resultCode == RESULT_CODE_NO_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Baseline Profile not found");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Compiled with profile");
                } else if (resultCode == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING) {
                    Log.d(TAG, "ProfileInstaller: App was installed through Play store");
                } else if (resultCode == RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST) {
                    Log.d(TAG, "ProfileInstaller: PackageName not found");
                } else if (resultCode == RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ) {
                    Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read");
                } else if (resultCode
                        == RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE) {
                    Log.d(TAG, "ProfileInstaller: Can't write cache file");
                } else if (resultCode == RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else {
                    Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued");
                }
            }

            @Override
            public void onFailure(Throwable t) {
                Log.d(TAG,
                        "ProfileInstaller: Error getting installation status: " + t.getMessage());
            }
        }, service);
    }
}

Những mã kết quả sau đưa ra gợi ý về nguyên nhân gây ra một số vấn đề:

RESULT_CODE_COMPILED_WITH_PROFILE
Hồ sơ được cài đặt, biên dịch và dùng bất cứ khi nào ứng dụng được chạy. Đây là kết quả mà bạn muốn thấy.
RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED
Không tìm thấy hồ sơ nào trong APK hoặc AAB đang chạy. Hãy đảm bảo rằng bạn đang sử dụng một biến thể bản dựng có chứa Hồ sơ cơ sở nếu thấy lỗi này và rằng APK đó có chứa một hồ sơ.
RESULT_CODE_NO_PROFILE
Chưa cài đặt hồ sơ nào cho ứng dụng này khi cài đặt ứng dụng qua cửa hàng ứng dụng hoặc trình quản lý gói. Nguyên nhân chính gây ra mã lỗi là trình cài đặt hồ sơ không chạy do ProfileInstallerInitializer bị tắt. Xin lưu ý rằng khi lỗi này được báo cáo, hệ thống vẫn tìm thấy hồ sơ được nhúng trong tệp APK của ứng dụng. Khi không tìm thấy hồ sơ được nhúng, mã lỗi được trả về là RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED.
RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
Đã tìm thấy một hồ sơ trong tệp APK hoặc AAB và hồ sơ này sẽ được đưa vào hàng đợi biên dịch. Khi được ProfileInstaller cài đặt, hồ sơ sẽ được đưa vào hàng đợi biên dịch vào lần tiếp theo hệ thống chạy quy trình tối ưu hoá DEX ở chế độ nền. Hồ sơ này chỉ hoạt động cho đến khi quá trình biên dịch hoàn tất. Đừng tìm cách đo điểm chuẩn Hồ sơ cơ sở của bạn cho đến khi quá trình biên dịch hoàn tất. Bạn có thể phải buộc thực hiện quy trình biên dịch Hồ sơ cơ sở. Lỗi này sẽ không xảy ra khi ứng dụng được cài đặt qua cửa hàng ứng dụng hoặc trình quản lý gói trên các thiết bị chạy Android 9 (API cấp 28) trở lên vì quá trình biên dịch được thực hiện trong khi cài đặt.
RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING
Một hồ sơ không phù hợp sẽ được cài đặt và ứng dụng đã được biên dịch với hồ sơ đó. Đây là kết quả của quá trình cài đặt qua Cửa hàng Google Play hoặc trình quản lý gói. Xin lưu ý rằng kết quả này khác với RESULT_CODE_COMPILED_WITH_PROFILE vì hồ sơ không phù hợp sẽ chỉ biên dịch bất kỳ phương thức nào vẫn dùng được giữa hồ sơ và ứng dụng. Kích thước hồ sơ nhỏ hơn đáng kể so với dự kiến và sẽ có ít phương thức được biên dịch hơn so với số phương thức có trong Hồ sơ cơ sở.
RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE
ProfileVerifier không thể ghi tệp kết quả xác minh vào bộ nhớ đệm. Điều này có thể xảy ra do có lỗi về quyền đối với thư mục ứng dụng hoặc nếu thiết bị không có đủ dung lượng ổ đĩa trống.
RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
ProfileVerifieris running on an unsupported API version of Android. ProfileVerifier chỉ hỗ trợ Android 9 (API cấp 28) trở lên.
RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST
Hệ thống sẽ gửi PackageManager.NameNotFoundException khi truy vấn PackageManager của gói ứng dụng. Điều này hiếm khi xảy ra. Hãy thử gỡ cài đặt ứng dụng rồi cài đặt lại mọi thứ.
RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ
Tệp kết quả xác minh trước trong bộ nhớ đệm đã tồn tại nhưng không đọc được. Điều này hiếm khi xảy ra. Hãy thử gỡ cài đặt ứng dụng rồi cài đặt lại mọi thứ.

Sử dụng ProfileVerifier trong phiên bản phát hành chính thức

Trong phiên bản phát hành chính thức, bạn có thể sử dụng ProfileVerifier cùng với các thư viện phân tích-báo cáo, chẳng hạn như Google Analytics cho Firebase để tạo các sự kiện phân tích cho biết trạng thái hồ sơ. Ví dụ: bạn sẽ nhanh chóng nhận được thông báo nếu phiên bản ứng dụng mới được phát hành không chứa Hồ sơ cơ sở.

Buộc thực hiện quy trình biên dịch Hồ sơ cơ sở

Nếu trạng thái biên dịch của Hồ sơ cơ sở là RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION, bạn có thể buộc thực hiện quy trình biên dịch ngay lập tức bằng cách sử dụng adb:

adb shell cmd package compile -r bg-dexopt PACKAGE_NAME

Kiểm tra trạng thái biên dịch mà không có ProfileVerifier

Nếu không dùng ProfileVerifier, bạn có thể kiểm tra trạng thái biên dịch bằng cách sử dụng adb, mặc dù mã này không cung cấp thông tin chi tiết chuyên sâu như ProfileVerifier:

adb shell dumpsys package dexopt | grep -A 2 PACKAGE_NAME

Khi sử dụng adb, kết quả tạo ra sẽ tương tự như sau:

  [com.google.samples.apps.nowinandroid.demo]
    path: /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/base.apk
      arm64: [status=speed-profile] [reason=bg-dexopt] [primary-abi]
        [location is /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/oat/arm64/base.odex]

Giá trị trạng thái cho biết trạng thái biên dịch hồ sơ và là một trong các giá trị sau:

Trạng thái biên dịch Ý nghĩa
speed‑profile Có hồ sơ được biên dịch và đang được sử dụng.
verify Không có hồ sơ đã biên dịch nào.

Trạng thái verify không có nghĩa là tệp APK hoặc AAB không chứa hồ sơ, vì hồ sơ này có thể được đưa vào hàng đợi biên dịch của tác vụ tối ưu hoá DEX tiếp theo ở chế độ nền.

Giá trị lý do cho biết yếu tố nào kích hoạt quá trình biên dịch hồ sơ và là một trong các giá trị sau:

Lý do Ý nghĩa
install‑dm Hồ sơ cơ sở được biên dịch theo cách thủ công hoặc bởi Google Play khi ứng dụng được cài đặt.
bg‑dexopt Một hồ sơ đã được biên dịch khi thiết bị của bạn ở trạng thái không hoạt động. Đây có thể là Hồ sơ cơ sở hoặc hồ sơ được thu thập trong quá trình dùng ứng dụng.
cmdline Quá trình biên dịch được kích hoạt bằng adb. Đây có thể là Hồ sơ cơ sở hoặc hồ sơ được thu thập trong quá trình dùng ứng dụng.

Vấn đề về hiệu suất

Phần này trình bày một số phương pháp hay nhất để giúp bạn xác định chính xác và đo điểm chuẩn Hồ sơ cơ sở nhằm thu được nhiều lợi ích nhất từ những hồ sơ đó.

Đo chính xác điểm chuẩn các chỉ số khởi động

Hồ sơ cơ sở của bạn sẽ mang lại nhiều hiệu quả hơn nếu bạn xác định rõ các chỉ số khởi động. Hai chỉ số chính là thời gian hiển thị khung hình đầu tiên (TTID)thời gian hiển thị khung hình đầu tiên với nội dung đầy đủ (TTFD).

TTID là khi ứng dụng vẽ khung hình đầu tiên. Bạn cần phải giữ cho chỉ số này ở mức thấp nhất có thể vì hệ thống sẽ hiển thị nội dung nào đó cho người dùng biết ứng dụng đang chạy. Thậm chí, bạn có thể hiển thị một chỉ báo tiến trình không xác định để cho biết ứng dụng có phản hồi.

TTFD là thời điểm ứng dụng có thể thực sự tương tác. Bạn cần phải giữ nội dung này ngắn nhất có thể để tránh gây khó chịu cho người dùng. Nếu bạn ra tín hiệu TTFD chính xác, hệ thống sẽ biết rằng mã đang chạy đến TTFD là một phần của quá trình khởi động ứng dụng. Do đó, hệ thống có nhiều khả năng sẽ đặt mã này vào hồ sơ.

Hãy duy trì cả TTID và TTFD ở mức thấp nhất có thể để ứng dụng của bạn phản hồi.

Hệ thống có thể phát hiện TTID, hiển thị chỉ số này trong Logcat và báo cáo chỉ số này là một phần của phép đo điểm chuẩn khởi động. Tuy nhiên, hệ thống không thể xác định TTFD và ứng dụng có trách nhiệm báo cáo khi chỉ số này đạt đến trạng thái tương tác được vẽ đầy đủ. Bạn có thể thực hiện việc này bằng cách gọi reportFullyDrawn() hoặc ReportDrawn nếu đang dùng Jetpack Compose. Nếu nhiều tác vụ của bạn ở chế độ nền đều cần phải hoàn tất trước khi ứng dụng được coi là vẽ đầy đủ, thì bạn có thể sử dụngFullyDrawnReporter, như được mô tả trong phần Cải thiện độ chính xác về thời gian khởi động.

Hồ sơ thư viện và hồ sơ tuỳ chỉnh

Khi đo điểm chuẩn tác động của tiểu sử, có thể khó tách riêng lợi ích của hồ sơ trong ứng dụng từ những hồ sơ do các thư viện đóng góp, chẳng hạn như Thư viện Jetpack. Khi bạn xây dựng APK, trình bổ trợ Android cho Gradle sẽ thêm bất kỳ các hồ sơ trong phần phụ thuộc của thư viện cũng như hồ sơ tuỳ chỉnh của bạn. Hay quá để tối ưu hoá hiệu suất tổng thể. Đây là phương pháp nên dùng cho các bản phát hành của bạn. Tuy nhiên, điều này khiến khó đo lường mức tăng hiệu suất bổ sung khỏi hồ sơ tuỳ chỉnh của mình.

Xem nhanh tối ưu hoá bổ sung do tuỳ chỉnh xoá nó và chạy phép đo điểm chuẩn của bạn. Sau đó, hãy thay thế và chạy điểm chuẩn lần nữa. Khi so sánh hai công cụ này, bạn sẽ thấy những điểm tối ưu hoá do chỉ các hồ sơ thư viện và hồ sơ thư viện cùng với hồ sơ tuỳ chỉnh của bạn.

Một cách tự động để so sánh hồ sơ là tạo một biến thể bản dựng mới chỉ chứa hồ sơ thư viện chứ không chứa hồ sơ tuỳ chỉnh. So sánh điểm chuẩn từ biến thể này đến biến thể bản phát hành chứa cả biến thể hồ sơ thư viện và hồ sơ tuỳ chỉnh của bạn. Ví dụ sau đây minh hoạ cách để thiết lập biến thể chỉ bao gồm hồ sơ thư viện. Thêm biến thể mới có tên releaseWithoutCustomProfile vào mô-đun người dùng hồ sơ của bạn, tức là thường là mô-đun ứng dụng của bạn:

Kotlin

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    create("releaseWithoutCustomProfile") {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile(project(":baselineprofile"))
}

baselineProfile {
  variants {
    create("release") {
      from(project(":baselineprofile"))
    }
  }
}

Groovy

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    releaseWithoutCustomProfile {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile ':baselineprofile"'
}

baselineProfile {
  variants {
    release {
      from(project(":baselineprofile"))
    }
  }
}

Ví dụ về mã trước đó xoá phần phụ thuộc baselineProfile khỏi tất cả và chỉ áp dụng có chọn lọc cho biến thể release. Có vẻ như khác thường là các hồ sơ thư viện vẫn đang được thêm vào khi phần phụ thuộc vào mô-đun trình tạo hồ sơ sẽ bị xoá. Tuy nhiên, mô-đun này lại chỉ chịu trách nhiệm tạo hồ sơ tuỳ chỉnh cho bạn. Gradle của Android trình bổ trợ vẫn đang chạy cho tất cả các biến thể và chịu trách nhiệm đưa vào hồ sơ thư viện.

Bạn cũng cần thêm biến thể mới vào mô-đun trình tạo hồ sơ. Trong phần này Ví dụ: mô-đun trình sản xuất có tên là :baselineprofile.

Kotlin

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      create("releaseWithoutCustomProfile") {}
      ...
    }
  ...
}

Groovy

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      releaseWithoutCustomProfile {}
      ...
    }
  ...
}

Khi chạy phép đo điểm chuẩn trong Android Studio, hãy chọn một releaseWithoutCustomProfile biến thể để đo lường hiệu suất chỉ với thư viện các cấu hình hoặc chọn một biến thể release để đo lường hiệu suất bằng thư viện và hồ sơ tuỳ chỉnh.

Tránh khởi động ứng dụng bị ràng buộc bởi I/O

Nếu ứng dụng của bạn đang thực hiện nhiều lệnh gọi I/O hoặc lệnh gọi mạng trong khi khởi động, thì việc này có thể ảnh hưởng tiêu cực đến cả thời gian khởi động ứng dụng và độ chính xác của phép đo điểm chuẩn khi khởi động. Các lệnh gọi hạng nặng này có thể mất một khoảng thời gian không xác định và có thể thay đổi theo thời gian, thậm chí là giữa các lần lặp lại của phép đo điểm chuẩn đó. Lệnh gọi I/O thường hiệu quả hơn lệnh gọi mạng, vì lệnh gọi mạng có thể chịu ảnh hưởng của các yếu tố bên ngoài thiết bị và trên chính thiết bị đó. Tránh dùng các lệnh gọi mạng khi đang khởi động. Khi sử dụng một lệnh gọi hoặc bắt buộc phải dùng lệnh gọi kia, hãy sử dụng lệnh gọi I/O.

Bạn nên thực hiện quá trình khởi động ứng dụng hỗ trợ cấu trúc ứng dụng mà không cần lệnh gọi mạng hoặc lệnh gọi I/O, ngay cả khi chỉ dùng lệnh gọi đó khi khởi động phép đo điểm chuẩn. Điều này giúp đảm bảo mức thay đổi ít nhất có thể giữa các lần lặp lại phép đo điểm chuẩn.

Nếu ứng dụng của bạn dùng Hilt, bạn có thể cung cấp giới hạn I/O giả mạo khi đo điểm chuẩn trong Microbenchmark và Hilt.

Bao gồm mọi hành trình quan trọng của người dùng

Điều quan trọng là bạn phải đề cập chính xác mọi hành trình quan trọng của người dùng trong quá trình tạo Hồ sơ cơ sở. Mọi hành trình của người dùng không được đề cập sẽ không được cải thiện qua Hồ sơ cơ sở. Hồ sơ cơ sở có hiệu quả nhất bao gồm mọi hành trình thường gặp của người dùng khi khởi động cũng như hành trình của người dùng trong ứng dụng liên quan đến hiệu suất, chẳng hạn như việc cuộn danh sách.