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