วิธีย่อโค้ด - วิธี จำกัด 65k ใน dex


91

ฉันมีแอป Android ที่ค่อนข้างใหญ่ซึ่งอาศัยโครงการห้องสมุดจำนวนมาก คอมไพเลอร์ Android มีข้อ จำกัด 65536 เมธอดต่อไฟล์. dex และฉันเกินจำนวนนั้น

โดยทั่วไปมีสองเส้นทางที่คุณสามารถเลือกได้ (อย่างน้อยก็ที่ฉันรู้) เมื่อคุณถึงขีด จำกัด วิธีการ

1) ย่อรหัสของคุณ

2) สร้างไฟล์ dex หลายไฟล์ ( ดูโพสต์บล็อกนี้ )

ฉันตรวจสอบทั้งสองอย่างและพยายามค้นหาว่าอะไรเป็นสาเหตุให้จำนวนวิธีของฉันสูงมาก Google Drive API เป็นกลุ่มที่ใหญ่ที่สุดด้วยการพึ่งพา Guava ที่มากกว่า 12,000 libs ทั้งหมดสำหรับ Drive API v2 สูงถึง 23,000!

คำถามของฉันฉันเดาคือคุณคิดว่าฉันควรทำอย่างไร ฉันควรลบการรวม Google Drive ออกจากแอปของฉันหรือไม่ มีวิธีลดขนาด API ลงหรือไม่ (ใช่ฉันใช้ proguard) ฉันควรไปเส้นทาง dex หลาย ๆ ทาง (ซึ่งดูค่อนข้างเจ็บปวดโดยเฉพาะการจัดการกับ API ของบุคคลที่สาม)


2
ฉันชอบแอปของคุณ คุณเคยคิดที่จะดาวน์โหลด libs พิเศษทั้งหมดในapkรูปแบบหลอกหรือไม่? โดยส่วนตัวฉันอยากเห็นการรวมไดรฟ์
JBirdVegas

8
เมื่อเร็ว ๆ นี้ Facebook ได้บันทึกวิธีแก้ปัญหาสำหรับสิ่งที่ดูเหมือนว่าเป็นปัญหาที่เหมือนกันเกือบทั้งหมดในแอพ Android ของพวกเขา อาจเป็นประโยชน์: facebook.com/notes/facebook-engineering/…
Reuben Scratton

4
เริ่มมุ่งหน้าไปตามเส้นทาง dex หลายเส้นทาง ฉันสร้างไฟล์ dex รองเพื่อทำงานกับ Google ไดรฟ์ได้สำเร็จ ฉันรู้สึกแย่กับใครก็ตามที่ต้องการฝรั่งเป็นที่พึ่งพา : P มันยังคงเป็นปัญหาใหญ่สำหรับฉัน
Jared Rummler

4
คุณนับวิธีการอย่างไร?
Bri6ko

1
หมายเหตุเพิ่มเติมที่นี่: stackoverflow.com/questions/21490382 (รวมถึงลิงก์ไปยังยูทิลิตี้ที่จะแสดงรายการการอ้างอิงวิธีการใน APK) โปรดทราบว่าขีด จำกัด 64K ไม่เกี่ยวข้องกับปัญหา Facebook ที่เชื่อมโยงกับความคิดเห็นบางส่วน
fadden

คำตอบ:


69

ดูเหมือนว่าในที่สุด Google จะใช้วิธีแก้ปัญหา / แก้ไขเพื่อให้เกินขีด จำกัด เมธอด 65K ของไฟล์ dex

เกี่ยวกับขีด จำกัด อ้างอิง 65K

ไฟล์แอปพลิเคชัน Android (APK) มีไฟล์ bytecode ที่เรียกใช้งานได้ในรูปแบบของไฟล์ Dalvik Executable (DEX) ซึ่งมีโค้ดคอมไพล์ที่ใช้ในการเรียกใช้แอปของคุณ ข้อมูลจำเพาะของ Dalvik Executable จำกัด จำนวนเมธอดทั้งหมดที่สามารถอ้างอิงได้ภายในไฟล์ DEX ไฟล์เดียวเป็น 65,536 รวมถึงวิธีการเฟรมเวิร์ก Android วิธีไลบรารีและวิธีการในโค้ดของคุณเอง การก้าวพ้นขีด จำกัด นี้ทำให้คุณต้องกำหนดค่ากระบวนการสร้างแอปเพื่อสร้างไฟล์ DEX มากกว่าหนึ่งไฟล์หรือที่เรียกว่าการกำหนดค่าแบบหลายมิติ

รองรับ Multidex ก่อน Android 5.0

เวอร์ชันของแพลตฟอร์มก่อน Android 5.0 ใช้รันไทม์ Dalvik ในการเรียกใช้โค้ดแอป โดยค่าเริ่มต้น Dalvik จะ จำกัด แอปไว้ที่ไฟล์ class.dex bytecode เดียวต่อ APK เพื่อหลีกเลี่ยงข้อ จำกัด นี้คุณสามารถใช้ไลบรารีการสนับสนุน multidexซึ่งจะกลายเป็นส่วนหนึ่งของไฟล์ DEX หลักของแอปของคุณจากนั้นจัดการการเข้าถึงไฟล์ DEX เพิ่มเติมและรหัสที่มีอยู่

รองรับ Multidex สำหรับ Android 5.0 ขึ้นไป

Android 5.0 ขึ้นไปใช้รันไทม์ที่เรียกว่า ART ซึ่งรองรับการโหลดไฟล์ dex หลายไฟล์จากไฟล์ APK ของแอปพลิเคชัน ART ทำการคอมไพล์ล่วงหน้าในเวลาติดตั้งแอพพลิเคชั่นซึ่งจะสแกนหาไฟล์คลาส (.. N) .dex และคอมไพล์เป็นไฟล์. oat ไฟล์เดียวเพื่อให้อุปกรณ์ Android ดำเนินการได้ สำหรับข้อมูลเพิ่มเติมเกี่ยวกับรันไทม์ Android 5.0 ให้ดูที่แนะนำ ART

ดู: การสร้างแอพด้วยวิธีการมากกว่า 65K


ไลบรารีการสนับสนุน Multidex

ไลบรารีนี้รองรับการสร้างแอพที่มีไฟล์ Dalvik Executable (DEX) หลายไฟล์ แอพที่อ้างถึงเมธอดมากกว่า 65536 จำเป็นต้องใช้การกำหนดค่าแบบหลายมิติ สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการใช้ multidex ดูปพลิเคชันที่มีอาคารกว่าวิธี 65K

ไลบรารีนี้อยู่ในไดเร็กทอรี / extras / android / support / multidex / หลังจากที่คุณดาวน์โหลด Android Support Libraries ไลบรารีไม่มีทรัพยากรส่วนติดต่อผู้ใช้ หากต้องการรวมไว้ในโครงการแอปพลิเคชันของคุณให้ทำตามคำแนะนำสำหรับการเพิ่มไลบรารีโดยไม่มีทรัพยากร

Gradle build script dependency identifier สำหรับไลบรารีนี้มีดังนี้:

com.android.support:multidex:1.0.+ สัญกรณ์อ้างอิงนี้ระบุเวอร์ชันที่วางจำหน่าย 1.0.0 หรือสูงกว่า


คุณควรหลีกเลี่ยงการกดขีด จำกัด เมธอด 65K โดยใช้ proguard อย่างแข็งขันและตรวจสอบการอ้างอิงของคุณ


6
+1 ทำไมคนถึงไม่โหวตคำตอบที่ถูกต้องเมื่อพวกเขาตอบโดยคน ๆ เดียวกัน?
Pacerier

ระดับ api ขั้นต่ำกลายเป็น 14!
Vihaan Verma

5
เราเขียนปลั๊กอิน Gradle ขนาดเล็กเพื่อให้วิธีการปัจจุบันของคุณมีค่าในแต่ละบิลด์ เป็นประโยชน์กับเราในการจัดการไลบรารี - github.com/KeepSafe/dexcount-gradle-plugin
philipp

53

คุณสามารถใช้ไลบรารีการสนับสนุน multidex เพื่อเปิดใช้งานmultidex

1)รวมไว้ในการอ้างอิง:

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

2)เปิดใช้งานในแอปของคุณ:

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

3)หากคุณมีคลาสแอปพลิเคชันสำหรับแอปของคุณให้แทนที่เมธอดattachBaseContextดังนี้:

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

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

4)ถ้าคุณไม่ได้มีการประยุกต์ใช้ระดับสำหรับการใช้งานของคุณแล้วลงทะเบียนandroid.support.multidex.MultiDexApplicationเป็นโปรแกรมในไฟล์ Manifest ของ แบบนี้:

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

และควรใช้งานได้ดี!


32

Play Services6.5+ ช่วย: http://android-developers.blogspot.com/2014/12/google-play-services-and-dex-method.html

"ตั้งแต่บริการ Google Play เวอร์ชัน 6.5 เป็นต้นไปคุณจะสามารถเลือกได้จาก API แต่ละรายการและคุณจะเห็น"

...

"สิ่งนี้จะรวมไลบรารี" ฐาน "ตามสกรรมกริยาซึ่งใช้กับ API ทั้งหมด"

นี้เป็นข่าวดีสำหรับเกมที่ง่ายตัวอย่างเช่นคุณอาจต้องการเพียงbase, และอาจจะgamesdrive

"รายชื่อ API ทั้งหมดอยู่ด้านล่างรายละเอียดเพิ่มเติมสามารถพบได้ในไซต์นักพัฒนา 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-identity: 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-wearable: 6.5.87
  • com.google.android.gms: play-services-all-wear: 6.5.87

มีข้อมูลเกี่ยวกับวิธีการดำเนินการภายในโครงการ Eclipse หรือไม่?
Brian White

ฉันยังไม่สามารถอัปเกรดเป็นเวอร์ชันนั้นได้ แต่ถ้าโครงการของคุณเป็นแบบ Maven หวังว่าคุณจะต้องแก้ปัญหานั้นในตัวคุณ maven pom
Csaba Toth

@ webo80 สิ่งนี้จะช่วยได้ก็ต่อเมื่อคุณมีเวอร์ชันสูงสุด 6.5.87 ฉันสงสัยเกี่ยวกับคำตอบของ petey ที่ proguard จะตัดฟังก์ชันที่ไม่ได้ใช้ออกไป ฉันสงสัยว่ามันเกี่ยวข้องกับ libs ของบุคคลที่ 2 ด้วยหรือแค่ของของคุณเอง ฉันควรอ่านเพิ่มเติมเกี่ยวกับ proguard
Csaba Toth

@BrianWhite ทางออกเดียวสำหรับตอนนี้ดูเหมือนจะตัดไฟล์. jar ด้วยเครื่องมือภายนอกบางอย่าง ..
milosmns

1
@CsabaToth คุณช่วยฉันไว้! ฉันเพิ่มเพียงไม่กี่รายการด้านบนแทนที่จะเป็น "com.google.android.gms: play-services" แบบเต็มและนั่นสร้างความแตกต่าง!
EZDsIt

9

ในบริการ Google Play เวอร์ชันก่อน 6.5 คุณต้องรวบรวมแพ็คเกจ API ทั้งหมดลงในแอปของคุณ ในบางกรณีการทำเช่นนั้นทำให้การรักษาจำนวนเมธอดในแอปของคุณทำได้ยากขึ้น (รวมถึง Framework APIs เมธอดไลบรารีและโค้ดของคุณเอง) ภายใต้ขีด จำกัด 65,536

จากเวอร์ชัน 6.5 คุณสามารถเลือกรวบรวม API ของบริการ Google Play ลงในแอปของคุณได้ ตัวอย่างเช่นหากต้องการรวมเฉพาะ Google Fit และ Android Wear APIs ให้แทนที่บรรทัดต่อไปนี้ในไฟล์ build.gradle ของคุณ:

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

ด้วยบรรทัดเหล่านี้:

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

สำหรับข้อมูลอ้างอิงเพิ่มเติมคุณสามารถคลิกที่นี่


จะทำอย่างไรในคราส?
Hardik9850

7

ใช้ proguard เพื่อแบ่งเบา apk ของคุณเนื่องจากวิธีการที่ไม่ได้ใช้จะไม่อยู่ในบิลด์สุดท้ายของคุณ ตรวจสอบอีกครั้งว่าคุณมีสิ่งต่อไปนี้ในไฟล์กำหนดค่า proguard ของคุณเพื่อใช้ proguard กับฝรั่ง (ขออภัยหากคุณมีสิ่งนี้อยู่แล้วไม่ทราบในขณะที่เขียน):

# 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.**

นอกจากนี้หากคุณใช้ ActionbarSherlock การเปลี่ยนไปใช้ไลบรารีสนับสนุน v7 appcompat จะช่วยลดจำนวนวิธีการของคุณได้มาก (ขึ้นอยู่กับประสบการณ์ส่วนตัว) คำแนะนำตั้งอยู่:


สิ่งนี้ดูดี แต่ฉันได้รับWarning: butterknife.internal.ButterKnifeProcessor: can't find superclass or interface javax.annotation.processing.AbstractProcessorเมื่อวิ่ง./gradlew :myapp:proguardDevDebug
ericn

1
ในระหว่างการพัฒนาแม้ว่าโดยทั่วไปแล้ว proguard จะไม่ทำงาน (ในที่สุดก็ไม่ใช่กับ Eclipse) ดังนั้นคุณจะไม่ได้รับประโยชน์จากการหดตัวจนกว่าจะทำการสร้างรุ่น
Brian White

7

คุณสามารถใช้Jar Jar Linksเพื่อลดขนาดไลบรารีภายนอกขนาดใหญ่เช่น Google Play Services (วิธีการ 16K!)

ในกรณีของคุณคุณจะริปทุกอย่างจากโถบริการ Google Play ยกเว้นcommon internalและdriveแพ็คเกจย่อย


4

สำหรับผู้ใช้ Eclipse ที่ไม่ได้ใช้ Gradle มีเครื่องมือที่จะแบ่งโถบริการ Google Play และสร้างขึ้นใหม่โดยใช้เฉพาะส่วนที่คุณต้องการเท่านั้น

ผมใช้strip_play_services.sh โดย dextorer

อาจเป็นเรื่องยากที่จะทราบแน่ชัดว่าควรรวมบริการใดบ้างเนื่องจากมีการอ้างอิงภายใน แต่คุณสามารถเริ่มต้นจากขนาดเล็กและเพิ่มในการกำหนดค่าได้หากปรากฎว่าสิ่งที่จำเป็นขาดหายไป


3

ฉันคิดว่าในระยะยาวการทำลายแอปของคุณด้วย dex หลายตัวน่าจะเป็นวิธีที่ดีที่สุด


2
ฉันกำลังมองหาวิธีที่เหมาะสมในการดำเนินการกับ Gradle: - / คำใบ้ใด ๆ ?
Ivan Morgillo


2

หากไม่ใช้ multidex ซึ่งทำให้กระบวนการสร้างช้ามาก คุณสามารถทำสิ่งต่อไปนี้ ในฐานะที่เป็นyahskaกล่าวถึงการใช้งานเฉพาะห้องสมุดให้บริการ Google เล่น สำหรับกรณีส่วนใหญ่จำเป็นต้องใช้สิ่งนี้เท่านั้น

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

นี่คือแพ็คเกจทั้งหมดที่มีให้เลือกรวบรวม APIs ลงในไฟล์ปฏิบัติการของคุณ

หากสิ่งนี้ไม่เพียงพอคุณสามารถใช้สคริปต์ gradle ใส่รหัสนี้ในไฟล์ '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
}

}

จากนั้นใช้สคริปต์นี้ใน build.gradle ของคุณเช่นนี้

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

1

หากใช้บริการ Google Play คุณอาจทราบว่ามีการเพิ่ม 20k + วิธีการ ดังที่ได้กล่าวไปแล้ว Android Studio มีตัวเลือกสำหรับการรวมบริการเฉพาะแบบโมดูลาร์ แต่ผู้ใช้ที่ติดอยู่กับ Eclipse ต้องใช้ modularisation ในมือของพวกเขาเอง :(

โชคดีที่มีเชลล์สคริปต์ที่ทำให้งานค่อนข้างง่าย เพียงแตกไฟล์ไปยังไดเร็กทอรี jar ของบริการ Google Play แก้ไขไฟล์. CONF ที่ให้มาตามต้องการและรันเชลล์สคริปต์

ตัวอย่างของการใช้งานเป็นที่นี่


1

หากใช้บริการ Google Play คุณอาจทราบว่ามีการเพิ่ม 20k + วิธีการ ดังที่ได้กล่าวไปแล้ว Android Studio มีตัวเลือกสำหรับการรวมบริการเฉพาะแบบโมดูลาร์ แต่ผู้ใช้ที่ติดอยู่กับ Eclipse ต้องใช้ modularisation ในมือของพวกเขาเอง :(

โชคดีที่มีเชลล์สคริปต์ที่ทำให้งานค่อนข้างง่าย เพียงแตกไฟล์ไปยังไดเร็กทอรี jar ของบริการ Google Play แก้ไขไฟล์. CONF ที่ให้มาตามต้องการและรันเชลล์สคริปต์

ตัวอย่างการใช้งานอยู่ที่นี่

อย่างที่เขาพูดฉันแทนที่compile 'com.google.android.gms:play-services:9.0.0'ด้วยไลบรารีที่ฉันต้องการและใช้งานได้จริง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.