Cải thiện hiệu suất của ứng dụng nhờ Cấu hình cơ sở

1. Trước khi bắt đầu

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách tạo Cấu hình cơ sở để tối ưu hóa hiệu quả hoạt động của ứng dụng và cách xác minh lợi ích của hiệu quả hoạt động khi sử dụng Cấu hình cơ sở.

Điều kiện tiên quyết

Khóa học này được xây dựng dựa trên lớp học lập trình có nội dung Kiểm tra hiệu suất của ứng dụng với Macrobenchmark để biết cách đo lường hiệu suất ứng dụng bằng thư viện Macrobenchmark.

Bạn cần có

Bạn sẽ thực hiện

  • Tạo Cấu hình cơ sở để tối ưu hóa hiệu suất
  • Xác minh mức tăng hiệu suất với thư viện Macrobenchmark

Kiến thức bạn sẽ học được

  • Đang tạo Cấu hình cơ sở
  • Tìm hiểu về mức tăng hiệu suất của Cấu hình cơ sở

2. Thiết lập

Để bắt đầu, hãy sao chép kho lưu trữ GitHub từ dòng lệnh bằng cách sử dụng lệnh sau:

$ git clone --branch baselineprofiles-main  https://github.com/googlecodelabs/android-performance.git

Ngoài ra, bạn có thể tải 2 tệp zip xuống:

Mở dự án vào trong Android Studio

  1. Trên cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), vui lòng chọn biểu tượng c01826594f360d94.png Open an Existing Project (Mở một dự án hiện có).
  2. Chọn thư mục [Download Location]/android-performance/benchmarking (lưu ý: nhớ chọn thư mục benchmarking có chứa build.gradle)
  3. Khi Android Studio đã nhập dự án, vui lòng đảm bảo bạn có thể chạy mô-đun app để tạo ứng dụng mẫu mà chúng tôi sẽ đo điểm chuẩn.

3. Cấu hình cơ sở là gì?

Cấu hình cơ sở là danh sách các lớp và phương thức có trong một APK mà Android Runtime (ART) sử dụng trong quá trình cài đặt ứng dụng để tổng hợp trước các đường dẫn quan trọng đến mã máy. Đây là một phương thức tối ưu hoá theo cấu hình (PGO) cho phép ứng dụng tối ưu hoá quá trình khởi động, giảm hiện tượng giật và cải thiện hiệu suất cho người dùng cuối.

Cách hoạt động của Cấu hình cơ sở

Các quy tắc hồ sơ được biên dịch dưới dạng tệp nhị phân trong APK, trong assets/dexopt/baseline.prof, sau đó được gửi trực tiếp cho người dùng (thông qua Google Play) cùng với APK đó.

Trong quá trình cài đặt, ART thực hiện việc tổng hợp các phương thức trước thời gian (AOT) trong cấu hình, nhờ đó các phương thức đó được thực thi nhanh hơn. Nếu cấu hình chứa các phương thức được dùng khi chạy ứng dụng hoặc trong quá trình kết xuất khung hình, thì người dùng sẽ có thời gian khởi động nhanh hơn và/hoặc giảm hiện tượng giật.

4. Cập nhật mô-đun đo điểm chuẩn

Là một nhà phát triển ứng dụng, bạn có thể tự động tạo Cấu hình cơ sở bằng cách sử dụng thư viện Jetpack Macrobenchmark. Để tạo Cấu hình cơ sở, bạn có thể sử dụng cùng một mô-đun được tạo để đo điểm chuẩn cho ứng dụng với một số thay đổi bổ sung.

Tắt tính năng làm rối mã nguồn cho Cấu hình cơ sở

Nếu ứng dụng của bạn đã bật tính năng làm rối mã nguồn, bạn cần tắt tính năng đó để thực hiện các phép đo điểm chuẩn.

Bạn có thể thực hiện việc này bằng cách thêm một tệp proguard vào mô-đun :app, đồng thời tắt tính năng làm rối mã nguồn tại đó và thêm tệp proguard vào loại bản dựng benchmark.

Tạo một tệp mới có tên benchmark-rules.pro trong mô-đun :app. Tệp này phải được đặt trong thư mục /app/ bên cạnh tệp build.gradle dành riêng cho mô-đun. 27bd3b1881011d06.png

Trong tệp này, hãy tắt tính năng làm rối mã nguồn bằng cách thêm -dontobfuscate như trong đoạn mã sau:

# Disables obfuscation for benchmark builds.
-dontobfuscate

Tiếp theo, hãy sửa đổi loại bản dựng benchmark trong mô-đun :app dành riêng cho build.gradle rồi thêm tệp mà bạn đã tạo vào. Vì chúng ta đang dùng loại bản dựng phát hành initWith, nên dòng này sẽ thêm tệp proguard benchmark-rules.pro vào các tệp proguard bản phát hành.

buildTypes {
   release {
      // ...
   }

   benchmark {
      initWith buildTypes.release
      // ...
      proguardFiles('benchmark-rules.pro')
   }
}

Giờ thì hãy viết một lớp Trình tạo Cấu hình cơ sở.

5. Viết lớp của trình tạo Cấu hình cơ sở

Bạn sẽ phải tạo Cấu hình cơ sở cho hành trình điển hình của người dùng trong ứng dụng.

Trong ví dụ này, bạn có thể xác định ba hành trình sau:

  1. Khởi động ứng dụng (điều này rất quan trọng đối với hầu hết các ứng dụng)
  2. Cuộn danh sách các món ăn nhẹ
  3. Chuyển đến phần thông tin chi tiết về món ăn nhẹ

Để tạo Cấu hình cơ sở, chúng tôi sẽ thêm một lớp kiểm thử mới BaselineProfileGenerator vào mô-đun :macrobenchmark. Lớp này sử dụng quy tắc kiểm thử BaselineProfileRule chứa một phương thức kiểm thử để tạo hồ sơ. Điểm nhập để tạo hồ sơ là hàm collectBaselineProfile. Hàm này chỉ yêu cầu 2 tham số:

  • packageName là gói ứng dụng của bạn
  • profileBlock (tham số hàm lambda cuối cùng)
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

   @get:Rule
   val rule = BaselineProfileRule()

   @Test
   fun generate() {
       rule.collectBaselineProfile("com.example.macrobenchmark_codelab") {
           // TODO Add interactions for the typical user journeys
       }
   }
}

Trong hàm lambda profileBlock, bạn sẽ chỉ định các tương tác bao gồm hành trình điển hình của người dùng trong ứng dụng. Thư viện sẽ chạy profileBlock nhiều lần đồng thời thu thập các lớp và hàm đã gọi để được tối ưu hóa cũng như tạo Cấu hình cơ sở trên thiết bị.

Bạn có thể xem phát thảo của trình tạo Cấu hình cơ sở bao gồm các hành trình điển hình trong đoạn mã sau:

@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

   @get:Rule
   val rule = BaselineProfileRule()

   @Test
   fun generate() {
       rule.collectBaselineProfile("com.example.macrobenchmark_codelab") {
           startApplicationJourney() // TODO Implement
           scrollSnackListJourney() // TODO Implement
           goToSnackDetailJourney() // TODO Implement
       }
   }
}

Giờ thì hãy viết nội dung tương tác cho từng hành trình vừa đề cập. Bạn có thể viết dưới dạng hàm mở rộng của MacrobenchmarkScope để truy cập vào các tham số cũng như hàm mà hành trình này cung cấp. Cách viết này cho phép bạn sử dụng lại các tương tác với điểm chuẩn để xác minh mức tăng hiệu suất.

Bắt đầu hành trình khởi động ứng dụng

Đối với hành trình khởi động ứng dụng (startApplicationJourney), bạn cần đưa vào các lượt tương tác sau:

  1. Nhấn vào màn hình chính để đảm bảo trạng thái của ứng dụng được khởi động lại
  2. Khởi động Hoạt động mặc định và đợi khung hình đầu tiên hiển thị
  3. Chờ cho đến khi nội dung được tải và hiển thị để người dùng có thể tương tác với nội dung đó
fun MacrobenchmarkScope.startApplicationJourney() {
   pressHome()
   startActivityAndWait()
   val contentList = device.findObject(By.res("snack_list"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(Until.hasObject(By.res("snack_collection")), 5_000)
}

Hành trình cuộn của danh sách

Đối với hành trình cuộn danh sách các món ăn nhẹ (scrollSnackListJourney), bạn có thể thực hiện theo các tương tác sau:

  1. Tìm thành phần trên giao diện người dùng của danh sách các món ăn nhẹ
  2. Đặt lề cho cử chỉ sao cho thanh điều hướng hệ thống không được kích hoạt
  3. Cuộn danh sách này và đợi đến khi giao diện người dùng hoàn tất
fun MacrobenchmarkScope.scrollSnackListJourney() {
   val snackList = device.findObject(By.res("snack_list"))
   // Set gesture margin to avoid triggering gesture navigation
   snackList.setGestureMargin(device.displayWidth / 5)
   snackList.fling(Direction.DOWN)
   device.waitForIdle()
}

Chuyển sang hành trình chi tiết

Hành trình gần đây nhất (goToSnackDetailJourney) sẽ triển khai các hoạt động tương tác này:

  1. Tìm danh sách các món ăn nhẹ và tìm tất cả các món ăn nhẹ mà bạn muốn đưa vào
  2. Chọn một mục trong danh sách
  3. Nhấp vào mục đó và đợi cho đến khi màn hình chi tiết được tải. Bạn có thể tận dụng một lợi thế là danh sách các món ăn nhẹ sẽ không xuất hiện trên màn hình nữa
fun MacrobenchmarkScope.goToSnackDetailJourney() {
   val snackList = device.findObject(By.res("snack_list"))
   val snacks = snackList.findObjects(By.res("snack_item"))
   // Select random snack from the list
   snacks[Random.nextInt(snacks.size)].click()
   // Wait until the screen is gone = the detail is shown
   device.wait(Until.gone(By.res("snack_list")), 5_000)
}

Giờ thì bạn đã xác định được tất cả các lượt tương tác cần thiết giúp trình tạo Cấu hình cơ sở sẵn sàng chạy, nhưng trước tiên, bạn cần xác định thiết bị sẽ chạy trên đó.

6. Chuẩn bị một Thiết bị do Gradle quản lý

Để tạo Cấu hình cơ sở, trước tiên, bạn cần có một trình mô phỏng userdebug. Để tự động hóa quá trình tạo Cấu hình cơ sở, bạn có thể sử dụng Thiết bị do Gradle quản lý. Bạn có thể tìm hiểu thêm về Thiết bị do Gradle quản lý trong phần tài liệu của chúng tôi.

Trước tiên, hãy xác định Thiết bị do Gradle quản lý trong tệp :macrobenchmark mô-đun build.gradle như trong đoạn mã sau:

testOptions {
    managedDevices {
        devices {
            pixel2Api31(com.android.build.api.dsl.ManagedVirtualDevice) {
                device = "Pixel 2"
                apiLevel = 31
                systemImageSource = "aosp"
            }
        }
    }
}

Để tạo Cấu hình cơ sở, bạn cần sử dụng thiết bị Android 9 (API 28) trở lên đã bị can thiệp vào hệ thống.

Trong trường hợp này, chúng ta sẽ sử dụng Android 11 (API cấp 31) và hình ảnh hệ thống aosp có khả năng truy cập can thiệp vào hệ thống.

Thiết bị do Gradle quản lý cho phép bạn chạy thử nghiệm trên trình mô phỏng Android mà không cần khởi động và ngắt kết nối theo cách thủ công. Sau khi thêm định nghĩa vào build.gradle, tác vụ pixel2Api31[BuildVariant]AndroidTest mới đã có thể sẵn sàng chạy. Chúng tôi sẽ sử dụng tác vụ đó trong bước tiếp theo để tạo Cấu hình cơ sở.

7. Chạy kiểm thử trình tạo Cấu hình cơ sở

Sau khi thiết lập Thiết bị do Gradle quản lý, bạn có thể bắt đầu kiểm tra trình tạo.

Chạy trình tạo trong cấu hình chạy

Thiết bị do Gradle quản lý yêu cầu chạy thử nghiệm dưới dạng một tác vụ của Gradle. Để bắt đầu nhanh, chúng tôi đã tạo một cấu hình chạy để chỉ định tác vụ có tất cả các tham số cần thiết để chạy.

Để chạy cấu hình đó, hãy tìm cấu hình chạy generateBaselineProfile và nhấp vào nút Chạy 229e32fcbe68452f.png.

8f6b7c9a5da6585.png

Thử nghiệm sẽ tạo hình ảnh trình mô phỏng được xác định trước đó, chạy các lượt tương tác nhiều lần, sau đó chia nhỏ trình mô phỏng và cung cấp kết quả cho Android Studio.

4b5b2d0091b4518c.png

(Không bắt buộc) Chạy trình tạo từ dòng lệnh

Để chạy trình tạo từ dòng lệnh, bạn có thể tận dụng tác vụ do Thiết bị mà Gradle quản lý tạo – :macrobenchmark:pixel2Api31BenchmarkAndroidTest.

Lệnh này chạy tất cả các thử nghiệm không thành công trong dự án, vì mô-đun này cũng chứa các Điểm chuẩn để xác minh mức tăng hiệu suất sau này.

Để làm được việc đó, bạn có thể lọc lớp mà bạn muốn chạy bằng tham số -P android.testInstrumentationRunnerArguments.class và chỉ định com.example.macrobenchmark.BaselineProfileGenerator đã viết trước đó.

Toàn bộ lệnh sẽ có dạng như sau:

./gradlew :macrobenchmark:pixel2Api31BenchmarkAndroidTest -P android.testInstrumentationRunnerArguments.class=com.example.macrobenchmark.BaselineProfileGenerator

8. Áp dụng Cấu hình cơ sở đã tạo

Sau khi trình tạo hoàn tất, bạn cần thực hiện vài thao tác để Cấu hình cơ sở có thể hoạt động trong ứng dụng.

Bạn cần đặt tệp Cấu hình cơ sở đã tạo vào thư mục src/main (bên cạnh AndroidManifest.xml). Để truy xuất tệp, bạn có thể sao chép tệp này từ thư mục managed_device_android_test_additional_output/ nằm ở /macrobenchmark/build/outputs/ như minh họa trong ảnh chụp màn hình bên dưới.

b104f315f06b3578.png

Hoặc bạn có thể nhấp vào đường liên kết results trong kết quả của Android Studio và lưu nội dung đó hoặc sử dụng lệnh adb pull đã in trong kết quả.

Tiếp theo, bạn cần đổi tên tệp thành baseline-prof.txt.

8973f012921669f6.png

Sau đó, hãy thêm phần phụ thuộc profileinstaller vào mô-đun :app.

dependencies {
  implementation("androidx.profileinstaller:profileinstaller:1.2.0-rc01")
}

Việc thêm phần phụ thuộc này vào sẽ giúp bạn:

  • Thiết lập điểm chuẩn cục bộ cho Cấu hình cơ sở.
  • Sử dụng Cấu hình cơ sở trên các thiết bị Android 7 (API cấp 24) và Android 8 (Api cấp 26) không hỗ trợ Cấu hình đám mây.
  • Sử dụng Cấu hình cơ sở trên các thiết bị không có Dịch vụ Google Play.

Sau cùng, hãy đồng bộ hóa dự án với Tệp Gradle bằng cách nhấp vào biểu tượng 1079605eb7639c75.png.

40cb2ba3d0b88dd6.png

Ở bước tiếp theo, chúng ta sẽ xem cách xác minh hiệu suất của ứng dụng tốt hơn thông qua Cấu hình cơ sở.

9. Xác minh mức độ cải thiện hiệu suất khi khởi động

Chúng ta hiện đã tạo xong Cấu hình cơ sở và thêm vào ứng dụng. Vui lòng xác minh cấu hình này có ảnh hưởng như mong đợi đến hiệu suất của ứng dụng.

Hãy quay lại lớp ExampleStartupBenchmark có chứa điểm chuẩn để đo lường khả năng khởi động của ứng dụng. Bạn cần thay đổi một chút hàm thử nghiệm startup() để tái sử dụng được với các chế độ biên dịch khác nhau. Điều này cho phép bạn so sánh sự khác biệt khi sử dụng Cấu hình cơ sở.

CompilationMode

Tham số CompilationMode xác định cách biên dịch trước ứng dụng thành mã máy. Mã này có các tùy chọn sau:

  • DEFAULT – Hàm này biên dịch trước một phần ứng dụng bằng Cấu hình cơ sở nếu có (tùy chọn này được sử dụng nếu không áp dụng tham số compilationMode)
  • None() – Hàm này sẽ đặt lại trạng thái biên dịch ứng dụng và không biên dịch trước ứng dụng. Tính năng biên dịch đúng thời điểm (JIT) vẫn được bật trong quá trình thực thi ứng dụng.
  • Partial() – Hàm này biên dịch trước ứng dụng với Cấu hình cơ sở và/hoặc chạy thử.
  • Full() – Hàm này biên dịch trước toàn bộ mã ứng dụng. Đây là tùy chọn duy nhất trên thiết bị Android 6 (API cấp 23) trở xuống.

Nếu muốn bắt đầu tối ưu hóa hiệu suất của ứng dụng, bạn có thể chọn chế độ biên dịch DEFAULT, vì hiệu suất sẽ tương tự như khi ứng dụng được cài đặt từ Google Play. Nếu muốn so sánh các lợi ích về hiệu suất mà Cấu hình cơ sở cung cấp, bạn có thể so sánh kết quả giữa chế độ biên dịch NonePartial.

Sửa đổi thử nghiệm khởi động với CompilationMode khác

Trước tiên, hãy xóa chú thích @Test khỏi phương thức startup (vì thử nghiệm JUnit không thể chứa tham số), và thêm tham số compilationMode rồi sử dụng nó trong hàm measureRepeated:

// Remove @Test annotation and add the compilationMode parameter.
fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
   packageName = "com.google.samples.apps.sunflower",
   metrics = listOf(StartupTimingMetric()),
   iterations = 5,
   compilationMode = compilationMode, // Set the compilation mode
   startupMode = StartupMode.COLD
) {
   pressHome()
   startActivityAndWait()
}

Sau đó, hãy thêm hai hàm kiểm thử bằng CompilationMode khác nhau. Hàm kiểm thử đầu tiên sẽ sử dụng CompilationMode.None, nghĩa là trước mỗi điểm chuẩn, trạng thái của ứng dụng sẽ được đặt lại và ứng dụng sẽ không có mã được biên dịch trước.

@Test
fun startupCompilationNone() = startup(CompilationMode.None())

Hàm kiểm thử thứ hai sẽ tận dụng CompilationMode.Partial để tải Cấu hình cơ sở, đồng thời biên dịch trước các lớp cũng như hàm đã chỉ định trong cấu hình trước khi chạy phép đo điểm chuẩn.

@Test
fun startupCompilationPartial() = startup(CompilationMode.Partial())

Bạn có thể thêm một phương thức thứ ba sẽ biên dịch trước toàn bộ ứng dụng bằng cách sử dụng CompilationMode.Full. Đây là tùy chọn duy nhất trên thiết bị Android 6 (API cấp 23) trở xuống vì hệ thống chỉ chạy các ứng dụng được biên dịch đầy đủ.

@Test
fun startupCompilationFull() = startup(CompilationMode.Full())

Tiếp theo, hãy chạy phép đo điểm chuẩn như trước đây (trên thiết bị thực) và đợi Macrobenchmark đo thời gian khởi động với các chế độ biên dịch khác nhau.

Sau khi hoàn tất việc đo điểm chuẩn, bạn có thể thấy dấu thời gian trong kết quả Android Studio như ở ảnh chụp màn hình dưới đây:

cbbc9660374a438.png

Trên ảnh chụp màn hình, bạn có thể thấy thời gian khởi động ứng dụng của mỗi CompilationMode là khác nhau. Giá trị trung bình như hiển thị trong bảng sau:

timeToInitialDisplay [ms]

timeToFullDisplay [ms]

Không có

364.4

846.5

Toàn bộ

325.8

739.1

Từng phần

296.1

708.1

Theo quan sát, chế độ biên dịch None hoạt động kém hiệu quả nhất vì thiết bị phải biên dịch JIT nhiều nhất trong quá trình khởi động ứng dụng. Trái với quan sát thông thường, Full không phải là chế độ biên dịch hoạt động tốt nhất. Vì mọi thứ được biên dịch ở chế độ này khiến tệp odex của ứng dụng rất lớn, và do đó hệ thống thường phải thực hiện IO nhiều hơn đáng kể trong quá trình khởi động ứng dụng. Hiệu suất tốt nhất là trong chế độ Partial sử dụng Cấu hình cơ sở. Đó là do chế độ biên dịch một phần tạo ra sự cân bằng giữa việc biên dịch mã mà người dùng có nhiều khả năng sử dụng nhất, nhưng để không phải biên dịch mã không quan trọng trước, bạn không cần phải tải ngay mã đó.

10. Xác minh hiệu suất cuộn

Tương tự như các bước trước đó, bạn có thể đo lường và xác minh các điểm chuẩn cuộn. Hãy sửa đổi lớp ScrollBenchmarks tương tự như trước – thêm tham số vào thử nghiệm scroll và thêm nhiều thử nghiệm hơn bằng tham số của chế độ biên dịch khác.

Mở tệp ScrollBenchmarks.kt, sửa đổi hàm scroll() để thêm tham số compilationMode vào:

fun scroll(compilationMode: CompilationMode) {
        benchmarkRule.measureRepeated(
            compilationMode = compilationMode, // Set the compilation mode
            // ...

Bạn hiện đã có thể xác định các kiểm thử sử dụng nhiều tham số khác nhau:

@Test
fun scrollCompilationNone() = scroll(CompilationMode.None())

@Test
fun scrollCompilationPartial() = scroll(CompilationMode.Partial())

Đồng thời chạy phép đo điểm chuẩn như trước đây để có kết quả giống với ảnh chụp màn hình bên dưới: 249af52e917a4fcf.png

Từ kết quả, bạn có thể thấy là CompilationMode.Partial có thời gian kết xuất khung hình trung bình ngắn hơn 0,5 mili giây, thời lượng quá ngắn để người dùng có thể nhận ra, nhưng kết quả sẽ rõ ràng hơn đối với các phân vị khác. Đối với P90, sự khác biệt sẽ là 11,7 mili giây, tương đương 70% thời gian được chỉ định để tạo khung hình (trên Pixel 3 là ~ 16 mili giây).

11. Xin chúc mừng

Chúc mừng bạn đã hoàn thành lớp học lập trình này và cải thiện hiệu suất của ứng dụng bằng cách dùng Cấu hình cơ sở!

Tiếp theo là gì?

Kiểm tra kho lưu trữ GitHub về mẫu hiệu suất của chúng tôi, nơi chứa Macrobenchmark và các mẫu hiệu suất khác. Ngoài ra, vui lòng xem ứng dụng mẫu Now In Android – một ứng dụng thực sử dụng điểm chuẩn và Cấu hình cơ sở để cải thiện hiệu suất.

Tài liệu tham khảo