17 February 2016

DEX with Over 65K Methods และการทำ MultiDex ที่นักพัฒนาแอนดรอยด์ควรรู้จักไว้

Updated on


"The number of method references in a .dex file cannot exceed 64K"

        เจ้าของบล็อกเชื่อว่ามีนักพัฒนาจำนวนไม่น้อยที่เคยเจอปัญหานี้ระหว่าง Build Project เป็น APK ซึ่งสุดท้ายก็จะจบด้วยการงมหาทางแก้ไขกันไปจนกว่ามันจะ Build APK ได้

        ว่าแต่มันคืออะไร เกิดจากอะไร แล้วจะแก้ไขมันอย่างไรได้บ้างล่ะ?

ทำความเข้าใจเกี่ยวกับไฟล์ .dex กันก่อน

        โดยปกติของ Java เวลาที่ Compile ไฟล์ .java มันจะกลายเป็นไฟล์​ .class เพื่อให้ Java VM เอาไป Execute เพื่อทำงานได้

        สำหรับแอนดรอยด์นั้นไม่ได้ใช้ Java VM แต่จะใช้ Dalvik VM แทนเพื่อ Execute ไฟล์ .dex เพราะว่าไฟล์ .dex เป็นไฟล์ที่ถูกบีบอัดมาจาก .class อีกทีจึงทำให้มีขนาดกระทัดรัดและสามารถทำงานบนอุปกรณ์พกพาอย่าง Smartphone ได้ดีกว่า

        ดังนั้นบนแอนดรอยด์เมื่อโปรเจค Java ถูกทำให้กลายเป็น APK สิ่งที่เกิดขึ้นก็คือไฟล์ .java ถูก Compile กลายเป็น .class และจะถูกแปลงให้กลายเป็น .dex แล้วยัดรวมไว้กับพวก Resource อื่นๆจนกลายเป็นไฟล์ .apk ที่คุ้นเคยกันดีนั่นเอง

        ประมาณนี้


        ซึ่งข้อจำกัดของไฟล์ .dex ก็คือจำนวน Method ที่สามารถรองรับจะได้มากสุดแค่ 65,536 Method เท่านั้น ซึ่งประกอบไปด้วย Android Framework, Library และโค๊ดที่ผู้ที่หลงเข้ามาอ่านเป็นคนเขียนขึ้นมาเอง

Exceed 65K Method ก็คือ Method เยอะเกินที่ .dex จะรับไหว

        นั่นล่ะครับสาเหตุของปัญหานี้ เพราะว่า .dex มันค่อนข้างจำกัด อาจจะเห็นว่าบางทีก็ 65K บางทีก็ 64K ซึ่งจริงๆมันก็เหมือนกันนะครับ เพราะ 65,536 / 1.024 ก็คือ 64,000 นั่นเอง


         ซึ่งเป็นเรื่องปกติที่โปรเจคใหญ่ๆมักจะเจอปัญหานี้กัน แต่ถ้าโปรเจคเล็กเจอปัญหานี้ก็อ่านต่อได้เลยว่าเพราะอะไร

อะไรนะ? ไม่เคยเจอปัญหานี้?

        ถ้าอยากลองดูว่ามันเป็นยังไงให้ลองสร้างโปรเจคขึ้นมาเปล่าๆซักตัวแล้วเพิ่ม Dependencies เข้าไปแบบนี้

compile 'com.google.android.gms:play-services:8.4.0'
compile 'com.android.support:design:23.1.1'
compile 'com.android.support:cardview-v7:23.1.1'
compile 'com.android.support:recyclerview-v7:23.1.1'
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:support-v4:23.1.1'
compile 'com.android.support:gridlayout-v7:23.1.1'
compile 'com.android.support:mediarouter-v7:23.1.1'
compile 'com.android.support:palette-v7:23.1.1'
compile 'com.android.support:preference-v7:23.1.1'
compile 'com.android.support:support-v13:23.1.1'
compile 'com.android.support:preference-v14:23.1.1'
compile 'com.android.support:preference-leanback-v17:23.1.1'
compile 'com.android.support:leanback-v17:23.1.1'
compile 'com.android.support:support-annotations:23.1.1'
compile 'com.android.support:customtabs:23.1.1'
compile 'com.android.support:percent:23.1.1'
compile 'com.squareup:otto:1.3.8'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
compile 'com.squareup.okhttp3:okhttp:3.0.1'

        แล้วตั้ง Minimum SDK Version เป็น 17 จากนั้นลองกด Run เพื่อให้มัน Build แล้วติดตั้งบนเครื่องดูครับ แล้วก็บู้มกลายเป็นโกโก้ครั้นช์ทันที~

แก้ปัญหายังไง?

        ผู้ที่หลงเข้ามาอ่านอาจจะทำ MultiDex เพื่อแก้ปัญหานี้ แต่เจ้าของบล็อกขอแนะนำวิธีแก้ไขที่เป็นขั้นเป็นตอนดีกว่าเนอะ เพราะการทำ MultiDex ไม่ใช่ทางออกที่ดีเสมอไป (ถ้าวิธีอื่นช่วยได้)

1. ลดจำนวน Dependencies ที่ใช้งานให้น้อยลง

        ถ้าปัญหามันเกิดมาจาก Method เยอะเกินไป ดังนั้นก็ควรเริ่มแก้ไขที่ต้นเหตุสิเนอะ

        เริ่มจากนั่งไล่เช็คในโปรเจคของผู้ที่หลงเข้ามาอ่านก่อนว่าจำเป็นทุกๆอันจริงๆหรือป่าว อันไหนตัดออกลดทอนได้ก็ควรทำ เพราะหลายๆโปรเจคที่เจ้าของบล็อกเจอคือใส่ Dependencies มาในโปรเจคเกินจำเป็น ทั้งๆที่ไม่ได้ใช้งาน หรือว่าใช้ Dependencies คนละเจ้ากัน อย่างเช่นใช้ทั้ง Glide หรือ Picasso ทั้งๆที่ในโปรเจคนั้นควรเลือกว่าจะใช้แค่ Glide หรือ Picasso เท่านั้น

        หรือที่เจอบ่อยที่สุดก็คือเรียก Dependencies ของ Google Play Services มาทั้งก้อนแบบนี้

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

        ซึ่งใน Google Play Services ทั้งก้อนเนี่ย มันใหญ่มาก มากเกินจำเป็น อย่างของ 8.4.0 พบว่ามี Method มากถึง 58,180 ตัวเลยนะ!!!

        ดังนั้นทางที่ดีคือควรเลือกใช้ Dependencies เฉพาะบางตัวที่ต้องการใช้งานจริงๆก็พอ

compile 'com.google.android.gms:play-services-location:8.4.0'
compile 'com.google.android.gms:play-services-maps:8.4.0'
compile 'com.google.android.gms:play-services-ads:8.4.0'

        แบบนี้ก็จะเหลือแค่ 27,766 Method แทน ซึ่งก็ยังเยอะอยู่ดีนะ เพราะว่าทั้ง 3 ตัวนี้ก็จะมี Sub Dependencies ด้วย ซึ่งจะมีพวก Base, Android Support v4 หรือ Support Annotation เป็นต้น ดังนั้นถ้าจะใช้ Google Play Services ก็ต้องเตรียมใจเลยว่าต้องเสีย Method Count ส่วนหนึ่งให้กับมันด้วย


        อย่างเช่น Location API ของ Google Play Services มี Method แค่ 1,829 ตัว แต่ทว่าตัวมันก็มี Dependencies ที่จำเป็นหลายตัว รวมๆแล้วมี Method ถึง 15,987 ตัวเลย

        ดังนั้นการ Reduce Dependencies ในโปรเจคจึงเป็นสิ่งที่ควรทำตั้งแต่แรกครับ เพราะถ้าลดได้ก็ช่วยแก้ปัญหาได้ และช่วยลด Build Time ให้น้อยลงได้อีกด้วย (ส่วนหนึ่งที่ Gradle มี Build Time นานก็เพราะ Dependencies เยอะนี่แหละ) เรียกว่าได้ประโยชน์สองต่อเลยทีเดียว

2. กำหนด Minimum SDK Version เป็น 21 ขึ้นไป

        วิธีนี้เป็นวิธีที่ไม่นิยมทำกันครับ เพราะการกำหนด Minimum SDK Version 21 หมายความว่าแอพตัวนั้นๆจะรองรับแอนดรอยด์เวอร์ชันต่ำสุดได้แค่ Android 5.0 เท่านั้น เหมาะสำหรับแอพใหม่ๆที่เน้นฟีเจอร์ใหม่ๆบน Android 5.0 ขึ้นไปเท่านั้น ซึ่งแอพส่วนใหญ่คงไม่แฮปปี้ซักเท่าไรถ้าเวอร์ชันต่ำกว่า 5.0 ใช้งานไม่ได้

        สาเหตุที่กำหนด Minimum SDK เป็นเวอร์ชัน 21 แล้วแก้ปัญหาได้ก็เพราะว่า ในเวอร์ชันนั้นเป็นต้นไปแอนดรอยด์ได้เปลี่ยนไปใช้ ART (Android Runtime) แทน Dalvik แล้ว ซึ่ง ART นั้นจะสามารถรองรับ .dex หลายๆไฟล์ได้ หรือที่เค้าเรียกกันว่า MultiDex น่ะแหละ เมื่อรองรับได้หลายๆไฟล์ก็หมายความว่าสามารถรองรับโปรเจคที่มี Method เกิน 65,536 ตัวได้

        ซี่งในการทำงานของ ART จะมีขั้นตอนมากกว่า Dalvik นิดหน่อย คือหลังจากที่ Compile ได้มาเป็น .dex แล้ว (เมื่อใช้ ART จะสามารถมีหลายไฟล์ได้อัตโนมัติ) ก็จะมี AOT (Ahead-of-Time) ที่จะมาช่วย Compile เจ้าไฟล์ .dex ให้กลายเป็น .oat แทน


        ดังนั้นถ้าโปรเจคไหนรองรับเวอร์ชันขั้นต่ำเป็น 21 ขึ้นไปก็จะถูก Build ในรูปแบบของ ART ทันที และก็จะหมดปัญหาเรื่อง Over 65K Methods ทันที เย้~

3. ใช้ ProGuard ช่วยลด Method ที่ไม่จำเป็นออกไป

        อันนี้น่าจะเป็นวิธีแก้สำหรับ Release Build ซะมากกว่า เพราะปกติแล้วเวลา Debug Build จะไม่นิยมใส่ ProGuard กัน เนื่องจากมันเสียเวลาและดู Log ได้ยาก

        โดยปกติแล้ว ProGuard จะคอย Shrink และ Obfuscate ตอน Build ให้ซึ่งตอน Shrink มันจะตัด Method ที่ไม่ได้ใช้ออกไปให้ (สามารถสั่ง Keep บางคลาสได้โดยกำหนดใน proguard-rules.pro) ซึ่งจะช่วยให้ลดจำนวน Method ในโปรเจคลงได้พอสมควร

4. ยอมทำ MultiDex ก็ได้วะ!

        เมื่อหมดหนทางแล้ว สุดท้ายก็คงต้องทำใจยินยอมใช้ MultiDex เข้ามาช่วยน่ะแหละ ซึ่ง MultiDex จะช่วยให้ไฟล์​ .dex มีมากกว่าหนึ่งไฟล์ จึงทำให้มี Method เกิน 65,536 ตัวบน Dalvik ได้ ซึ่งทาง Google ก็ได้มีไลบรารีที่ชื่อว่า MultiDex เพื่อรองรับกับโปรเจคเวอร์ชันต่ำกว่า 5.0 (สำหรับ Dalvik) โดยมีเงื่อนไขว่าต้องใช้ Android SDK Build Tools เวอร์ชัน 21.1 ขึ้นไป (ทุกวันนี้ก็ใช้เวอร์ชันสูงกว่านั้นกันหมดแล้ว)

ข้อจำกัดเกี่ยวกับ MultiDex ที่ควรรู้ก่อนจะใช้งาน

        • อาจจะทำให้เกิด ANR ตอนเปิดแอพได้ ถ้าทำ MultiDex แล้ว .dex ชุดหลังมีขนาดใหญ่
        • ควรใช้กับโปรแกรมที่กำหนด Minimum SDK Version 14 ขึ้นไป (ทุกวันนี้ก็ควรกำหนดเป็น 15 ขึ้นไปอยู่แล้วเนอะ)
        • การใช้ MultiDex จะทำให้แอพใช้ Memory มากกว่าปกติ และอาจจะทำให้ Crash ระหว่างทำงานได้ เพราะ Memory Allocation เกินจำกัด
        • ตอน Build Gradle จะมี Build Time นานขึ้นกว่าเดิม

การทำ MultiDex ให้กับโปรเจค

        ให้กำหนด build.gradle ของ Module ที่ต้องการจะ Build ลงไปดังนี้

android {

    ...
    buildToolsVersion "21.1.0"

    defaultConfig {

        minSdkVersion 14
        ...

        multiDexEnabled true
    }
    ...
}

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

        ตรง buildToolsVersion ให้ใช้เวอร์ชันที่สูงกว่า 21.1.0 นะ (แก้ไขจากในตัวอย่างได้เลย)

        และใน Android Manifest ต้องกำหนดให้ Application ไปใช้คลาส MultiDexApplication ด้วย

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    ...>
    
    <application
        ...
        android:name="android.support.multidex.MultiDexApplication">

        ...

    </application>
</manifest>

        ซึ่งคลาส MultiDexApplication ตัวนี้จะไปจัดในการส่วนของการสร้างไฟล์ .dex หลายๆไฟล์ให้เอง

        แต่ถ้าโปรเจคของผู้ที่หลงเข้ามาอ่านนั้นมีการใช้ Custom Application ของตัวเอง ก็ให้ประกาศคำสั่งสำหรับ MultiDex ลงในคลาสนั้นๆดังนี้

package com.akexorcist.multidextest;

import android.app.Application;
import android.content.Context;
import android.support.multidex.MultiDex;

public class MyApplication extends Application {

    ...
    
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }
}

        เพียงเท่านี้ MyApplication ก็รองรับ MultiDex แล้ว สามารถเอาไปกำหนดใน Android Manifest แทน MultiDexApplication ได้เลย

แนะนำการใช้ MultiDex ระหว่างที่ยังพัฒนาไม่เสร็จ

        เนื่องจากการใช้ MultiDex จะทำให้เสียเวลา Build Project นานมากขึ้น ซึ่งมันจะกินเวลาพัฒนานานมากขึ้นไปอีก ดังนั้นเพื่อความสะดวกรวดเร็วจึงนิยมยัด MultiDex ไว้ตอนทำเป็น Production เท่านั้น ส่วนตอน Develop ก็ให้ Build สำหรับ ART ไปเลย เพื่อความสะดวกรวดเร็ว (นั่นก็หมายความว่าต้องเทสกับเครื่องแอนดรอยด์เวอร์ชัน 5.0 ขึ้นไป)

        นั่นก็หมายความว่าจะมีการใช้ Build Variant ในโปรเจคด้วย โดยแยกเป็น Developer กับ Production ใน build.gradle แบบนี้

...

android {
    ...

    productFlavors {
        develop {
            minSdkVersion 21
        }
        production {
            minSdkVersion 14
            multiDexEnabled true
        }
    }

    ...
}

dependencies {

    ...
    compile 'com.android.support:multidex:1.0.0'

}

        ส่วนการใช้ Build Variant ลองไปอ่านเพิ่มเติมกันได้ที่ ทำชีวิตให้ง่ายด้วย Build Variants

เช็คยังไงว่าโปรเจคนั้นๆใช้ Method Count ไปเท่าไร ?

        โดยปกติแล้วใน Android Studio จะไม่บอกว่าโปรเจคของผู้ที่หลงเข้ามาอ่านใช้ Method ไปทั้งหมดเท่าไร ดังนั้นถ้าอยากจะรู้ก็ต้องติดตั้ง Plugin เพิ่มเล็กน้อย โดยเป็น Plugin ของ Gradle ที่มีชื่อว่า DexCount ของ KeepSafe

        วิธีใช้ก็โคตรง่าย เพียงแค่ใส่ใน build.gradle ของ Module ที่ต้องการดังนี้

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.4.1'
    }
}

apply plugin: 'com.getkeepsafe.dexcount'

        เวลาที่ Build Gradle เสร็จในแต่ละครั้งจะมีการแจ้งบอกใน Console (หน้าต่าง Gradle Console และหน้าต่าง Message) ว่าโปรเจคนี้มี Method Count เท่าไร


        และที่เจ้าของบล็อกชอบก็คือมี Report ให้ด้วยว่าคลาสไหนใช้ไปเท่าไรบ้าง ซึ่งจะสร้างเป็นไฟล์ txt อยู่ในโฟลเดอร์ build 

จะรู้ได้ไงว่า Dependencies ที่เอามาใช้ในโปรเจคมี Method Count เท่าไร

        การเช็ค Method Count ใน Dependencies ที่เอามาใช้ในโปรเจคสามารถเช็คได้จากหน้าเว็ป Methods Count - Your solution for a perfectly fit APK สามารถใส่ชื่อ Dependencies ลงในนั้นได้เลย แล้วเว็ปก็จะบอกให้ว่าใช้เท่าไร


        และยังมี Plugin ให้ติดตั้งบน Android Studio ด้วย เพื่อเช็ค Method Count จากใน build.gradle ได้เลย เพียงแค่เปิดหน้าต่าง Plugin ใน Android Studio ขึ้นมาแล้วค้นหาว่า Android Methods Count แล้วติดตั้งให้เรียบร้อย

        โดยที่ Dependencies แต่ละตัวจะมีการบอก Method Count ให้ในโค๊ดทันที และสามารถเอาเม้าส์ไปวางบนสัญลักษณ์วงกลมสีน้ำเงินข้างหน้าบรรทัดนั้นๆเพื่อดู Dependencies ที่เกี่ยวข้องกับตัวนั้นๆได้ด้วย


สรุป

        ปัญหาเรื่อง Over 65K Methods เป็นเรื่องปกติที่โปรเจคใหญ่ๆมักจะพบเจอ (งานของเจ้าของบล็อกก็เจออยู่) แต่ทว่าการทำ MultiDex นั้นไม่ใช่ทางออกที่ดีเสมอไป เรียกว่าเป็นหนทางสุดท้ายมากกว่า ซึ่งผู้ที่หลงเข้ามาอ่านควรเช็คที่ตัวโปรเจคก่อนว่ามีการใช้ Dependencies เกินจำเป็นมั้ย ตัวไหนตัดได้มั้ย เพราะมันจะส่งผลดีในกว่าการทำ MultiDex มาก (ที่แน่ๆคือจะได้ลด Build Time ด้วย) ถ้าสุดทางแก้หรือจำเป็นจริงๆแล้วก็คงต้องยอมทำ MultiDex แหละนะ

        และการลง Plugin ก็จะช่วยเพิ่มความสะดวกในการเช็ค Method Count และดูว่า Dependencise ตัวไหนที่ใช้มากและใช้น้อยได้อีกด้วย เพราะงั้นลงไว้ซะจะได้ไม่ลำบากชีวิต