Cách thu nhỏ mã - Giới hạn phương thức 65k trong dex


91

Tôi có một ứng dụng Android khá lớn dựa trên nhiều dự án thư viện. Trình biên dịch Android có giới hạn là 65536 phương thức trên mỗi tệp .dex và tôi đang vượt qua con số đó.

Về cơ bản, có hai con đường bạn có thể chọn (ít nhất là tôi biết) khi bạn đạt đến giới hạn phương pháp.

1) Thu nhỏ mã của bạn

2) Xây dựng nhiều tệp dex ( xem bài đăng trên blog này )

Tôi đã xem xét cả hai và cố gắng tìm ra nguyên nhân khiến số lượng phương pháp của tôi tăng cao như vậy. API Google Drive chiếm tỷ trọng lớn nhất với sự phụ thuộc của Guava với hơn 12.000. Tổng số libs cho API Drive v2 đạt hơn 23.000!

Tôi đoán câu hỏi của tôi là, bạn nghĩ tôi nên làm gì? Tôi có nên xóa tích hợp Google Drive như một tính năng của ứng dụng của mình không? Có cách nào để thu nhỏ API xuống không (vâng, tôi sử dụng proguard)? Tôi có nên sử dụng nhiều tuyến dex (có vẻ khá khó khăn, đặc biệt là xử lý các API của bên thứ ba)?


2
Tôi tình cờ yêu ứng dụng của bạn. Bạn đã nghĩ đến việc thực hiện tải xuống bắt buộc tất cả các lib bổ sung ở dạng giả apkchưa? Cá nhân tôi muốn thấy tích hợp Drive
JBirdVegas

8
Facebook gần đây đã ghi lại cách giải quyết của họ cho những gì có vẻ như một vấn đề gần như giống hệt nhau trong ứng dụng Android của họ. Có thể hữu ích: facebook.com/notes/facebook-engineering/…
Reuben Scratton

4
Bắt đầu đi xuống tuyến đường nhiều dex. Tôi đã tạo thành công tệp dex phụ để hoạt động với Google Drive. Tôi cảm thấy tồi tệ cho bất cứ ai cần ổi để phụ thuộc. : P Nó vẫn là một vấn đề khá lớn đối với tôi mặc dù
Jared Rummler

4
làm thế nào để bạn đếm các phương pháp?
Bri6ko

1
Một số lưu ý bổ sung tại đây: stackoverflow.com/questions/21490382 (bao gồm liên kết đến tiện ích sẽ liệt kê các tham chiếu phương thức trong APK). Lưu ý rằng giới hạn 64K không liên quan đến vấn đề Facebook liên kết một số bình luận lên.
fadden

Câu trả lời:


69

Có vẻ như Google cuối cùng đã triển khai giải pháp thay thế / sửa lỗi để vượt qua giới hạn phương pháp 65K của tệp dex.

Giới hạn tham chiếu 65 nghìn

Tệp ứng dụng Android (APK) chứa tệp bytecode thực thi ở dạng tệp Dalvik Executable (DEX), chứa mã đã biên dịch được sử dụng để chạy ứng dụng của bạn. Đặc tả Dalvik Executable giới hạn tổng số phương thức có thể được tham chiếu trong một tệp DEX là 65.536, bao gồm các phương thức khung Android, phương thức thư viện và phương thức trong mã của riêng bạn. Vượt qua giới hạn này yêu cầu bạn định cấu hình quy trình xây dựng ứng dụng của mình để tạo nhiều tệp DEX, được gọi là cấu hình đa cấp độ.

Hỗ trợ Multidex trước Android 5.0

Các phiên bản của nền tảng trước Android 5.0 sử dụng thời gian chạy Dalvik để thực thi mã ứng dụng. Theo mặc định, Dalvik giới hạn các ứng dụng trong một tệp bytecode class.dex cho mỗi APK. Để khắc phục hạn chế này, bạn có thể sử dụng thư viện hỗ trợ multidex , thư viện này sẽ trở thành một phần của tệp DEX chính của ứng dụng của bạn và sau đó quản lý quyền truy cập vào các tệp DEX bổ sung và mã chứa chúng.

Hỗ trợ Multidex cho Android 5.0 trở lên

Android 5.0 trở lên sử dụng thời gian chạy có tên ART, vốn dĩ hỗ trợ tải nhiều tệp dex từ các tệp APK ứng dụng. ART thực hiện quá trình biên dịch trước tại thời điểm cài đặt ứng dụng, quét các lớp (.. N) tệp .dex và biên dịch chúng thành một tệp .oat duy nhất để thiết bị Android thực thi. Để biết thêm thông tin về thời gian chạy Android 5.0, hãy xem Giới thiệu ART .

Xem: Xây dựng ứng dụng với hơn 65 nghìn phương pháp


Thư viện hỗ trợ Multidex

Thư viện này cung cấp hỗ trợ để xây dựng ứng dụng với nhiều tệp Dalvik Executable (DEX). Các ứng dụng tham chiếu đến hơn 65536 phương pháp được yêu cầu để sử dụng cấu hình multidex. Để biết thêm thông tin về cách sử dụng multidex, hãy xem Xây dựng ứng dụng với hơn 65 nghìn phương pháp .

Thư viện này nằm trong thư mục / extras / android / support / multidex / sau khi bạn tải xuống Android Support Libraries. Thư viện không chứa tài nguyên giao diện người dùng. Để đưa nó vào dự án ứng dụng của bạn, hãy làm theo hướng dẫn Thêm thư viện mà không cần tài nguyên.

Mã định danh phụ thuộc tập lệnh xây dựng Gradle cho thư viện này như sau:

com.android.support:multidex:1.0.+ Ký hiệu phụ thuộc này chỉ định phiên bản phát hành 1.0.0 trở lên.


Bạn vẫn nên tránh đạt đến giới hạn 65 nghìn phương pháp bằng cách tích cực sử dụng proguard và xem xét các yếu tố phụ thuộc của mình.


6
+1, Tại sao mọi người không ủng hộ câu trả lời đúng khi họ được cùng một người trả lời?
Pacerier

cấp độ tối thiểu api trở thành 14!
Vihaan Verma

5
Chúng tôi đã viết một plugin Gradle nhỏ để cung cấp cho bạn số lượng phương pháp hiện tại của bạn trên mỗi bản dựng. Đã được hữu ích cho chúng tôi để quản lý thư viện - github.com/KeepSafe/dexcount-gradle-plugin
Philipp

53

bạn có thể sử dụng thư viện hỗ trợ multidex cho điều đó, Để bật multidex

1) bao gồm nó trong các phụ thuộc:

dependencies {
  ...
  compile 'com.android.support:multidex:1.0.0'
}

2) Bật nó trong ứng dụng của bạn:

defaultConfig {
    ...
    minSdkVersion 14
    targetSdkVersion 21
    ....
    multiDexEnabled true
}

3) nếu bạn có một lớp ứng dụng cho ứng dụng của mình thì hãy Ghi đè phương thức attachmentBaseContext như sau:

package ....;
...
import android.support.multidex.MultiDex;

public class MyApplication extends Application {
  ....
   @Override
   protected void attachBaseContext(Context context) {
    super.attachBaseContext(context);
    MultiDex.install(this);
   }
}

4) nếu bạn không có lớp ứng dụng cho ứng dụng của mình thì hãy đăng ký android.support.multidex.MultiDexApplication làm ứng dụng của bạn trong tệp kê khai của bạn. như thế này:

<application
    ...
    android:name="android.support.multidex.MultiDexApplication">
    ...
</application>

và nó sẽ hoạt động tốt!


31

Play Services6.5+ hữu ích: http://android-developers.blogspot.com/2014/12/google-play-services-and-dex-method.html

"Bắt đầu từ phiên bản 6.5, của các dịch vụ của Google Play, bạn sẽ có thể chọn từ một số API riêng lẻ và bạn có thể thấy"

...

"điều này sẽ tạm thời bao gồm các thư viện 'cơ sở', được sử dụng trên tất cả các API."

Đây là một tin tốt, đối với một trò chơi đơn giản chẳng hạn, bạn có thể chỉ cần base, gamesvà có thể drive.

"Dưới đây là danh sách đầy đủ các tên API. Bạn có thể tìm thêm thông tin chi tiết trên trang web Nhà phát triển Android.:

  • com.google.android.gms: play-services-base: 6.5.87
  • com.google.android.gms: play-services-ads: 6.5.87
  • com.google.android.gms: play-services-appindexing: 6.5.87
  • com.google.android.gms: play-services-maps: 6.5.87
  • com.google.android.gms: play-services-location: 6.5.87
  • com.google.android.gms: play-services-fitness: 6.5.87
  • com.google.android.gms: play-services-panorama: 6.5.87
  • com.google.android.gms: play-services-drive: 6.5.87
  • com.google.android.gms: play-services-games: 6.5.87
  • com.google.android.gms: play-services-wallet: 6.5.87
  • com.google.android.gms: play-services-ID: 6.5.87
  • com.google.android.gms: play-services-cast: 6.5.87
  • com.google.android.gms: play-services-plus: 6.5.87
  • com.google.android.gms: play-services-appstate: 6.5.87
  • com.google.android.gms: play-services-wear: 6.5.87
  • com.google.android.gms: play-services-all-wear: 6.5.87

Bất kỳ thông tin nào về cách thực hiện điều đó trong dự án Eclipse?
Brian White

Tôi chưa thể nâng cấp lên phiên bản đó. Nhưng nếu dự án của bạn dựa trên Maven, thì hy vọng bạn chỉ cần giải quyết vấn đề đó trong maven pom.
Csaba Toth

@ webo80 Chà, điều này chỉ hữu ích nếu bạn lên phiên bản 6.5.87. Tôi tự hỏi về câu trả lời của petey, proguard loại bỏ các chức năng không sử dụng. Tôi tự hỏi liệu điều đó có liên quan đến lib của bên thứ hai hay chỉ là những thứ của riêng bạn. Tôi nên đọc thêm về proguard.
Csaba Toth

@BrianWhite Giải pháp duy nhất cho bây giờ dường như dải file .jar với một số công cụ bên ngoài ..
milosmns

Tôi đã kết thúc bằng cách sử dụng công cụ này: gist.github.com/dextorer/a32cad7819b7f272239b
Brian White

9

Trong các phiên bản của dịch vụ Google Play trước 6.5, bạn phải biên dịch toàn bộ gói API vào ứng dụng của mình. Trong một số trường hợp, làm như vậy sẽ khó khăn hơn để giữ số lượng phương thức trong ứng dụng của bạn (bao gồm API khung, phương thức thư viện và mã của riêng bạn) dưới giới hạn 65.536.

Từ phiên bản 6.5, bạn có thể biên dịch có chọn lọc các API dịch vụ của Google Play vào ứng dụng của mình. Ví dụ: để chỉ bao gồm API Google Fit và Android Wear, hãy thay thế dòng sau trong tệp build.gradle của bạn:

compile 'com.google.android.gms:play-services:6.5.87'

với những dòng này:

compile 'com.google.android.gms:play-services-fitness:6.5.87'
compile 'com.google.android.gms:play-services-wearable:6.5.87'

để tham khảo thêm, bạn có thể bấm vào đây


Làm thế nào để làm điều đó trong nhật thực?
Hardik9850

7

Sử dụng proguard để làm sáng gói ứng dụng của bạn vì các phương pháp không được sử dụng sẽ không có trong bản dựng cuối cùng của bạn. Kiểm tra kỹ xem bạn có thông tin sau trong tệp cấu hình proguard của mình để sử dụng proguard với ổi không (tôi xin lỗi nếu bạn đã có cái này, nó không được biết tại thời điểm viết bài):

# Guava exclusions (http://code.google.com/p/guava-libraries/wiki/UsingProGuardWithGuava)
-dontwarn sun.misc.Unsafe
-dontwarn com.google.common.collect.MinMaxPriorityQueue
-keepclasseswithmembers public class * {
    public static void main(java.lang.String[]);
} 

# Guava depends on the annotation and inject packages for its annotations, keep them both
-keep public class javax.annotation.**
-keep public class javax.inject.**

Ngoài ra, nếu bạn đang sử dụng ActionbarSherlock, việc chuyển sang thư viện hỗ trợ appcompat v7 cũng sẽ giảm số lượng phương thức của bạn đi rất nhiều (dựa trên kinh nghiệm cá nhân). Hướng dẫn được định vị:


vẻ này promissing nhưng tôi đã nhận Warning: butterknife.internal.ButterKnifeProcessor: can't find superclass or interface javax.annotation.processing.AbstractProcessorkhi chạy./gradlew :myapp:proguardDevDebug
ericn

1
Tuy nhiên, trong quá trình phát triển, proguard thường không chạy (cuối cùng là không chạy với Eclipse), vì vậy bạn không thể hưởng lợi từ việc thu nhỏ cho đến khi thực hiện bản phát hành.
Brian White

7

Bạn có thể sử dụng Liên kết Jar Jar để thu nhỏ các thư viện bên ngoài khổng lồ như Dịch vụ của Google Play (16K phương pháp!)

Trong trường hợp của bạn, bạn sẽ chỉ tách mọi thứ từ lọ Dịch vụ của Google Play ngoại trừ common internaldrivecác gói phụ.


4

Đối với những người dùng Eclipse không sử dụng Gradle, có những công cụ sẽ phá vỡ jar Dịch vụ của Google Play và xây dựng lại nó chỉ với những phần bạn muốn.

Tôi sử dụng strip_play_services.sh của dextorer .

Có thể khó biết chính xác dịch vụ nào cần bao gồm vì có một số phụ thuộc nội bộ nhưng bạn có thể bắt đầu nhỏ và thêm vào cấu hình nếu hóa ra thiếu những thứ cần thiết.


3

Tôi nghĩ rằng về lâu dài, phá vỡ ứng dụng của bạn bằng nhiều dex sẽ là cách tốt nhất.


2
Tôi đang tìm một cách thích hợp để thực hiện việc này với Gradle: - / Có gợi ý nào không?
Ivan Morgillo,


2

Nếu không sử dụng multidex khiến quá trình xây dựng rất chậm. Bạn có thể làm như sau. Như yahska đã đề cập, hãy sử dụng thư viện dịch vụ google play cụ thể. Đối với hầu hết các trường hợp, điều này là cần thiết.

compile 'com.google.android.gms:play-services-base:6.5.+'

Đây là tất cả các gói có sẵn Biên dịchchọn lọc các API thành tệp thực thi của bạn

Nếu điều này sẽ không đủ, bạn có thể sử dụng tập lệnh gradle. Đặt mã này vào tệp 'strip_play_services.gradle'

def toCamelCase(String string) {
String result = ""
string.findAll("[^\\W]+") { String word ->
    result += word.capitalize()
}
return result
}

afterEvaluate { project ->
Configuration runtimeConfiguration = project.configurations.getByName('compile')
println runtimeConfiguration
ResolutionResult resolution = runtimeConfiguration.incoming.resolutionResult
// Forces resolve of configuration
ModuleVersionIdentifier module = resolution.getAllComponents().find {
    it.moduleVersion.name.equals("play-services")
}.moduleVersion


def playServicesLibName = toCamelCase("${module.group} ${module.name} ${module.version}")
String prepareTaskName = "prepare${playServicesLibName}Library"
File playServiceRootFolder = project.tasks.find { it.name.equals(prepareTaskName) }.explodedDir


def tmpDir = new File(project.buildDir, 'intermediates/tmp')
tmpDir.mkdirs()
def libFile = new File(tmpDir, "${playServicesLibName}.marker")

def strippedClassFileName = "${playServicesLibName}.jar"
def classesStrippedJar = new File(tmpDir, strippedClassFileName)

def packageToExclude = ["com/google/ads/**",
                        "com/google/android/gms/actions/**",
                        "com/google/android/gms/ads/**",
                        // "com/google/android/gms/analytics/**",
                        "com/google/android/gms/appindexing/**",
                        "com/google/android/gms/appstate/**",
                        "com/google/android/gms/auth/**",
                        "com/google/android/gms/cast/**",
                        "com/google/android/gms/drive/**",
                        "com/google/android/gms/fitness/**",
                        "com/google/android/gms/games/**",
                        "com/google/android/gms/gcm/**",
                        "com/google/android/gms/identity/**",
                        "com/google/android/gms/location/**",
                        "com/google/android/gms/maps/**",
                        "com/google/android/gms/panorama/**",
                        "com/google/android/gms/plus/**",
                        "com/google/android/gms/security/**",
                        "com/google/android/gms/tagmanager/**",
                        "com/google/android/gms/wallet/**",
                        "com/google/android/gms/wearable/**"]

Task stripPlayServices = project.tasks.create(name: 'stripPlayServices', group: "Strip") {
    inputs.files new File(playServiceRootFolder, "classes.jar")
    outputs.dir playServiceRootFolder
    description 'Strip useless packages from Google Play Services library to avoid reaching dex limit'

    doLast {
        def packageExcludesAsString = packageToExclude.join(",")
        if (libFile.exists()
                && libFile.text == packageExcludesAsString
                && classesStrippedJar.exists()) {
            println "Play services already stripped"
            copy {
                from(file(classesStrippedJar))
                into(file(playServiceRootFolder))
                rename { fileName ->
                    fileName = "classes.jar"
                }
            }
        } else {
            copy {
                from(file(new File(playServiceRootFolder, "classes.jar")))
                into(file(playServiceRootFolder))
                rename { fileName ->
                    fileName = "classes_orig.jar"
                }
            }
            tasks.create(name: "stripPlayServices" + module.version, type: Jar) {
                destinationDir = playServiceRootFolder
                archiveName = "classes.jar"
                from(zipTree(new File(playServiceRootFolder, "classes_orig.jar"))) {
                    exclude packageToExclude
                }
            }.execute()
            delete file(new File(playServiceRootFolder, "classes_orig.jar"))
            copy {
                from(file(new File(playServiceRootFolder, "classes.jar")))
                into(file(tmpDir))
                rename { fileName ->
                    fileName = strippedClassFileName
                }
            }
            libFile.text = packageExcludesAsString
        }
    }
}

project.tasks.findAll {
    it.name.startsWith('prepare') && it.name.endsWith('Dependencies')
}.each { Task task ->
    task.dependsOn stripPlayServices
}
project.tasks.findAll { it.name.contains(prepareTaskName) }.each { Task task ->
    stripPlayServices.mustRunAfter task
}

}

Sau đó, áp dụng tập lệnh này trong build.gradle của bạn, như thế này

apply plugin: 'com.android.application'
apply from: 'strip_play_services.gradle'

1

Nếu sử dụng Dịch vụ của Google Play, bạn có thể biết rằng nó thêm 20k + phương thức. Như đã đề cập, Android Studio có tùy chọn để đưa vào mô-đun các dịch vụ cụ thể, nhưng người dùng bị mắc kẹt với Eclipse phải tự thực hiện mô-đun hóa :(

May mắn thay, có một tập lệnh shell giúp công việc trở nên khá dễ dàng. Chỉ cần giải nén vào thư mục jar của google play services, chỉnh sửa tệp .conf được cung cấp nếu cần và thực thi tập lệnh shell.

Một ví dụ về việc sử dụng nó là ở đây .


1

Nếu sử dụng Dịch vụ của Google Play, bạn có thể biết rằng nó thêm 20k + phương thức. Như đã đề cập, Android Studio có tùy chọn để đưa vào mô-đun các dịch vụ cụ thể, nhưng người dùng bị mắc kẹt với Eclipse phải tự thực hiện mô-đun hóa :(

May mắn thay, có một tập lệnh shell giúp công việc trở nên khá dễ dàng. Chỉ cần giải nén vào thư mục jar của google play services, chỉnh sửa tệp .conf được cung cấp nếu cần và thực thi tập lệnh shell.

Một ví dụ về việc sử dụng nó là ở đây.

Giống như anh ấy nói, tôi compile 'com.google.android.gms:play-services:9.0.0'chỉ thay thế bằng các thư viện mà tôi cần và nó hoạt động.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.