การพัฒนาแบบทำซ้ำอย่างรวดเร็วสำหรับ 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
จะเริ่มแอปโดยอัตโนมัติ
หากมีข้อสงสัย ให้ดูตัวอย่างหรือติดต่อเรา
บทนำ
หนึ่งในคุณสมบัติที่สําคัญที่สุดของเครื่องมือทางเทคนิคของนักพัฒนาซอฟต์แวร์คือความเร็ว การเปลี่ยนแปลงโค้ดและเห็นโค้ดทํางานภายในไม่กี่วินาทีนั้นแตกต่างกับต้องรอหลายนาทีหรือบางครั้งหลายชั่วโมงจึงจะได้รับความคิดเห็นว่าการเปลี่ยนแปลงทํางานตามที่คาดไว้หรือไม่
น่าเสียดายที่เครื่องมือ Android แบบเดิมสำหรับการสร้าง .apk นั้นต้องใช้ จำนวนมาก ต้องดำเนินการเป็นขั้นตอน ตามลำดับ และทั้งหมดนี้ต้องทำเพื่อ สร้างแอป Android ที่ Google ใช้เวลา 5 นาทีในการสร้างเส้นเดียว ก็ไม่ปกติในโครงการขนาดใหญ่อย่าง Google Maps
bazel mobile-install
ทำให้การพัฒนาแบบวนซ้ำสำหรับ Android เร็วขึ้นมากโดย
โดยใช้ทั้งการตัดการเปลี่ยนแปลง การชาร์ดดิ้งงาน และการปรับแต่งที่ชาญฉลาด
ใช้งานภายใน Android ได้โดยไม่ต้องเปลี่ยนโค้ดของแอป
ปัญหาเกี่ยวกับการติดตั้งแอปแบบดั้งเดิม
การสร้างแอป Android มีปัญหาบางอย่าง ได้แก่
การแยกไฟล์ โดยค่าเริ่มต้น "dx" ถูกเรียกใช้เพียงครั้งเดียวในการสร้าง และไม่ใช่ รู้วิธีนำงานจากงานสร้างก่อนหน้ามาใช้ใหม่ เพราะโมเดลนั้นจะถอดรหัสทุกเมธอดอีกครั้ง แม้จะมีการเปลี่ยนแปลงเพียงวิธีเดียว
การอัปโหลดข้อมูลไปยังอุปกรณ์ adb ไม่ได้ใช้แบนด์วิดท์ของการเชื่อมต่อ USB 2.0 อย่างเต็มรูปแบบ และแอปขนาดใหญ่อาจใช้เวลานานในการอัปโหลด ทั้งแอป แม้ว่าจะมีการเปลี่ยนแปลงเพียงเล็กน้อยก็ตาม ตัวอย่างเช่น ทรัพยากรหรือ วิธีนี้เป็นจุดคอขวดที่สำคัญ
การคอมไพล์เป็นโค้ดเนทีฟ Android L ได้เปิดตัว ART ซึ่งเป็นรันไทม์ Android ใหม่ที่จะคอมไพล์แอปล่วงหน้าแทนที่จะคอมไพล์แบบทันท่วงทีเหมือน Dalvik ซึ่งจะทำให้แอปทำงานได้เร็วขึ้นมาก แต่ใช้เวลาติดตั้งนานขึ้น นี่เป็นข้อเสียเปรียบที่ยอมรับได้สำหรับผู้ใช้ เนื่องจากโดยทั่วไปผู้ใช้จะติดตั้งแอปเพียงครั้งเดียวและใช้หลายครั้ง แต่จะทำให้การพัฒนาช้าลงเมื่อมีการติดตั้งแอปหลายครั้งและแต่ละเวอร์ชันทำงานเพียงไม่กี่ครั้ง
แนวทางของ bazel mobile-install
bazel mobile-install
ทำการปรับปรุงต่อไปนี้
Dexing แบบชาร์ด หลังจากสร้างโค้ด Java ของแอปแล้ว Bazel จะแบ่งไฟล์คลาสออกเป็นส่วนๆ ขนาดเท่าๆ กันและเรียกใช้
dx
แยกกัน ระบบจะไม่เรียกใช้dx
บนกลุ่มที่ไม่มีการแก้ไขนับตั้งแต่บิลด์ล่าสุดการโอนไฟล์แบบเพิ่ม ระบบจะนำทรัพยากร Android, ไฟล์ .dex และไลบรารีแบบเนทีฟออกจาก .apk หลักและจัดเก็บไว้ในไดเรกทอรี mobile-install แยกต่างหาก ช่วยให้อัปเดตโค้ดและ Android ได้ ทรัพยากรแยกต่างหากโดยไม่ต้องติดตั้งทั้งแอปอีกครั้ง ดังนั้น การโอนไฟล์ใช้เวลาน้อยกว่า และมีเพียงไฟล์ .dex ที่มี และได้คอมไพล์ซ้ำในอุปกรณ์
การโหลดส่วนต่างๆ ของแอปจากภายนอก .apk ระบบจะใส่แอปพลิเคชันตัวอย่างขนาดเล็กลงใน .apk ซึ่งจะโหลดทรัพยากร Android, โค้ด Java และโค้ดเนทีฟจากไดเรกทอรีการติดตั้งบนอุปกรณ์เคลื่อนที่ จากนั้นโอนการควบคุมไปยังแอปจริง โดยแอปจะไม่รู้เรื่องนี้เลย ยกเว้นในกรณีที่พบไม่บ่อยนักตามที่อธิบายไว้ด้านล่าง
การแยก Dex
ชาร์ด Dexing นั้นตรงไปตรงมาพอสมควร นั่นคือเมื่อสร้างไฟล์ .jar แล้ว
เครื่องมือ
ชาร์ดออกเป็นไฟล์ .jar แยกที่มีขนาดเท่าๆ กัน แล้วจึงเรียกใช้
dx
สำหรับรายการที่มีการเปลี่ยนแปลงตั้งแต่บิลด์ก่อนหน้า ตรรกะที่
กำหนดว่าชาร์ดใดที่จะใช้ Dex ไม่ได้เจาะจงเฉพาะ Android เพียงแค่ใช้
อัลกอริทึมการตัดการเปลี่ยนแปลงทั่วไปของ Bazel
อัลกอริทึมการจัดสรรส่วนที่แบ่งออกเป็นหลายส่วนเวอร์ชันแรกจะจัดเรียงไฟล์ .class ตามลําดับตัวอักษร จากนั้นจะตัดรายการออกเป็นส่วนๆ ที่มีขนาดเท่าๆ กัน แต่วิธีนี้ไม่ได้ผลลัพธ์ที่ดีที่สุด หากมีการเพิ่มหรือนําคลาสออก (แม้แต่คลาสที่ฝังอยู่หรือคลาสที่ไม่ระบุตัวตน) คลาสทั้งหมดที่อยู่หลังคลาสนั้นๆ จะเลื่อนตามลําดับตัวอักษรไป 1 คลาส ซึ่งส่งผลให้ต้องจัดทําดัชนีกลุ่มเหล่านั้นอีกครั้ง เราจึงตัดสินใจที่จะแบ่งแพ็กเกจ Java แทนการแบ่งคลาสแต่ละคลาส แน่นอนว่าการดำเนินการนี้ยังคงส่งผลให้มีการจัดทำดัชนีหลายกลุ่มหากมีการเพิ่มหรือนำแพ็กเกจใหม่ออก แต่การดำเนินการดังกล่าวเกิดขึ้นน้อยกว่ามากเมื่อเทียบกับการเพิ่มหรือนำคลาสเดียวออก
จำนวนกลุ่มจะควบคุมโดยไฟล์ BUILD (โดยใช้แอตทริบิวต์ android_binary.dex_shards
) ในทางทฤษฎี Bazel จะกำหนดจำนวนกลุ่มย่อยที่เหมาะสมที่สุดโดยอัตโนมัติ แต่ปัจจุบัน Bazel ต้องทราบชุดการดำเนินการ (เช่น คำสั่งที่จะดำเนินการระหว่างการบิลด์) ก่อนดำเนินการใดๆ จึงไม่สามารถกำหนดจำนวนกลุ่มย่อยที่เหมาะสมที่สุดได้เนื่องจากไม่ทราบจำนวนคลาส Java ที่จะอยู่ในแอปในที่สุด โดยทั่วไปแล้ว ยิ่งมีกลุ่มย่อยมากเท่าใด การบิลด์และการติดตั้งก็จะยิ่งเร็วขึ้น แต่การเริ่มต้นแอปก็จะช้าลงด้วยเนื่องจากโปรแกรมลิงก์แบบไดนามิกต้องทำงานมากขึ้น โดยปกติแล้วจำนวนที่เหมาะสมจะอยู่ระหว่าง 10 ถึง 50 ชาร์ด
การโอนไฟล์แบบเพิ่ม
หลังจากสร้างแอปแล้ว ขั้นตอนต่อไปคือการติดตั้ง ความพยายามน้อยที่สุดเท่าที่จะเป็นไปได้ การติดตั้งมีขั้นตอนดังนี้
- การติดตั้ง .apk (โดยปกติจะใช้
adb install
) - การอัปโหลดไฟล์ .dex, ทรัพยากร Android และไลบรารีแบบเนทีฟไปยัง ไดเรกทอรีสำหรับติดตั้งบนอุปกรณ์เคลื่อนที่
ไม่มีส่วนเพิ่มมากนักในขั้นตอนแรก กล่าวคือมีการติดตั้งแอป
หรือไม่ ขณะนี้ Bazel อาศัยผู้ใช้ในการระบุว่าควรทำขั้นตอนนี้หรือไม่
ผ่านตัวเลือกบรรทัดคำสั่ง --incremental
เนื่องจากไม่สามารถระบุ
ทั้งหมดในกรณีที่จำเป็น
ในขั้นตอนที่ 2 จะมีการเปรียบเทียบไฟล์ของแอปจากบิลด์กับในอุปกรณ์ ไฟล์ Manifest ที่แสดงรายการไฟล์ของแอปที่อยู่ในอุปกรณ์และ การตรวจสอบข้อผิดพลาด ระบบจะอัปโหลดไฟล์ใหม่ไปยังอุปกรณ์ อัปเดตไฟล์ที่มีการเปลี่ยนแปลง และลบไฟล์ที่ถูกนำออกออกจากอุปกรณ์ หากไม่มีไฟล์ Manifest ระบบจะถือว่าต้องอัปโหลดทุกไฟล์
โปรดทราบว่าคุณสามารถหลอกอัลกอริทึมการติดตั้งที่เพิ่มขึ้นได้โดย การเปลี่ยนแปลงไฟล์ในอุปกรณ์ แต่ไม่ตรวจสอบข้อผิดพลาดในไฟล์ Manifest การดำเนินการนี้อาจ ป้องกันโดยการคำนวณการตรวจสอบข้อผิดพลาด (Checksum) ของไฟล์ใน แต่ไม่คุ้มกับการเพิ่มเวลาในการติดตั้ง
แอปพลิเคชัน Stub
แอปพลิเคชัน Stub คือที่ที่เวทมนตร์โหลดไฟล์ Dexe, โค้ดแบบเนทีฟ และ
ทรัพยากร Android จากไดเรกทอรี mobile-install
ในอุปกรณ์เกิดขึ้น
การโหลดจริงจะใช้การแยกคลาสย่อยของ BaseDexClassLoader
และเป็นเทคนิคที่มีการบันทึกไว้ค่อนข้างดี การดำเนินการนี้จะเกิดขึ้นก่อนที่ระบบจะโหลดคลาสของแอป เพื่อให้คลาสแอปพลิเคชันที่อยู่ใน apk วางไว้ในไดเรกทอรี mobile-install
ในอุปกรณ์ได้เพื่ออัปเดตคลาสเหล่านั้นได้โดยไม่ต้องใช้ adb install
การดำเนินการนี้ต้องเกิดขึ้นก่อนที่ระบบจะโหลดคลาสใดๆ ของแอป เพื่อที่จะไม่ต้องมีคลาสแอปพลิเคชันในไฟล์ .apk ซึ่งหมายความว่าการเปลี่ยนแปลงคลาสเหล่านั้นจะต้องติดตั้งใหม่ทั้งหมด
ซึ่งทำได้โดยการแทนที่คลาส Application
ที่ระบุใน AndroidManifest.xml
ด้วยแอปพลิเคชันจำลอง การดำเนินการนี้จะควบคุมเมื่อแอปเริ่มต้น และปรับแต่งตัวโหลดคลาสและเครื่องมือจัดการทรัพยากรอย่างเหมาะสมตั้งแต่เนิ่นๆ (ตัวสร้างคอนสตรัคเตอร์) โดยใช้การสะท้อนของ Java ในเฟรมเวิร์ก Android
อีกสิ่งหนึ่งที่แอปพลิเคชัน Stub ทำก็คือการคัดลอกไลบรารีแบบเนทีฟ
โดยการติดตั้งด้วยการติดตั้งบนอุปกรณ์เคลื่อนที่ไปยังตำแหน่งอื่น ซึ่งเป็นสิ่งจำเป็นเนื่องจาก
ตัวลิงก์แบบไดนามิกต้องใช้บิต X
เพื่อตั้งค่าในไฟล์ ซึ่งใช้ไม่ได้
สำหรับตำแหน่งที่ตั้งใดๆ ที่เข้าถึงได้ด้วย adb
ที่ไม่ใช่ระดับรูท
เมื่อทำสิ่งเหล่านี้เสร็จแล้ว แอปพลิเคชัน stub จะสร้างอินสแตนซ์
Application
จริง เปลี่ยนการอ้างอิงตัวเองทั้งหมดเป็นฟังก์ชันจริง
ภายในเฟรมเวิร์กของ Android
ผลลัพธ์
ประสิทธิภาพ
โดยทั่วไปแล้ว bazel mobile-install
ทำให้การสร้างเมืองเร็วขึ้น 4 ถึง 10 เท่า
และติดตั้งแอปขนาดใหญ่
หลังจากมีการเปลี่ยนแปลงเล็กน้อย
ตัวเลขต่อไปนี้คำนวณจากผลิตภัณฑ์ Google บางรายการ
แน่นอนว่าเวลาที่ใช้จะขึ้นอยู่กับลักษณะของการเปลี่ยนแปลง การคอมไพล์อีกครั้งหลังจากการเปลี่ยนแปลงไลบรารีพื้นฐานจะใช้เวลานานกว่า
ข้อจำกัด
เทคนิคที่แอปพลิเคชันจำลองใช้อาจใช้ไม่ได้ในบางกรณี กรณีต่อไปนี้จะไฮไลต์กรณีที่ฟีเจอร์ไม่ทำงานตามที่ควรจะเป็น
เมื่อแคสต์
Context
ไปยังชั้นเรียนApplication
ในContentProvider#onCreate()
ระบบจะเรียกเมธอดนี้ในระหว่างแอปพลิเคชัน เริ่มต้นทำงานก่อนที่เราจะมีโอกาสแทนที่อินสแตนซ์ของApplication
ดังนั้นContentProvider
จะยังคงอ้างอิงแอปพลิเคชัน stub แทนที่จะเป็นรูปจริง คุณสามารถโต้แย้งได้ว่านี่ไม่ใช่ข้อบกพร่องเนื่องจากคุณไม่ควรดาวน์แคสต์Context
เช่นนี้ แต่ดูเหมือนว่าปัญหานี้จะเกิดขึ้นในแอปบางแอปของ Googleทรัพยากรที่ติดตั้งโดย
bazel mobile-install
สามารถเข้าถึงได้จากภายในเท่านั้น แอปนั้น หากแอปอื่นๆ เข้าถึงทรัพยากรผ่านPackageManager#getApplicationResources()
แหล่งข้อมูลเหล่านี้มาจาก ที่ไม่เพิ่มขึ้นครั้งสุดท้ายอุปกรณ์ที่ไม่ได้ใช้ ART แม้ว่าแอปพลิเคชัน Stub จะใช้งานได้ดีใน Froyo และในเวลาต่อมา Dalvik มีข้อบกพร่องที่ทำให้คิดว่าแอป ไม่ถูกต้องหากมีการกระจายโค้ดไปยังไฟล์ .dex หลายไฟล์ใน เช่น เมื่อใช้คำอธิบายประกอบ Java ใน ที่เจาะจง ตราบใดที่แอปของคุณไม่มีปัญหาข้อบกพร่องเหล่านี้ ก็ควรจะทำงานร่วมกับ Dalvik ได้เช่นกัน (อย่างไรก็ตาม โปรดทราบว่าการรองรับ Android เวอร์ชันเก่าไม่ใช่จุดสนใจของเรา)