Công cụ và hiệu suất

Quy tắc giữ an toàn về kiểu và tối ưu hoá DEX

Loa

Giới thiệu

Giới thiệu về bài thuyết trình

Ứng dụng Android thường được phát hành khi chưa được tối ưu hoá đầy đủ do các quy tắc giữ lại quá nghiêm ngặt (thường là từ thư viện bên thứ ba), dẫn đến tệp APK bị phình to, thời gian khởi động nguội chậm hơn và hiệu suất thời gian chạy bị giảm sút.

Với ngưỡng hiệu suất mới của Google Play về việc tối ưu hoá DEX, khả năng hiển thị của ứng dụng hiện phụ thuộc trực tiếp vào việc duy trì hiệu suất tối ưu hoá.

Bài nói chuyện này giới thiệu một quy trình làm việc hiện đại của R8 để giải quyết tình trạng quy tắc bị quá tải. Bạn sẽ tìm hiểu cách:

  • Sử dụng trình phân tích cấu hình R8 và kỹ năng để xác định mức hao tổn của quy tắc cũ.
  • Chuyển đổi từ các quy tắc giữ ký tự đại diện rộng sang các chú thích được tạo chính xác, an toàn về kiểu.
  • Ngăn chặn tình trạng suy giảm hiệu suất tối ưu hoá bằng các công cụ Android CLI mới.

Tóm tắt

  • Google Play áp dụng ngưỡng chất lượng ứng dụng yêu cầu tối ưu hoá mã hơn 25%. Việc đáp ứng các yêu cầu này giúp giảm mức sử dụng bộ nhớ, đồng thời cải thiện tốc độ khởi động và tỷ lệ lỗi ANR.
  • Trình phân tích cấu hình R8 tạo một báo cáo HTML cho biết quy tắc giữ lại nào giữ lại mã. Công cụ này làm nổi bật các quy tắc được bao gồm và không được sử dụng, đồng thời đo lường tiềm năng tối ưu hoá trước khi biên dịch.
  • Quy tắc giữ lại quá rộng (thường được đưa ra để ngăn chặn sự cố phản chiếu hoặc được kế thừa từ thư viện) sẽ làm giảm khả năng tối ưu hoá ứng dụng. Một quy tắc ký tự đại diện duy nhất trên toàn gói có thể giữ lại hàng chục lớp không dùng đến.
  • Jetpack Keep sử dụng công cụ tìm lỗi mã nguồn để phát hiện phản chiếu và tạo chú thích chính xác cũng như quy tắc giữ lại có điều kiện ngay từ mã. Công cụ này xuất các quy tắc tiêu chuẩn dành cho người dùng tương thích với các phiên bản R8 cũ hơn.
  • Android CLI kiểm tra gói ứng dụng và AAR bằng logic chính xác của Google Play mà không cần tải lên. Công cụ này gắn cờ các quy tắc có vấn đề và xuất báo cáo JSON để kiểm thử hồi quy tự động.

Tài nguyên

Gemini Notebook

Chủ đề

  • dex-optimization
  • r8
  • keep-rules
  • jetpack-keep
  • android-cli
  • hiệu-suất-ứng-dụng

Bản chép lời

Giới thiệu về tính năng tối ưu hoá DEX

  • Xin chào mọi người!

  • Tôi là Breana, thuộc nhóm Quan hệ với nhà phát triển Android.

  • Còn tôi là Chris Craik, thuộc nhóm Hiệu suất Android.

  • Chào mừng bạn đến với các quy tắc giữ an toàn về kiểu và tối ưu hoá DEX.

  • Hôm nay, chúng ta sẽ nói về định nghĩa của việc tối ưu hoá DEX, lý do bạn nên quan tâm và một số thông tin cập nhật thú vị gần đây.

  • Từ lâu, việc tối ưu hoá DEX đã được coi là một việc nên làm.

  • Vì vậy, có thể bạn sẽ định cấu hình một lần rồi để nguyên, hoặc có thể bạn sẽ thực hiện khi có thời gian.

  • Tuy nhiên, trong bối cảnh ngành thiết bị di động đang phải đối mặt với những hạn chế đáng kể về nguồn cung phần cứng, ảnh hưởng đến dung lượng bộ nhớ của thiết bị, thì việc tối ưu hoá DEX là điều cần thiết để ứng dụng của bạn sử dụng ít bộ nhớ hơn, khởi động nhanh hơn, giảm lỗi ANR, đồng thời cải thiện hiệu suất kết xuất và thời gian chạy nói chung.

  • Để duy trì một hệ sinh thái Android lành mạnh và đảm bảo ứng dụng của bạn tiếp tục mang lại trải nghiệm cao cấp, Google Play đã công bố yêu cầu mới về chất lượng ứng dụng kèm theo các ngưỡng hiệu suất sẽ có hiệu lực từ tháng 2.

  • Trong đó có các ngưỡng để tối ưu hoá mã DEX.

  • Ứng dụng cần đạt mức trên 25% về rút gọn, tối ưu hoá và làm rối mã nguồn.

  • Vậy điều này có ý nghĩa gì đối với bạn, một nhà phát triển Android?

  • Mã DEX chưa tối ưu hoá không chỉ ảnh hưởng tiêu cực đến trải nghiệm người dùng mà còn có thể ảnh hưởng đến khả năng hiển thị và phát hành của ứng dụng trên Cửa hàng Play.

  • Tuy nhiên, bạn đã đến đúng nơi và khi kết thúc buổi trò chuyện này, bạn sẽ có các công cụ để không chỉ đáp ứng mà còn vượt qua các yêu cầu này.

  • Bạn có thể tối ưu hoá mã DEX bằng R8, trình tối ưu hoá ứng dụng của Android.

  • Mặc dù R8 không bắt buộc nếu một trình tối ưu hoá ứng dụng khác có thể giúp ứng dụng của bạn vượt qua các ngưỡng này, nhưng đây là cách nên dùng và được tích hợp trực tiếp vào chuỗi công cụ của Android.

  • Mặc dù tệp DEX và gói ứng dụng được tối ưu hoá tốt sẽ sử dụng ít bộ nhớ hơn, nhưng lợi ích chính ở đây vẫn liên quan đến hiệu suất: ít lỗi ANR hơn, khởi động nhanh hơn và cải thiện hiệu suất thời gian chạy.

  • Việc tối ưu hoá ứng dụng mang lại kết quả tích cực, thực tế và đã được chứng minh cho ứng dụng và người dùng của bạn.

  • Ví dụ: bằng cách áp dụng chế độ đầy đủ của R8 và tinh chỉnh các quy tắc giữ lại, Grab đã có thể tối ưu hoá 78%, thu nhỏ 79% và làm rối mã nguồn 82% cho ứng dụng của họ.

  • Có thể bạn đang nhìn vào trang trình bày này và nghĩ: "Được rồi, những con số đó khá cao, nhưng thực tế thì sao?"

  • Nhờ những điểm cải tiến này, kết hợp với Hồ sơ cơ sở, họ đã giảm 32% số lỗi ANR và cải thiện 35% thời gian khởi động.

  • Khi các ứng dụng hàng đầu lần đầu sử dụng R8, họ nhận thấy kích thước DEX giảm trung bình 38% và tỷ lệ lỗi ANR mà người dùng nhận thấy giảm 16%.

  • Và nếu bạn đang sử dụng R8, chúng tôi có tin vui cho bạn.

  • Đối với những ứng dụng đã bật R8, sau đó thực hiện các bước để cải thiện hơn nữa mức độ tối ưu hoá, thu nhỏ và làm rối mã nguồn lên hơn 50%, thì trung bình, họ nhận thấy kích thước DEX giảm thêm 25%, thời gian khởi động giảm 4% và lỗi ANR mà người dùng nhận thấy giảm thêm 17%, ngoài mức giảm 16% mà chúng tôi đã trình bày trong trang trình bày trước.

  • Vì vậy, hôm nay chúng tôi muốn hướng dẫn bạn cách đạt được những con số như thế này.

  • Chúng tôi sẽ đề cập đến các công cụ có thể giúp bạn tối ưu hoá mã DEX và chuyển từ các quy tắc giữ lại quá rộng và bị định cấu hình sai sang các quy tắc cụ thể và hẹp.

  • Trước tiên, chúng tôi cũng muốn đề cập rằng R8 đã có mặt từ nhiều năm nay.

  • Chúng tôi cũng nhận thấy rằng mặc dù tính năng này giúp tối ưu hoá hiệu quả, nhưng vẫn cần phải thiết lập rất cẩn thận.

  • Lệnh gọi phản chiếu có nghĩa là dữ liệu đầu vào không thể dự đoán được và có thể dẫn đến sự cố trong thời gian chạy.

  • Điều này đã khiến cả nhóm ứng dụng và nhóm thư viện trước đây thêm các quy tắc giữ lại quá rộng chỉ để đảm bảo an toàn.

  • Chúng tôi biết rằng 41% ứng dụng hàng đầu có mức độ tối ưu hoá dưới 25% theo định nghĩa của Google Play, nhưng chúng tôi cũng biết rằng 13% ứng dụng hàng đầu đã đạt được mức tối ưu hoá gấp ba lần ngưỡng đó.

  • Và đây là những ứng dụng thuộc nhiều ngành và hành trình của người dùng có thể đạt được con số này.

  • Và bạn có thể đạt được điểm tối ưu hoá cao cho ứng dụng của mình, miễn là bạn có các công cụ phù hợp và khả năng hiển thị phù hợp để quản lý cả quy tắc giữ lại của riêng bạn và những quy tắc được kế thừa từ thư viện.

  • Trước khi tìm hiểu sâu về các công cụ và điểm cải tiến mới giúp ích cho việc này, chúng tôi cũng muốn nêu bật một số điểm cải tiến khác của R8 trong năm qua, giúp việc tối ưu hoá này trở nên dễ dàng hơn một chút.

  • Trước tiên, các mẫu ứng dụng mới bao gồm phạm vi gói, giúp đảm bảo mức tối ưu hoá tối thiểu ngay từ đầu.

  • Trình phân tích cấu hình R8 (chúng ta sẽ nói đến ngay sau đây) hiện được hỗ trợ trên các phiên bản AGP cũ.

  • Ngoài ra, Bazel cũng hỗ trợ không chính thức cho tính năng này.

  • Trong AGP 9.2, các dữ liệu gốc coroutine nhanh gấp đôi.

  • Nhờ những điểm cải tiến của R8 trong AGP 9.2, Tinder nhận thấy tỷ lệ ANR giảm 13% sau khi nâng cấp từ phiên bản 9.0.1.

  • AGP 9.3 cũng mang đến một API R8 được thiết kế lại với cách đặt tên rõ ràng hơn, các giá trị mặc định đơn giản hơn cho các quy tắc mặc định và tính năng rút gọn tài nguyên, cũng như hỗ trợ thư mục nguồn AGP cho các quy tắc giữ lại.

    android {
        buildTypes {
            release {
                optimization {
                    // no need to specify default
                    // rules and resource shrinking
                    enabled = true
                }
            }
        }
        ...
    }
    android {
        buildTypes {
            release {
                // Enables code optimizations.
                isMinifyEnabled = true
    
                // Enables resource shrinking.
                isShrinkResources = true
    
                proguardFiles(
                    // Default rules.
                    getDefaultProguardFile(
                        "proguard-android-optimize.txt"
                    ),
                    ... // list out each file with keep rules
                )
                ...
            }
        }
        ...
    }
  • Những nội dung cập nhật mà chúng tôi sẽ đề cập hôm nay cũng là những điểm cải tiến cho trải nghiệm của nhà phát triển.

  • Chúng ta sẽ bắt đầu bằng cách tìm hiểu cách tận dụng Trình phân tích cấu hình R8 và kỹ năng R8 để xác định mức hao tổn của quy tắc cũ và tìm ra hướng cải thiện.

  • Sau đó, Chris sẽ trình bày cách chuyển đổi từ các quy tắc giữ lại ký tự đại diện rộng sang các chú thích mới, chính xác, an toàn về kiểu và được tạo tự động.

  • Cuối cùng, chúng ta sẽ tìm hiểu cách sử dụng các công cụ xác thực CLI mới để đảm bảo tối ưu hoá mạnh mẽ mà không gây ra sự cố.

Tổng quan về trình phân tích cấu hình R8

  • Vì vậy, Trình phân tích cấu hình R8 là nơi lý tưởng để bắt đầu tìm hiểu về việc tối ưu hoá DEX.

  • Nếu bạn chưa từng sử dụng công cụ này, thì đây là giao diện của công cụ.

  • Đây là báo cáo HTML trực quan hoá thông tin về cấu hình R8 của bạn.

  • Báo cáo này dành cho ứng dụng của tôi, trong đó có một thư viện tên là Analytics Bloat.

  • Và như bạn có thể đoán, đây không phải là một ứng dụng thực hay thư viện thực, nhưng nó được đánh dấu bằng màu xanh dương ở trên cùng.

  • Trình phân tích cấu hình đo lường những thông tin sau: thu gọn, tức là tỷ lệ phần trăm các lớp, phương thức và trường mà R8 không bị các quy tắc giữ lại ngăn thu gọn.

  • Và giá trị này được tính sau khi R8 thực hiện lượt thu hẹp đầu tiên.

  • Chỉ số này cũng đo lường mức độ tối ưu hoá, tức là tỷ lệ phần trăm mã có thể áp dụng các biện pháp tối ưu hoá toàn bộ chương trình như chèn cùng dòng phương thức và hợp nhất lớp.

  • Và làm rối mã nguồn, tức là tỷ lệ phần trăm thành viên có thể bị làm rối mã nguồn hoặc đổi tên thành giá trị nhận dạng ngắn hơn.

  • Tôi cũng muốn đề cập rằng vì Trình phân tích cấu hình R8 tính toán các điểm số này trước khi tối ưu hoá và biên dịch DEX, nên các giá trị mà bạn thấy trong báo cáo này về việc tối ưu hoá, rút gọn và làm rối mã nguồn sẽ không khớp chính xác với các giá trị mà bạn thấy trong Play Console.

  • Nguyên nhân là do Play đo lường kết quả DEX cuối cùng.

  • Và lý do cho sự khác biệt này là để đảm bảo rằng khi phát triển, bạn có thể truy cập vào một vòng hồi tiếp cục bộ, nhanh chóng.

  • Tuy nhiên, điểm số bạn thấy trong Trình phân tích cấu hình R8 vẫn rất hữu ích trong việc xác thực tiềm năng tối ưu hoá DEX trước khi bạn tải lên Cửa hàng Play.

  • Báo cáo này cũng bao gồm bảng Phân tích quy tắc giữ lại, đây là chế độ xem chính mà bạn thấy khi truy cập vào báo cáo.

  • Từ đó, bạn có thể nhấp vào bất kỳ quy tắc nào và xem số lượng chính xác các lớp, phương thức và trường mà một quy tắc cụ thể đang giữ lại để có thể biết quy tắc nào đang giữ lại nhiều hơn mức cần thiết.

  • Nó cũng có các quy tắc được bao gồm, đây là những gì được phóng to ở đây.

  • Và đây là những quy tắc trùng lặp với một quy tắc rộng hơn.

  • Những từ khoá này sẽ được làm nổi bật để bạn có thể xoá từ khoá rộng và chỉ giữ lại từ khoá hẹp hơn.

  • Công cụ này cũng cho bạn thấy các quy tắc không dùng đến, tức là những quy tắc có thể xoá mà không gây rủi ro cho độ ổn định của ứng dụng.

  • Vì vậy, Trình phân tích cấu hình được thiết kế để giải quyết một số vấn đề, bắt đầu từ việc tích luỹ quy tắc cũ.

  • Trước đây, khi không có chế độ hiển thị theo quy tắc, việc xoá các quy tắc cũ có thể gây ra rủi ro.

  • Vì vậy, các nhóm thường bỏ qua hoặc chỉ thêm các ký tự đại diện rộng hơn.

  • Thứ hai, việc này giúp bạn phát hiện các điểm mù trong quy tắc thư viện.

  • Vì vậy, khi bạn thêm một SDK trước đây, rất khó để biết liệu một quy tắc đi kèm với thư viện đó có quá rộng hay không, hoặc liệu quy tắc đó có cần thiết hay không, chẳng hạn như sau khoảng 10 phiên bản.

  • Chúng ta cũng sẽ tìm hiểu thêm về thư viện trong phần sau.

  • Thứ ba, báo cáo này giúp bạn đánh giá mức tác động của việc tối ưu hoá.

  • Trước đây, không có cách nào dễ dàng để biết một quy tắc đang giữ lại 2 lớp hay 2.000 lớp.

  • Hãy cùng xem cách R8 hoạt động trong bối cảnh ứng dụng của tôi và thư viện mà chúng ta đã xem trước đó.

  • Vì vậy, ở đây tôi có một mô-đun ứng dụng và ba thư viện.

  • Và hãy chú ý đến thư viện thứ ba ở bên phải.

  • Đó là thư viện Analytics Bloat, bao gồm một quy tắc lưu giữ trên toàn gói.

  • Trước tiên, R8 xác định điểm truy cập gốc từ tất cả quy tắc giữ lại đã hợp nhất, được đánh dấu màu vàng tại đây.

  • Và nhìn chung, đây phải là các điểm truy cập chính của ứng dụng, chẳng hạn như các lớp trong tệp kê khai.

  • Và do quy tắc giữ ký tự đại diện của thư viện Analytics Bloat, mọi lớp trong thư viện đều được coi là một điểm truy cập.

  • Tiếp theo, R8 sẽ theo dõi khả năng kết nối trên biểu đồ lệnh gọi từ các gốc đó.

  • Bất kỳ lớp nào có thể truy cập được từ một gốc đều được đánh dấu để giữ lại bằng màu xanh lục, trong khi các lớp không được tham chiếu được phân loại là mã chết và được đánh dấu bằng màu đỏ tại đây.

  • Sau đó, R8 sẽ thực hiện rung cây và loại bỏ mã chết không được tham chiếu.

  • Vì vậy, sau khi loại bỏ mã không dùng đến hoặc sau lần thu hẹp đầu tiên, đây là những gì còn lại.

  • Ứng dụng của chúng tôi có tỷ lệ thu nhỏ là 42,5%, cao hơn ngưỡng 25% của Google Play, nhưng vẫn chưa đạt mức chúng tôi mong muốn.

  • Và điều này phần lớn là do quy tắc giữ lại trên toàn gói từ thư viện Analytics.

  • Và điều đó khiến chỉ có 17 trong số 40 lớp còn lại có thể được thu nhỏ thêm.

  • Và điểm mà tôi muốn nhấn mạnh ở đây là một quy tắc giữ lại không phù hợp có thể gây ra các vấn đề lớn cho việc tối ưu hoá ứng dụng.

  • Vì vậy, hãy nhanh chóng tập trung vào các thư viện.

  • Trong năm qua, nhóm của chúng tôi đã đầu tư rất nhiều vào việc tìm hiểu xem các quy tắc giữ lại của người dùng thư viện ảnh hưởng như thế nào đến việc tối ưu hoá cho hệ sinh thái.

  • Nhiều thư viện sẽ đi kèm với các quy tắc quá rộng để đảm bảo tính ổn định.

  • Hoặc họ cũng sẽ chỉ định các quy tắc chung trong tài liệu để nhà phát triển ứng dụng tự thêm vào ứng dụng của mình.

  • Tuy nhiên, việc phát hành ứng dụng có các quy tắc rộng không cần thiết cho độ ổn định và ảnh hưởng tiêu cực đến hiệu suất, trong đó chi phí chủ yếu do bạn (nhà phát triển ứng dụng) chịu.

  • Chúng tôi đã thấy những trường hợp mà toàn bộ thư viện được giữ lại theo quy tắc giữ lại trên toàn gói, chẳng hạn như thư viện phân tích quá lớn. Trong trường hợp của thư viện đó, điều này có tác động sâu rộng đến tiềm năng tối ưu hoá của ứng dụng.

  • Tôi cũng muốn đề cập rằng ngay cả SDK của Google cũng không tránh khỏi vấn đề này và hơn một chục thư viện của Google đã khắc phục các quy tắc giữ lại quá rộng trong năm qua.

  • Vì vậy, nếu bạn muốn sử dụng trình phân tích cấu hình, thì trình này được tích hợp trực tiếp vào AGP 9.3, nhưng cũng có sẵn cho các phiên bản cũ hơn nếu bạn nâng cấp phiên bản R8.

  • Vì vậy, khi chạy lệnh này, bạn sẽ tạo được báo cáo HTML mà chúng tôi đã trình bày, cho thấy các quy tắc giữ lại, tác động đến việc thu nhỏ, tối ưu hoá, làm rối mã nguồn cho từng quy tắc và toàn bộ ứng dụng của bạn.

  • Nếu bạn đang chạy một bản phát hành R8 đầy đủ, thì báo cáo sẽ được tạo tự động và bạn có thể tìm thấy báo cáo đó tại đường dẫn tệp này.

  • Lợi ích của tác vụ độc lập này là mất ít thời gian hơn để tạo so với bản dựng đầy đủ.

Nghiên cứu điển hình: Tối ưu hoá WHOOP

  • Nhóm tại WHOOP, có thể đang ở đâu đó tại đây, đã chạy trình phân tích cấu hình và nhận được kết quả thu hẹp ở mức 74%, làm rối mã nguồn ở mức 74% và tối ưu hoá ở mức 0.

  • Và như bạn có thể hình dung, nhóm thực sự ngạc nhiên khi thấy giá trị tối ưu hoá này.

  • Thông tin chi tiết từ trình phân tích cấu hình đã giúp họ phát hiện ra rằng bản dựng của họ đang sử dụng một tệp ProGuard cũ đã tắt tính năng tối ưu hoá. Do đó, bằng cách chuyển sang phiên bản mới, họ đã có thể bật tính năng tối ưu hoá cho ứng dụng của mình.

  • Điều này giúp tối ưu hoá 55% và giảm 4% kích thước tệp APK.

  • Khi kết hợp với Hồ sơ cơ sở, họ đã cải thiện thời gian bắt đầu nguội thêm 38% và giảm 28% số khung hình bị treo quá mức.

  • Màn hình bên trái không có Hồ sơ cơ sở và màn hình bên phải có Hồ sơ cơ sở. Như bạn có thể thấy, ứng dụng khởi động nhanh hơn khi có thêm Hồ sơ cơ sở.

  • Hãy phát lại một lần nữa.

  • Chúng ta tìm hiểu tiếp nhé.

  • Đường liên kết này sẽ đưa bạn đến hướng dẫn về trình phân tích cấu hình. Việc xem xét báo cáo theo cách thủ công là rất tốt, nhưng như bạn có thể hình dung, tuỳ thuộc vào trạng thái cấu hình R8 của ứng dụng, việc này có thể mất nhiều thời gian.

  • Vì vậy, để đẩy nhanh tiến độ, chúng ta có kỹ năng Trình phân tích R8.

    $ android skills list
    $ android skills add r8-analyzer
  • Bạn có thể yêu cầu tác nhân phân tích cấu hình R8 của mình. Kỹ năng này không chỉ tạo báo cáo trình phân tích cấu hình R8 mà còn chỉ ra cho bạn những quy tắc có tác động mạnh nhất.

  • Dưới đây là kết quả mẫu. Để nêu bật một vài chi tiết, kết quả này cho thấy điểm tối ưu hoá là 42%, nghĩa là 52% – chính xác hơn là 58% – của toàn bộ mã nguồn không thể được tối ưu hoá bằng R8.

  • Nếu xem xét cụ thể phần quy tắc giữ lại, thì quy tắc giữ lại trên toàn bộ gói cho thư viện bloat của Analytics sẽ ngăn 22% toàn bộ mã nguồn được tối ưu hoá. Kỹ năng này cũng đề xuất chúng ta tinh chỉnh quy tắc giữ lại này vì như chúng ta đã biết, quy tắc này quá rộng.

  • Giờ đây, bạn có thể chẩn đoán nguyên nhân gây ra tình trạng phình to ứng dụng và chặn tính năng tối ưu hoá DEX bằng trình phân tích cấu hình R8 và kỹ năng này.

Những thách thức của việc phản ánh trong quá trình tối ưu hoá DEX

  • Nhưng sau khi tìm thấy một quy tắc giữ lại quá rộng, làm thế nào để bạn thay thế quy tắc đó một cách an toàn mà không vô tình gây ra sự cố ngoài ý muốn?

  • Tôi sẽ chuyển sang Chris, người sẽ hướng dẫn bạn cách chuyển đổi từ các quy tắc giữ lại cũ sang chú thích giữ lại mới được tạo tự động và an toàn về kiểu.

  • Cảm ơn Breana.

  • Được rồi, hãy nói về cách chúng ta đạt được điều này.

  • Với thư viện phân tích cồng kềnh này, cùng với tất cả các quy tắc giữ lại này – với quy tắc giữ lại này, về cơ bản là giữ lại mọi thứ, vấn đề cốt lõi mà chúng ta gặp phải là sự phản ánh.

  • Vì vậy, hãy xem xét một chút về mã phản chiếu như một lời nhắc nhỏ ở đây.

    fun createWithReflection(className: String): MyObject {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as MyObject
    }
  • Đây là một hàm tạo thực thể đối tượng chỉ tăng cường một đối tượng cụ thể và thực hiện việc này một cách phản chiếu.

  • Giờ đây, nhược điểm của việc này trong bối cảnh tối ưu hoá DEX là nó sẽ trì hoãn tất cả các lệnh gọi tĩnh này cho đến thời gian chạy.

  • Chỉ bằng cách xem mã, tuỳ thuộc vào nguồn gốc của tên lớp này, trình tối ưu hoá R8 của chúng tôi không thể biết được điều gì đang xảy ra ở đây.

  • Đây là những trường hợp làm hỏng tính năng tối ưu hoá Dex theo mặc định, vì vậy, thông thường bạn sẽ phải thêm quy tắc Keep.

  • Bây giờ, lý tưởng nhất là bạn chỉ cần tránh điều này, phải không?

  • Lý tưởng nhất là chúng ta nên tránh hoàn toàn việc phản chiếu, đặc biệt là vào năm 2026, khi chúng ta có các công cụ tạo mã tuyệt vời để chuyển đổi tuần tự, chèn phần phụ thuộc, có thể loại bỏ hoàn toàn loại vấn đề này.

  • Nhưng đó không phải là tất cả, đúng không?

  • Chúng ta vẫn cần đến phản chiếu trong một số trường hợp nhất định.

  • Giờ thì hãy cùng tìm hiểu xem chính xác thì điều này đã xảy ra như thế nào trong thư viện cồng kềnh này.

  • Sau đây là một ví dụ cụ thể và phổ biến về phần phụ thuộc không bắt buộc.

    // Problem 1 - optional dependency
    fun tryInitCrashReporter(context: Context): EventTransport? {
       return runCatching {
           Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
               .getDeclaredConstructor(Context::class.java)
               .newInstance(context) as? EventTransport
       }.getOrNull()
    }
    
    // Problem 2 - dynamically instantiate subclass
    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
    
    // Problem 3 - serialization
    fun serializeEvent(event: UserActionEvent): JSONObject {
       val json = JSONObject()
       for (field in event.javaClass.declaredFields) {
           field.isAccessible = true
           json.put(field.name, field.get(event))
       }
       return json
    }
  • Ở đây, chúng ta đang tăng cường một loại cụ thể không truy cập được trên classpath của thư viện, vì vậy, chúng ta phải thực hiện việc này bằng một chuỗi.

  • Điều này tham chiếu đến lớp cụ thể có thể có hoặc không có tại thời điểm thư viện được tạo, vì vậy, thư viện này sử dụng phản chiếu để tìm một thành viên bên trong thư viện liên quan.

  • Một trường hợp khác mà chúng ta có ở đây là tạo thực thể cho các lớp con một cách linh động.

  • Vì vậy, đặc biệt là nếu có nội dung nào đó xuất phát từ tệp kê khai hoặc được gửi từ phía máy chủ, chúng tôi sẽ thông báo cho ứng dụng cục bộ rằng: "Này, vui lòng tăng cường nội dung này, tạo nội dung này muộn".

  • Và sau đó, danh mục thứ ba mà chúng ta thấy ở đây là tuần tự hoá song song.

  • Ý tôi là, tôi có một quá trình chuyển đổi tuần tự JSON đang diễn ra bằng cách lặp lại tất cả các thành viên.

  • Giờ thì, điều này hoàn toàn kinh tởm, nhưng, bạn biết đấy, đôi khi những thứ như thế này vẫn tồn tại.

  • Không phải lúc nào cũng được khắc phục ngay khi cần.

  • Nhưng hãy nói về kết quả của việc này, đúng không?

  • Khi các lệnh gọi phản chiếu này chồng chất, bạn sẽ có thêm động lực để làm những việc dễ dàng và tối ưu hoá trải nghiệm của nhà phát triển thay vì trải nghiệm của người dùng.

  • Ý tôi là, điều đó khá tệ, vì vậy tôi sẽ thêm một việc cần làm và mọi thứ sẽ ổn thôi.

    # TODO: clean this up
    -keep com.example.analytics.bloat.** { * };
  • Một ngày nào đó, tôi sẽ quay lại và sửa nó, phải không?

  • Hãy cùng nói về những việc chúng ta nên làm, ít nhất là trước hôm nay, cách bạn viết một quy tắc Lưu giữ lý tưởng tại đây.

    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
  • Được rồi, hãy chuẩn bị sẵn sàng.

  • Vì vậy, phần đầu tiên ở đây là chúng ta muốn giữ lại lớp cụ thể mà chúng ta đang trả về từ hàm phản chiếu này.

    ### Keep the EventFilter
    -keep class com.example.analytics.bloat.EventFilter {
        *
    }
  • Thực ra, không, chờ đã.

  • Bạn biết gì không?

  • Điều này thực ra không đúng.

  • Đây là một lớp Kotlin, tôi cần đảm bảo lớp này được nhập theo chuỗi, hãy để tôi đưa KT vào đó.

    ### Keep the actual EventFilter
    -keep class com.example.analytics.bloat.EventFilterKt {
        *
    }
  • Và trên thực tế, điều quan trọng ở đây là tôi không thực sự trả về một EventFilter, mà thực ra tôi đang trả về một lớp con của EventFilter, vì vậy tôi cần giữ lại các lớp mở rộng EventFilter để có thể tăng cường lớp này một cách chính xác.

    ### Keep the EventFilter subclasses
    -keep class ** extends com.example.analytics.bloat.EventFilterKt {
        *
    }
  • Và thực sự, nếu tôi muốn làm việc này hiệu quả hơn, thì cách đó sẽ hiệu quả, nhưng để hiệu quả hơn, với tư cách là một nhà phát triển thư viện giỏi, tôi nên làm gì hơn nữa?

  • Tôi nên nói -keepclasseswithmembers và chỉ giữ lại những lớp có hàm khởi tạo <init> cụ thể mà tôi đang dùng để tăng cường đối tượng.

    ### Keep the EventFilter subclasses, if they have this constructor
    -keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
       void <init>();
    }
  • Và thực sự, nếu tôi muốn làm tốt hơn, tôi sẽ thêm allowaccessmodification để cho phép tối ưu hoá hơn nữa trên tất cả các API đang được nhắm mục tiêu theo quy tắc Keep này vì tôi không muốn ngăn chặn việc tối ưu hoá hơn nữa từ R8, tôi muốn cố gắng và trở thành một công dân tốt.

    ### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
    -keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
       void <init>();
    }
  • Và bước cuối cùng, hầu như không ai làm vì phải làm thêm một chút, nhưng lại rất hữu ích cho thư viện, đó là đặt điều kiện.

    ### -if makes this conditional, critical for libraries
    -if class com.example.analytics.bloat.EventToolsKt {
        class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
    }
    ### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
    -keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
        void <init>();
    }
  • Giờ đây, về mặt lý thuyết, bạn không cần giữ lại tất cả các lớp con đó trừ phi bạn thực sự gọi vào hàm này, và đó là chức năng của câu lệnh if này ở đầu quy tắc Keep.

  • Nhưng, bạn biết đấy, tôi có thể yêu cầu giơ tay, nhưng tôi biết hầu như không ai làm điều này, phải không?

  • Chẳng hạn như viết mã, đặt điều kiện.

  • Vậy, nếu chúng ta có thể cải thiện điều này thì sao?

  • Đầu tiên, hãy cùng xem, thích, được rồi.

  • Có lẽ có cách khác để làm việc này.

  • Vì vậy, chúng tôi đã thảo luận về việc, điều gì sẽ xảy ra nếu tôi viết một quy tắc Keep hẹp, được viết thủ công?

  • Về mặt kỹ thuật thì có thể, nhưng khó viết, khó đọc và rất khó duy trì vì có nhiều chuỗi nhỏ dễ bị hỏng khi có sự cố xảy ra.

  • Bên dưới một thứ gì đó, bên dưới nó thay đổi.

  • Và chúng tôi có các giải pháp thay thế.

  • Chúng ta có các chú thích như @Keep.

  • Giờ đây, @Keep đã có trong gói chú thích AndroidX từ lâu, nhưng thực ra hiện nay không nên dùng chú thích này.

    @androidx.annotation.Keep
    class EventFilter { … }
    
    @androidx.annotation.Keep
    class SizeBasedEventFilter { … }
    
    @androidx.annotation.Keep
    class DropLogsEventFilter { … }
  • Đây không phải là giải pháp phù hợp cho nhà phát triển thư viện và trên thực tế, giải pháp này bị cấm trong nội bộ vì mặc dù dễ duy trì hơn nhưng lại rất thiếu linh hoạt và không có một số tính năng tối ưu hoá mà tôi đã đề cập trước đó.

  • Nó không thể.

  • Nó không cho phép phân tích trang web cuộc gọi đó.

  • Bây giờ, nếu chúng ta xem xét trường hợp cụ thể mà chúng ta đã có trước đây, "Ồ, tôi chỉ cần đặt @Keep vào EventFilter của mình, thế là được rồi, phải không?"

    @Keep
    class EventFilter { … }
  • À, không.

  • @Keep không thực sự nhắm đến các lớp con, mà chỉ nhắm đến lớp đó.

    @Keep
    class EventFilter { … }
    
    @Keep
    class SizeBasedEventFilter { … }
    
    @Keep
    class DropLogsEventFilter { … }
  • Vì vậy, thực ra tôi cần nhắm đến tất cả các lớp con và hiện tại, tôi đang viết nhiều chú thích @Keep hơn so với khi chỉ dùng một quy tắc Keep khó viết.

  • Vậy cả hai cách này đều có vấn đề, đúng không?

  • Bạn dễ quên, biết muộn và theo mặc định, các lệnh gọi lại này không phụ thuộc vào người gọi.

  • Đặc biệt là đối với các thư viện, chúng tôi muốn có một cách tốt hơn.

  • Chúng tôi muốn có một cách dễ dàng hơn để thực hiện mọi việc ngay từ đầu.

  • Và một trong những vấn đề lớn mà chúng ta thấy ở đây là những thứ này đang sống xa sự phản chiếu mà chúng ta đang thực sự bảo vệ.

  • Cuối cùng, giải pháp mà chúng ta muốn là một giải pháp nằm ngay bên cạnh mã phản chiếu không hợp lệ ở đây, thực sự đang tăng cường các mục tiêu.

Giới thiệu về Jetpack Keep

  • Vì vậy, hãy bắt đầu nói về Jetpack Keep, thư viện và trình bổ trợ mới của AndroidX được thiết kế để giúp giải quyết chính xác vấn đề này.

  • Vì vậy, để sử dụng – mục tiêu của Jetpack Keep là trước tiên bắt đầu bằng cách tạo quy tắc giữ lại từ chú giải, nhưng thậm chí còn xa hơn thế, tạo chú giải trực tiếp từ phản chiếu đã có trong toàn bộ mã nguồn của bạn.

  • Vì vậy, việc thiết lập rất đơn giản.

    plugins {
       id("com.android.library")
       id("androidx.annotation.keep")
    }
    
    dependencies {
        implementation("androidx.annotation.keep:keep")
        ...
    }
  • Đây là một trình bổ trợ Gradle, một thư viện chú thích.

  • Nhưng hãy xem cách mã này thực sự chạy trong thời gian xây dựng.

  • Vậy nếu tôi có ví dụ trước đó, trong đó tôi đang tăng cường một lớp con của một loại đã biết, thì tôi phải làm gì?

  • Tôi chỉ chạy lintFix.

    ./gradlew analyticsbloat:lintFix
  • Lint ở đây phân tích tất cả phản ánh đang diễn ra bên trong phương thức này và cho biết: "Này, bạn cần chú thích này".

    @UsesReflectionToConstruct(
       classConstant = EventFilter::class,
       parameterTypes = []
    )
    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
  • Chú thích này thể hiện chính xác chức năng của phản chiếu bên trong lệnh gọi này.

  • Nó cho biết, "Tôi đang tăng cường một lớp thuộc loại này".

  • Trong trường hợp cụ thể này, EventFilter là một lớp đã biết.

  • Tôi không dùng chuỗi để tham chiếu vì không cần thiết.

  • Đó là tính năng an toàn bổ sung và thậm chí tôi còn mã hoá parameterTypes để cho biết chính xác hàm khởi tạo nào đang được giữ lại.

  • Nhưng điều này có tác dụng gì?

  • Ví dụ: làm thế nào để thông tin này giúp ích cho bản dựng R8 cuối cùng?

  • Nó tạo ra quy tắc giữ lại và như bạn thấy trước đó, đây chính là quy tắc giữ lại lý tưởng mà chúng ta đã có trước đây.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.EventTools {
        class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
    }
    -keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
        void <init>();
    }
  • Nó nhắm đến chính xác các lớp, lớp con và có điều kiện trên điểm phản chiếu này.

  • Và tôi không cần phải duy trì một-- tôi không cần phải quan tâm đến việc chú thích tất cả các lớp con.

  • Tôi không cần phải lưu giữ một tệp quy tắc giữ lại riêng biệt ở bất kỳ nơi nào khác.

  • Đây là cách dễ dàng hơn nhiều để quản lý các quy tắc lưu giữ.

  • Hãy xem lại ví dụ về phần phụ thuộc không bắt buộc đó.

    @UsesReflectionToConstruct(
       className = "com.example.analytics.bloat.crash.CrashReporterTransport",
       parameterTypes = [Context::class]
    )
    fun tryInitCrashReporter(context: Context): EventTransport? {
       return runCatching {
           Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
               .getDeclaredConstructor(Context::class.java)
               .newInstance(context) as? EventTransport
       }.getOrNull()
    }
  • Bây giờ, lớp không nằm trên đường dẫn lớp, nhưng tôi vẫn có thể tham chiếu lớp đó.

  • Tôi chỉ làm như vậy bằng cách sử dụng một chuỗi, nhưng mọi thứ khác, các loại tham số, chúng có thể được tham chiếu bằng các tham chiếu lớp, vì vậy tôi làm.

  • Và một lần nữa, chúng ta tạo quy tắc giữ lại lý tưởng cho trường hợp này.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.CrashConnection {
        class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
    }
    -keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
        void <init>(android.content.Context);
    }
  • Nó có điều kiện, cụ thể và được nhắm mục tiêu, đồng thời cho phép tối ưu hoá bất cứ khi nào có thể trên các lớp được nhắm mục tiêu đó.

  • Đây là phản ánh về các trường và trong trường hợp cụ thể này, tôi đang lặp lại trên tất cả các trường bên trong một đối tượng, nhưng ở đây, một lần nữa, chúng tôi xử lý điều đó bằng cách nói rằng nhắm mục tiêu tất cả các trường bằng toán tử dấu sao này.

    // TODO: migrate to Kotlinx Serialization or Moshi codegen.
    @UsesReflectionToAccessField(
       classConstant = UserActionEvent::class,
       fieldName = "*"
    )
    fun serializeEvent(event: UserActionEvent): JSONObject {
       val json = JSONObject()
       for (field in event.javaClass.declaredFields) {
           field.isAccessible = true
           json.put(field.name, field.get(event))
       }
       return json
    }
  • Điều này cho phép tôi nhắm đến tất cả các trường và tạo ra chính xác quy tắc giữ lại mà bạn mong đợi cho việc này, bạn biết đấy, sự phản ánh cực kỳ đáng tiếc không nên có trong ứng dụng của tôi, nhưng tôi sẽ quay lại và dọn dẹp, tôi hoàn toàn hứa.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.serialization.EventSerializer {
        class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
    }
    -keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
        <fields>;
    }
  • Vậy Jetpack Keep có chức năng gì?

  • Điều này giúp giảm đáng kể công sức duy trì quy tắc.

  • Đối với mọi thứ mà bạn có thể tự động tạo, bạn chỉ cần xoá khỏi khu vực bạn quan tâm và khu vực rủi ro để duy trì các quy tắc giữ lại.

  • Nó cũng cung cấp cho bạn tính tất định của lint.

  • Công cụ này không chỉ khắc phục sự cố cho bạn khi bạn lặp lại và cố gắng di chuyển, mà còn phát hiện phản ánh mới và phát hiện các thay đổi phản ánh bất cứ khi nào mã thay đổi, và sự an toàn đó là vô cùng quan trọng.

  • Tôi có thể sử dụng thông báo này không?

  • Vậy các phần phụ thuộc ở đây là gì?

  • Trình bổ trợ và công cụ tìm lỗi mã nguồn yêu cầu AGP 8.7.

  • Đó là hầu hết mọi người.

  • Nhưng bạn cũng có thể sử dụng tính năng này một cách rất linh hoạt.

  • Bạn có thể sử dụng tính năng này trong các ứng dụng.

  • Bạn có thể sử dụng tính năng này trong các thư viện Android, AAR, thư viện KMP và JAR. Trong trường hợp đó, bạn cần thực hiện một số thao tác kết nối thủ công.

  • Nhưng tính năng chính ở đây là tính năng này an toàn để sử dụng với tư cách là nhà phát triển thư viện.

  • Ngay cả khi ai đó đang sử dụng phiên bản R8 cũ hoặc thậm chí là một công cụ của bên thứ ba đang sử dụng thư viện của bạn, chúng tôi vẫn xuất các quy tắc người dùng chuẩn thực tế để mọi người vẫn có thể sử dụng bạn và không phải lo lắng về việc sử dụng một số loại định dạng mới không tương thích.

Phân tích ứng dụng bằng Android CLI

  • Sau khi nói về những việc Jetpack Keep có thể làm để giúp bạn tối ưu hoá và bớt lo lắng về việc bảo vệ các lệnh gọi mã phản chiếu, hãy cùng nói về các tính năng mới mà chúng tôi vừa ra mắt trong Android CLI để kiểm tra kỹ giai đoạn phát hành gần đây nhất.

  • Breana đã đề cập trước đó rằng có các công cụ trong Play Findings sẽ cho bạn biết thông tin về kích thước, kích thước DEX và tỷ lệ phần trăm tối ưu hoá.

  • Giờ đây, chúng tôi đã bắt đầu cung cấp tính năng đó thông qua Android CLI.

  • Công cụ này sử dụng cùng một logic với Cửa hàng Play, vì vậy, bạn không cần tải gói lên Cửa hàng Play mà chỉ cần phân tích gói trên CLI.

    $ android analyze <mybundle.aab>
    $ android analyze <mybundle.apk>
  • Và bạn sẽ nhận được thông tin chính xác được báo cáo trực tiếp và ngay lập tức cho bạn bằng tính năng phân tích cục bộ.

  • Như bạn có thể thấy trong trường hợp cụ thể này, ứng dụng này nhận được cảnh báo về tỷ lệ tối ưu hoá rất thấp, đồng thời cũng nhận được thông báo "Đây là kích thước tệp DEX của bạn và đây là tỷ lệ phần trăm tối ưu hoá", giống hệt như thông báo của Cửa hàng Play.

  • Nhưng cũng hữu ích ở đây, chúng tôi có đầu ra JSON để nếu bạn muốn viết tập lệnh tự động hoá kiểm thử hồi quy đối với bất kỳ giá trị nào trong số này giảm đáng kể, vì một lần nữa, đây là những con số mà Play thực thi, bạn muốn theo dõi chúng, bạn có thể làm điều đó thực sự dễ dàng với giá trị JSON này – các báo cáo JSON này.

    {
      "optimization": {
        "bundleAnalysisResult": {
          "agpVersion": "9.3.2",
          "r8Json": {
            "version": "9.3.16",
            "options": {
              "isRepackageClassesEnabled": true,
              "isProguardCompatibilityModeEnabled": false
            },
            "stats": {
              "shrinkingRatio": 0.2306,
              "optimizationRatio": 0.2351,
              "obfuscationRatio": 0.2322
            },
            ...
          }
        "mappingObfuscationRatio": 0.21631891,
        "dexHeuristicObfuscationRatio": 0.25118626,
        "uncompressedDexSizeBytes": "16198152"
      }
  • Tại đây, chúng ta cũng thấy các con số giống hệt nhau.

  • Đây không phải là tính năng duy nhất của android analyze.

  • android analyze cũng có thể chạy trên các thư viện.

    $ android analyze <library.aar>
    $ android analyze <library.jar>
  • Và như Breana đã đề cập, có rất nhiều vấn đề với các thư viện chạy kèm theo các quy tắc giữ lại không mong muốn bên trong chúng.

  • Vậy nên, hãy dùng công cụ này để xem xét một số vấn đề.

  • Vì vậy, khi chạy công cụ này trên một tệp AAR cụ thể, tôi có thể thấy thư viện này có một số vấn đề được nêu bật ngay ở trên cùng.

  • Thư viện này có tỷ lệ giữ lại cao, tức là thư viện này giữ lại quá nhiều quy tắc giữ lại không giới hạn.

  • Nó chứa các quy tắc giữ lại rộng, ngăn không cho chính nó được tối ưu hoá và có một số con số bên dưới cho bạn biết chính xác mức độ tồi tệ của điều đó, đồng thời nó cũng chứa các quy tắc giữ lại, có lẽ đáng sợ nhất trong số này, không nhắm mục tiêu bất cứ điều gì, có lẽ có nghĩa là họ đang nhắm mục tiêu người khác theo một cách không giới hạn nào đó.

  • Mã có dấu hiệu rất nguy hiểm.

  • Vì vậy, bạn có thể kiểm tra tất cả những điều đó trong AAR, đặc biệt là với tư cách là nhà xuất bản thư viện, đây là một cách tuyệt vời để đảm bảo rằng bạn không cung cấp các quy tắc giữ lại không hợp lệ và bạn không để chúng rò rỉ khi ai đó có thể không phải là chuyên gia viết quy tắc giữ lại đang làm việc trên thư viện.

  • Tại đây, bạn có thể thấy chúng tôi cũng đưa ra tỷ lệ giữ chân.

  • Đây là một cách hay để đánh giá chất lượng tổng thể của thư viện.

  • Nếu tôi chỉ tối ưu hoá một thư viện riêng lẻ hoàn toàn độc lập, không có tệp đối chiếu, bằng R8, thì tôi sẽ thấy thư viện đó có kích thước bằng 0 byte, đúng không?

  • Nó sẽ trống rỗng vì không có gì được giữ lại bên trong.

  • Đó là điều lý tưởng.

  • Trong trường hợp này, câu trả lời là không.

  • Bạn có thể xem trực tiếp kích thước của R8 và D8.

  • Trên thực tế, chúng tôi cũng nêu bật một số quy tắc giữ lại tệ nhất để bạn dễ thấy hơn.

  • Tệp này cũng có định dạng JSON, vì vậy, cho dù bạn là nhà phát triển ứng dụng đang cố gắng kiểm soát các bản cập nhật thư viện hay là nhà phát triển thư viện đang cố gắng ngăn chặn tình trạng hồi quy lớn về chất lượng thư viện, thì bạn đều có thể thực hiện cùng một quy trình phân tích và ngăn chặn những vấn đề này xảy ra bằng một tập lệnh đơn giản bao bọc Android CLI.

    {
      "optimization": {
        "libraryAnalysisResult": {
          "dex": {
            "d8CompileResult": {
              "dexSizeBytes": 4523352
            },
            "r8CompileResult": {
              "dexSizeBytes": 3266596,
              "configuration": {
                "usedPackageWideKeepRules": [
                  {
                    "source": "-keep class com.example.analytics.bloat.** { *; }",
                    "keptItemCount": 125579,
                  },
                  ...
                ],
                "unusedPackageWideKeepRules": [
                  {
                    "source": "-keep class some.other.library.** { *; }",
                  },
                  ...
                ]
              },
              ...
    }
  • Vì vậy, nếu bạn phát hiện vấn đề về thư viện, trước tiên, hãy thử cập nhật thư viện đó.

  • Như Breana đã đề cập trước đó, chúng tôi đã sửa hơn một tá thư viện của Google để khắc phục một số quy tắc giữ lại rất đáng tiếc và có những thư viện khác đã được sửa, nhưng các ứng dụng sử dụng chúng không được cập nhật.

  • Đây là bước đầu tiên rất quan trọng.

  • Thứ hai, bạn có thể tìm vấn đề và liên hệ với nhà phát triển thư viện, đồng thời có thể đính kèm báo cáo của trình phân tích để cho họ biết rằng: "Này, bạn đang giữ quá nhiều.

  • Vui lòng giúp tôi tối ưu hoá ứng dụng của mình."

Những điểm chính cần ghi nhớ và tài nguyên

  • Tóm lại, chúng ta đã nói về Trình phân tích cấu hình R8, giúp bạn xem xét mọi thứ đang diễn ra trong bản dựng R8, mọi thứ cho biết những gì có thể tối ưu hoá và những gì không.

  • Chúng ta đã nói về Jetpack Keep, thư viện mới giúp bạn tự động viết quy tắc giữ lại mà không cần phải làm gì.

  • Và Android CLI, công cụ mới giúp bạn kiểm tra và đảm bảo, kiểm tra kỹ bước cuối cùng và đảm bảo bạn, nhân viên hỗ trợ, kỹ sư cấp dưới trong nhóm của bạn không làm hỏng bất cứ điều gì cho người tiêu dùng.

  • Vì vậy, nếu bạn muốn tìm hiểu thêm, hãy xem tài liệu về R8, trình phân tích cấu hình và Jetpack Keep.

  • Ngoài ra, còn có tài liệu về Android CLI nói về lệnh phân tích và cách sử dụng lệnh này.

  • Cảm ơn bạn rất nhiều.

Mã

Đoạn mã

Đoạn mã Kotlin Script ở phút 05:15 trong video "Giới thiệu về tính năng tối ưu hoá DEX"
android {
    buildTypes {
        release {
            optimization {
                // no need to specify default
                // rules and resource shrinking
                enabled = true
            }
        }
    }
    ...
}
Đoạn mã Kotlin Script ở 05:15 trong phần "Giới thiệu về tính năng tối ưu hoá DEX"
android {
    buildTypes {
        release {
            // Enables code optimizations.
            isMinifyEnabled = true

            // Enables resource shrinking.
            isShrinkResources = true

            proguardFiles(
                // Default rules.
                getDefaultProguardFile(
                    "proguard-android-optimize.txt"
                ),
                ... // list out each file with keep rules
            )
            ...
        }
    }
    ...
}
Đoạn mã shell ở phút 13:19 trong video "Nghiên cứu điển hình: Tối ưu hoá WHOOP"
$ android skills list
$ android skills add r8-analyzer
Đoạn mã Kotlin ở phút 14:41 trong video "Challenges of reflection in DEX optimization" (Thách thức của việc phản ánh trong quá trình tối ưu hoá DEX)
fun createWithReflection(className: String): MyObject {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as MyObject
}
Đoạn mã Kotlin ở phút 15:40 trong video "Challenges of reflection in DEX optimization" (Thách thức của việc phản chiếu trong quá trình tối ưu hoá DEX)
// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
   return runCatching {
       Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
           .getDeclaredConstructor(Context::class.java)
           .newInstance(context) as? EventTransport
   }.getOrNull()
}

// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}

// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
   val json = JSONObject()
   for (field in event.javaClass.declaredFields) {
       field.isAccessible = true
       json.put(field.name, field.get(event))
   }
   return json
}
Đoạn mã Shell ở phút 16:52 trong video "Thách thức của việc phản chiếu trong quá trình tối ưu hoá DEX"
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };
Đoạn mã Kotlin ở phút 16:59 trong video "Challenges of reflection in DEX optimization" (Những thách thức của việc phản ánh trong quá trình tối ưu hoá DEX)
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
Đoạn mã Shell lúc 17:07 trong phần "Thách thức của việc phản ánh trong quá trình tối ưu hoá DEX"
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
    *
}
Đoạn mã Shell ở phút 17:15 trong video "Challenges of reflection in DEX optimization" (Thách thức của việc phản ánh trong quá trình tối ưu hoá DEX)
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
    *
}
Đoạn mã shell ở phút 17:19 trong video "Challenges of reflection in DEX optimization" (Thách thức của việc phản chiếu trong quá trình tối ưu hoá DEX)
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
    *
}
Đoạn mã Shell ở phút 17:40 trong video "Challenges of reflection in DEX optimization" (Thách thức của phản chiếu trong việc tối ưu hoá DEX)
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
Đoạn mã shell ở phút 17:50 trong video "Challenges of reflection in DEX optimization" (Thách thức của việc phản ánh trong quá trình tối ưu hoá DEX)
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
Đoạn mã shell ở phút 18:03 trong video "Challenges of reflection in DEX optimization" (Thách thức của việc phản ánh trong quá trình tối ưu hoá DEX)
### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
    class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
    void <init>();
}
Đoạn mã Kotlin ở phút 18:57 trong video "Challenges of reflection in DEX optimization" (Thách thức của việc phản ánh trong quá trình tối ưu hoá DEX)
@androidx.annotation.Keep
class EventFilter { … }

@androidx.annotation.Keep
class SizeBasedEventFilter { … }

@androidx.annotation.Keep
class DropLogsEventFilter { … }
Đoạn mã Kotlin ở phút 19:22 trong video "Challenges of reflection in DEX optimization" (Thách thức của phản chiếu trong việc tối ưu hoá DEX)
@Keep
class EventFilter { … }
Đoạn mã Kotlin ở phút 19:29 trong video "Challenges of reflection in DEX optimization" (Thách thức của việc phản ánh trong quá trình tối ưu hoá DEX)
@Keep
class EventFilter { … }

@Keep
class SizeBasedEventFilter { … }

@Keep
class DropLogsEventFilter { … }
Đoạn mã Kotlin Script ở phút 20:38 trong video "Giới thiệu về Jetpack Keep"
plugins {
   id("com.android.library")
   id("androidx.annotation.keep")
}

dependencies {
    implementation("androidx.annotation.keep:keep")
    ...
}
Đoạn mã Shell ở phút 20:54 trong video "Giới thiệu về Jetpack Keep"
./gradlew analyticsbloat:lintFix
Đoạn mã Kotlin ở phút 20:55 trong video "Giới thiệu về Jetpack Keep"
@UsesReflectionToConstruct(
   classConstant = EventFilter::class,
   parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
Đoạn mã Shell ở phút 21:25 trong video "Giới thiệu về Jetpack Keep"
# Generated Keep Rule

-if class com.example.analytics.bloat.EventTools {
    class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
    void <init>();
}
Đoạn mã Kotlin ở phút 21:52 trong video "Giới thiệu về Jetpack Keep"
@UsesReflectionToConstruct(
   className = "com.example.analytics.bloat.crash.CrashReporterTransport",
   parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
   return runCatching {
       Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
           .getDeclaredConstructor(Context::class.java)
           .newInstance(context) as? EventTransport
   }.getOrNull()
}
Đoạn mã Shell ở phút 22:05 trong video "Giới thiệu về Jetpack Keep"
# Generated Keep Rule

-if class com.example.analytics.bloat.CrashConnection {
    class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
    void <init>(android.content.Context);
}
Đoạn mã Kotlin ở phút 22:16 trong video "Giới thiệu về Jetpack Keep"
// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
   classConstant = UserActionEvent::class,
   fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
   val json = JSONObject()
   for (field in event.javaClass.declaredFields) {
       field.isAccessible = true
       json.put(field.name, field.get(event))
   }
   return json
}
Đoạn mã Shell ở phút 22:27 trong video "Giới thiệu về Jetpack Keep"
# Generated Keep Rule

-if class com.example.analytics.bloat.serialization.EventSerializer {
    class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
    <fields>;
}
Shell snippet at 24:22 in "Analyzing apps with Android CLI"
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>
Đoạn mã JSON ở phút 24:52 trong video "Phân tích ứng dụng bằng CLI Android"
{
  "optimization": {
    "bundleAnalysisResult": {
      "agpVersion": "9.3.2",
      "r8Json": {
        "version": "9.3.16",
        "options": {
          "isRepackageClassesEnabled": true,
          "isProguardCompatibilityModeEnabled": false
        },
        "stats": {
          "shrinkingRatio": 0.2306,
          "optimizationRatio": 0.2351,
          "obfuscationRatio": 0.2322
        },
        ...
      }
    "mappingObfuscationRatio": 0.21631891,
    "dexHeuristicObfuscationRatio": 0.25118626,
    "uncompressedDexSizeBytes": "16198152"
  }
Đoạn mã shell ở phút 25:17 trong video "Phân tích ứng dụng bằng Android CLI"
$ android analyze <library.aar>
$ android analyze <library.jar>
Đoạn mã JSON ở phút 26:46 trong video "Phân tích ứng dụng bằng Android CLI"
{
  "optimization": {
    "libraryAnalysisResult": {
      "dex": {
        "d8CompileResult": {
          "dexSizeBytes": 4523352
        },
        "r8CompileResult": {
          "dexSizeBytes": 3266596,
          "configuration": {
            "usedPackageWideKeepRules": [
              {
                "source": "-keep class com.example.analytics.bloat.** { *; }",
                "keptItemCount": 125579,
              },
              ...
            ],
            "unusedPackageWideKeepRules": [
              {
                "source": "-keep class some.other.library.** { *; }",
              },
              ...
            ]
          },
          ...
}