การพัฒนาแบบวนซ้ำอย่างรวดเร็วสำหรับ 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จะเริ่มแอปโดยอัตโนมัติ
หากไม่แน่ใจ ให้ดูตัวอย่าง หรือ ติดต่อเรา
บทนำ
หนึ่งในแอตทริบิวต์ที่สำคัญที่สุดของ Toolchain ของนักพัฒนาซอฟต์แวร์คือความเร็ว ซึ่งมีความแตกต่างกันอย่างมากระหว่างการเปลี่ยนโค้ดแล้วเห็นโค้ดทำงานภายใน 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 แอปพลิเคชัน Stub จะควบคุมเมื่อมีการเริ่มแอป และปรับแต่งตัวโหลดคลาสและตัวจัดการทรัพยากรอย่างเหมาะสมในเวลาที่เร็วที่สุด (คอนสตรักเตอร์) โดยใช้การสะท้อน 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 เวอร์ชันเก่าไม่ใช่เป้าหมายหลักของเรา)