การพัฒนาแบบวนซ้ำอย่างรวดเร็วสำหรับ Android
หน้านี้จะอธิบายวิธีที่ bazel mobile-install ช่วยให้การพัฒนาแบบวนซ้ำสำหรับ Android เร็วขึ้นมาก โดยจะอธิบายถึงประโยชน์ของแนวทางนี้เมื่อเทียบกับ
ความท้าทายของวิธีการติดตั้งแอปแบบเดิม
สรุป
หากต้องการติดตั้งการเปลี่ยนแปลงเล็กๆ น้อยๆ ในแอป Android อย่างรวดเร็ว ให้ทำดังนี้
- ค้นหา
android_binaryของแอปที่ต้องการติดตั้ง - ปิดใช้ Proguard โดยนำแอตทริบิวต์
proguard_specsออก - ตั้งค่าแอตทริบิวต์
multidexเป็นnative - ตั้งค่าแอตทริบิวต์
dex_shardsเป็น10 - เชื่อมต่ออุปกรณ์ที่ใช้ ART (ไม่ใช่ Dalvik) ผ่าน USB และเปิดใช้การแก้ไขข้อบกพร่อง USB ในอุปกรณ์
- เรียกใช้
bazel mobile-install :your_targetการเริ่มต้นแอปจะช้ากว่าปกติเล็กน้อย - แก้ไขโค้ดหรือทรัพยากร Android
- เรียกใช้
bazel mobile-install --incremental :your_target - คุณจึงไม่ต้องรอนาน
ตัวเลือกบรรทัดคำสั่งบางอย่างสำหรับ Bazel ที่อาจเป็นประโยชน์มีดังนี้
--adbบอก Bazel ว่าจะใช้ไบนารี adb ใด--adb_argใช้เพื่อเพิ่มอาร์กิวเมนต์พิเศษลงในบรรทัดคำสั่งของadbได้ การใช้งานที่มีประโยชน์อย่างหนึ่งคือการเลือกอุปกรณ์ที่ต้องการติดตั้ง หากมีอุปกรณ์หลายเครื่องเชื่อมต่อกับเวิร์กสเตชันbazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target--start_appจะเริ่มแอปโดยอัตโนมัติ
หากไม่แน่ใจ ให้ดูตัวอย่าง หรือติดต่อเรา
บทนำ
คุณสมบัติที่สำคัญที่สุดอย่างหนึ่งของเครื่องมือสำหรับนักพัฒนาแอปคือความเร็ว ความแตกต่างระหว่างการเปลี่ยนโค้ดและดูโค้ดทำงานภายใน 1 วินาทีกับการต้องรอเป็นนาทีหรือบางครั้งเป็นชั่วโมงก่อนที่จะได้รับความคิดเห็นว่าการเปลี่ยนแปลงของคุณทำงานได้ตามที่คาดไว้หรือไม่นั้นมีมาก
น่าเสียดายที่ Toolchain แบบเดิมของ Android สำหรับการสร้าง .apk นั้นมีขั้นตอนแบบลำดับที่ซับซ้อนหลายขั้นตอน และต้องทำทั้งหมดนี้เพื่อสร้างแอป Android ที่ Google การรอ 5 นาทีเพื่อสร้างการเปลี่ยนแปลงบรรทัดเดียวไม่ใช่เรื่องแปลกในโปรเจ็กต์ขนาดใหญ่ เช่น Google Maps
bazel mobile-install ช่วยให้การพัฒนาแบบวนซ้ำสำหรับ Android เร็วขึ้นมากด้วยการใช้การผสมผสานระหว่างการตัดทอนการเปลี่ยนแปลง การแบ่งงาน และการจัดการที่ชาญฉลาดขององค์ประกอบภายในของ Android โดยไม่ต้องเปลี่ยนแปลงโค้ดของแอป
ปัญหาเกี่ยวกับการติดตั้งแอปแบบเดิม
การสร้างแอป Android มีปัญหาบางอย่าง เช่น
Dexing โดยค่าเริ่มต้น "dx" จะเรียกใช้เพียงครั้งเดียวในการสร้าง และไม่ทราบวิธีนำงานจากการสร้างครั้งก่อนมาใช้ซ้ำ ซึ่งจะแปลงทุกเมธอดเป็น Dex อีกครั้ง แม้ว่าจะมีการเปลี่ยนแปลงเพียงเมธอดเดียวก็ตาม
การอัปโหลดข้อมูลไปยังอุปกรณ์ adb ไม่ได้ใช้แบนด์วิดท์ทั้งหมดของการเชื่อมต่อ USB 2.0 และแอปขนาดใหญ่อาจใช้เวลาในการอัปโหลดนานมาก ระบบจะอัปโหลดทั้งแอปแม้ว่าจะมีการเปลี่ยนแปลงเพียงเล็กน้อย เช่น ทรัพยากรหรือเมธอดเดียว ซึ่งอาจเป็นจุดคอขวดที่สำคัญ
การคอมไพล์เป็นโค้ดที่มาพร้อมเครื่อง Android L ได้เปิดตัว ART ซึ่งเป็นรันไทม์ใหม่ของ Android ที่คอมไพล์แอปแบบล่วงหน้าแทนที่จะคอมไพล์แบบทันทีเหมือน Dalvik ซึ่งจะทำให้แอปทำงานได้เร็วขึ้นมาก แต่จะใช้เวลาในการติดตั้งนานขึ้น ซึ่งเป็นข้อแลกเปลี่ยนที่ดีสำหรับผู้ใช้ เนื่องจากโดยปกติแล้วผู้ใช้จะติดตั้งแอปเพียงครั้งเดียวและใช้งานหลายครั้ง แต่จะทำให้การพัฒนาช้าลงในกรณีที่แอปมีการติดตั้งหลายครั้งและแต่ละเวอร์ชันจะทำงานเพียงไม่กี่ครั้งเท่านั้น
แนวทางของ bazel mobile-install
bazel mobile-install ได้ทำการปรับปรุงดังนี้
การ Dex แบบ Shard หลังจากสร้างโค้ด Java ของแอปแล้ว Bazel จะแบ่งไฟล์คลาส ออกเป็นส่วนที่มีขนาดเท่ากันโดยประมาณ และเรียกใช้
dxแยกกันdxจะไม่เรียกใช้ใน Shard ที่ไม่มีการเปลี่ยนแปลงตั้งแต่การสร้างครั้งล่าสุดการโอนไฟล์แบบเพิ่ม ระบบจะนำทรัพยากร Android, ไฟล์ .dex และไลบรารีแบบเนทีฟ ออกจาก .apk หลักและจัดเก็บไว้ในไดเรกทอรีการติดตั้งบนอุปกรณ์เคลื่อนที่แยกต่างหาก ซึ่งช่วยให้คุณอัปเดตโค้ดและทรัพยากร Android ได้อย่างอิสระโดยไม่ต้องติดตั้งแอปทั้งแอปใหม่ ดังนั้น การโอนไฟล์จึงใช้เวลาน้อยลง และระบบจะคอมไพล์ไฟล์ .dex ที่มีการเปลี่ยนแปลงใหม่ในอุปกรณ์เท่านั้น
โหลดส่วนต่างๆ ของแอปจากภายนอก .apk ระบบจะใส่แอปพลิเคชัน Stub ขนาดเล็กไว้ใน .apk ซึ่งจะโหลดทรัพยากร Android, โค้ด Java และโค้ดแบบเนทีฟจากไดเรกทอรีการติดตั้งบนอุปกรณ์เคลื่อนที่ จากนั้นจะโอนการควบคุมไปยังแอปจริง ทั้งหมดนี้จะโปร่งใสต่อแอป ยกเว้นในกรณีที่พบได้ยากบางกรณีที่อธิบายไว้ด้านล่าง
การจัดทำดัชนีแบบชาร์ด
การแบ่ง DEX ออกเป็นส่วนๆ นั้นค่อนข้างตรงไปตรงมา เมื่อสร้างไฟล์ .jar แล้ว เครื่องมือจะ
แบ่งไฟล์เหล่านั้นออกเป็นไฟล์ .jar แยกกันซึ่งมีขนาดใกล้เคียงกัน จากนั้นจะเรียกใช้
dx ในไฟล์ที่มีการเปลี่ยนแปลงนับตั้งแต่การสร้างครั้งก่อน ตรรกะที่กำหนดว่าควรใช้ Dex กับ Shard ใดไม่ได้เจาะจงสำหรับ Android แต่ใช้อัลกอริทึมการตัดแต่งการเปลี่ยนแปลงทั่วไปของ Bazel
อัลกอริทึมการแบ่งข้อมูลเวอร์ชันแรกเพียงแค่จัดเรียงไฟล์ .class ตามลำดับตัวอักษร แล้วตัดรายการออกเป็นส่วนๆ ที่มีขนาดเท่ากัน แต่ปรากฏว่าวิธีนี้ไม่เหมาะสม หากมีการเพิ่มหรือนำคลาสออก (แม้จะเป็นคลาสที่ซ้อนกันหรือคลาสที่ไม่ระบุชื่อ) ก็จะทำให้คลาสทั้งหมดที่อยู่หลังคลาสนั้นตามลำดับตัวอักษรเลื่อนไป 1 ตำแหน่ง ส่งผลให้ต้องทำการ Dexing Shard เหล่านั้นอีกครั้ง จึงตัดสินใจที่จะแยกแพ็กเกจ Java แทนที่จะแยกคลาสแต่ละคลาส แน่นอนว่าการดำเนินการนี้ยังคงส่งผลให้มีการจัดทำดัชนีหลายๆ ชิ้นหากมีการเพิ่มหรือนำแพ็กเกจใหม่ออก แต่การดำเนินการนี้จะเกิดขึ้นไม่บ่อยเท่ากับการเพิ่มหรือนำคลาสเดียวออก
ไฟล์ BUILD จะควบคุมจำนวน Shard (โดยใช้แอตทริบิวต์ android_binary.dex_shards) ในโลกที่สมบูรณ์แบบ Bazel จะ
กำหนดจำนวน Shard ที่ดีที่สุดโดยอัตโนมัติ แต่ปัจจุบัน Bazel ต้องทราบ
ชุดการดำเนินการ (เช่น คำสั่งที่จะดำเนินการระหว่างการสร้าง) ก่อน
ที่จะดำเนินการใดๆ จึงไม่สามารถกำหนดจำนวน Shard ที่เหมาะสมได้
เนื่องจากไม่ทราบว่าในท้ายที่สุดจะมีคลาส Java กี่คลาสใน
แอป โดยทั่วไปแล้ว ยิ่งมี Shard มากเท่าใด การสร้างและการ
ติดตั้งก็จะยิ่งเร็วขึ้น แต่การเริ่มต้นแอปจะช้าลงเนื่องจากลิงก์เกอร์แบบไดนามิก
ต้องทำงานมากขึ้น โดยปกติแล้ว จำนวนที่เหมาะสมจะอยู่ระหว่าง 10 ถึง 50 ชิ้น
การโอนไฟล์แบบเพิ่ม
หลังจากสร้างแอปแล้ว ขั้นตอนถัดไปคือการติดตั้งแอป ซึ่งควรทำด้วย ความพยายามน้อยที่สุด การติดตั้งประกอบด้วยขั้นตอนต่อไปนี้
- การติดตั้ง .apk (โดยปกติจะใช้
adb install) - การอัปโหลดไฟล์ .dex, ทรัพยากร Android และไลบรารีเนทีฟไปยัง ไดเรกทอรีการติดตั้งบนอุปกรณ์เคลื่อนที่
ขั้นตอนแรกไม่ได้เพิ่มขึ้นมากนัก เนื่องจากแอปจะติดตั้งหรือไม่ติดตั้ง
ก็ได้ ปัจจุบัน Bazel อาศัยผู้ใช้ในการระบุว่าควรทำขั้นตอนนี้หรือไม่
ผ่าน--incrementalตัวเลือกบรรทัดคำสั่ง เนื่องจากไม่สามารถระบุได้ใน
ทุกกรณีว่าจำเป็นหรือไม่
ในขั้นตอนที่ 2 ระบบจะเปรียบเทียบไฟล์ของแอปจากบิลด์กับไฟล์ Manifest ในอุปกรณ์ที่แสดงรายการไฟล์ของแอปที่อยู่ในอุปกรณ์และ Checksum ของไฟล์เหล่านั้น ระบบจะอัปโหลดไฟล์ใหม่ไปยังอุปกรณ์ อัปเดตไฟล์ที่มีการเปลี่ยนแปลง และลบไฟล์ที่นำออกไปจากอุปกรณ์ หากไม่มีไฟล์ Manifest ระบบจะถือว่าต้องอัปโหลดทุกไฟล์
โปรดทราบว่าการเปลี่ยนไฟล์ในอุปกรณ์แต่ไม่เปลี่ยน Checksum ในไฟล์ Manifest อาจหลอกอัลกอริทึมการติดตั้งแบบเพิ่มทีละรายการได้ เราสามารถป้องกันปัญหานี้ได้โดยการคำนวณผลรวมตรวจสอบของไฟล์ในอุปกรณ์ แต่เราเห็นว่าการเพิ่มเวลาในการติดตั้งนั้นไม่คุ้มค่า
แอปพลิเคชัน Stub
แอปพลิเคชัน Stub คือที่ที่เกิดการโหลดไฟล์ DEX, โค้ดที่มาพร้อมเครื่อง และ
ทรัพยากร Android จากไดเรกทอรี mobile-install ในอุปกรณ์
การโหลดจริงจะดำเนินการโดยการสร้างคลาสย่อยของ BaseDexClassLoader และเป็นเทคนิคที่มีเอกสารประกอบค่อนข้างดี
ซึ่งจะเกิดขึ้นก่อนที่จะโหลดคลาสใดๆ ของแอป เพื่อให้คลาสแอปพลิเคชันใดๆ ที่อยู่ใน APK สามารถวางไว้ในไดเรกทอรี mobile-install ในอุปกรณ์เพื่อให้สามารถอัปเดตได้โดยไม่ต้องใช้ adb install
การดำเนินการนี้ต้องเกิดขึ้นก่อนที่จะโหลดคลาสใดๆ ของแอป เพื่อไม่ให้คลาสแอปพลิเคชันต้องอยู่ใน .apk ซึ่งหมายความว่าการเปลี่ยนแปลงคลาสเหล่านั้นจะต้องมีการติดตั้ง ใหม่ทั้งหมด
โดยการแทนที่Applicationคลาสที่ระบุใน
AndroidManifest.xmlด้วย
แอปพลิเคชัน Stub ซึ่งจะ
ควบคุมเมื่อเริ่มแอป และปรับแต่ง Class Loader และ
Resource Manager อย่างเหมาะสมในเวลาที่เร็วที่สุด (ตัวสร้าง) โดยใช้
การสะท้อน Java ในส่วนภายในของเฟรมเวิร์ก Android
อีกสิ่งหนึ่งที่แอปพลิเคชัน Stub ทำคือการคัดลอกไลบรารีเนทีฟ
ที่ติดตั้งโดย mobile-install ไปยังตำแหน่งอื่น การดำเนินการนี้จำเป็นเนื่องจาก
โปรแกรมลิงก์แบบไดนามิกต้องตั้งค่าบิต X ในไฟล์ ซึ่งทำไม่ได้สำหรับ
ตำแหน่งใดก็ตามที่เข้าถึงได้โดย adb ที่ไม่ใช่รูท
เมื่อดำเนินการทั้งหมดนี้แล้ว แอปพลิเคชัน Stub จะสร้างอินสแตนซ์ของคลาส Application จริง โดยเปลี่ยนการอ้างอิงทั้งหมดไปยังแอปพลิเคชันจริงภายในเฟรมเวิร์ก Android
ผลลัพธ์
ประสิทธิภาพ
โดยทั่วไปแล้ว bazel mobile-install จะช่วยให้การสร้าง
และการติดตั้งแอปขนาดใหญ่หลังจากมีการเปลี่ยนแปลงเล็กน้อยเร็วขึ้น 4-10 เท่า
เราคำนวณตัวเลขต่อไปนี้สำหรับผลิตภัณฑ์บางอย่างของ Google
ซึ่งแน่นอนว่าขึ้นอยู่กับลักษณะของการเปลี่ยนแปลง การคอมไพล์ใหม่หลังจากเปลี่ยนไลบรารีพื้นฐานจะใช้เวลานานกว่า
ข้อจำกัด
กลเม็ดที่แอปพลิเคชัน Stub ใช้ไม่ได้ผลในบางกรณี กรณีต่อไปนี้แสดงให้เห็นว่าฟีเจอร์นี้ทำงานไม่เป็นไปตามที่คาดไว้
เมื่อแคสต์
Contextไปยังชั้นเรียนApplicationในContentProvider#onCreate()เมธอดนี้จะเรียกใช้ในระหว่างการเริ่มต้นแอปพลิเคชัน ก่อนที่เราจะมีโอกาสแทนที่อินสแตนซ์ของคลาสApplicationดังนั้นContentProviderจะยังอ้างอิงแอปพลิเคชัน Stub แทนแอปพลิเคชันจริง อาจกล่าวได้ว่านี่ไม่ใช่ข้อบกพร่องเนื่องจากคุณไม่ควร ดาวน์แคสต์Contextแบบนี้ แต่ดูเหมือนว่าปัญหานี้จะเกิดขึ้นในแอป บางแอปของ Googleทรัพยากรที่ติดตั้งโดย
bazel mobile-installจะใช้ได้ภายในแอปเท่านั้น หากแอปอื่นๆ เข้าถึงทรัพยากรผ่านPackageManager#getApplicationResources()ทรัพยากรเหล่านี้จะมาจาก การติดตั้งครั้งล่าสุดที่ไม่ใช่การติดตั้งแบบเพิ่มอุปกรณ์ที่ไม่ได้ใช้ ART แม้ว่าแอปพลิเคชัน Stub จะทำงานได้ดีใน Froyo และเวอร์ชันที่ใหม่กว่า แต่ Dalvik มีข้อบกพร่องที่ทำให้คิดว่าแอปไม่ถูกต้องหากมีการกระจายโค้ดในไฟล์ .dex หลายไฟล์ในบางกรณี เช่น เมื่อใช้คำอธิบายประกอบ Java ในลักษณะเฉพาะ ตราบใดที่แอปของคุณไม่ทำให้เกิดข้อบกพร่องเหล่านี้ แอปก็ควรจะทำงานกับ Dalvik ได้ด้วย (โปรดทราบว่าเราไม่ได้มุ่งเน้นที่การรองรับ Android เวอร์ชันเก่าโดยเฉพาะ)