การใช้ API ที่ใหม่กว่า

หน้านี้อธิบายวิธีที่แอปของคุณสามารถใช้ฟังก์ชันการทำงานใหม่ของระบบปฏิบัติการเมื่อเรียกใช้ในอุปกรณ์ใหม่ เวอร์ชันของระบบปฏิบัติการโดยยังคงความเข้ากันได้กับอุปกรณ์รุ่นเก่ากว่า

โดยค่าเริ่มต้น การอ้างอิงไปยัง NDK API ในแอปพลิเคชันของคุณถือเป็นการอ้างอิงที่ชัดเจน ตัวโหลดแบบไดนามิกของ Android จะพยายามแก้ปัญหาเหล่านี้เมื่อไลบรารีของคุณ โหลดแล้ว หากไม่พบสัญลักษณ์ดังกล่าว แอปจะล้มเลิก ซึ่งตรงข้ามกับ ลักษณะการทำงานของ Java ซึ่งจะไม่มีการส่งข้อยกเว้นจนกว่า API จะตกหล่น โทรออก

ด้วยเหตุนี้ NDK จึงจะป้องกันคุณจากการสร้างการอ้างอิงที่แข็งแกร่งไปยัง API ที่ใหม่กว่า minSdkVersion ของแอป วิธีนี้จะช่วยปกป้องคุณจาก รหัสการจัดส่งโดยไม่ตั้งใจซึ่งทำงานในระหว่างการทดสอบแต่โหลดไม่สำเร็จ (UnsatisfiedLinkError จะโยนจาก System.loadLibrary()) ในเก่ากว่า อุปกรณ์ ในทางกลับกัน การเขียนโค้ดที่ใช้ API จะทำได้ยากกว่า ใหม่กว่าminSdkVersionของแอปคุณ เนื่องจากคุณต้องเรียกใช้ API โดยใช้ dlopen() และ dlsym() แทนที่จะเป็นการเรียกใช้ฟังก์ชันปกติ

อีกทางเลือกหนึ่งแทนที่จะใช้การอ้างอิงที่มีประสิทธิภาพคือการใช้การอ้างอิงที่ไม่รัดกุม แอปที่ไม่รัดกุม ไม่พบการอ้างอิงเมื่อไลบรารีโหลดผลลัพธ์ในที่อยู่ของ สัญลักษณ์ดังกล่าวเป็น nullptr แทนที่จะโหลดไม่สำเร็จ รูปภาพ ไม่สามารถเรียกอย่างปลอดภัยได้ ตราบเท่าที่มีการป้องกัน Callsites เพื่อป้องกันการโทรติดต่อ API เมื่อไม่พร้อมใช้งาน โค้ดที่เหลือจะทำงานได้ และคุณจะ เรียกใช้ API ตามปกติโดยไม่ต้องใช้ dlopen() และ dlsym()

การอ้างอิง API ที่ไม่รัดกุมไม่ต้องการการสนับสนุนเพิ่มเติมจากตัวลิงก์แบบไดนามิก เพื่อให้ใช้กับ Android เวอร์ชันใดก็ได้

การเปิดใช้การอ้างอิง API ที่ไม่ปลอดภัยในบิลด์ของคุณ

ผู้ผลิต

ผ่าน -DANDROID_WEAK_API_DEFS=ON เมื่อใช้ CMake หากคุณใช้ CMake ผ่าน externalNativeBuild ให้เพิ่มรายการต่อไปนี้ลงใน build.gradle.kts (หรือ เทียบเท่ากับการใช้ภาษา build.gradle:

android {
    // Other config...

    defaultConfig {
        // Other config...

        externalNativeBuild {
            cmake {
                arguments.add("-DANDROID_WEAK_API_DEFS=ON")
                // Other config...
            }
        }
    }
}

Ndk-Build

เพิ่มโค้ดต่อไปนี้ลงในไฟล์ Application.mk

APP_WEAK_API_DEFS := true

หากคุณยังไม่มีไฟล์ Application.mk ให้สร้างไฟล์เดียวกัน เป็นไฟล์ Android.mk การเปลี่ยนแปลงเพิ่มเติมในบัญชี ไฟล์ build.gradle.kts (หรือ build.gradle) ไม่จำเป็นสำหรับ ndk-build

ระบบบิลด์อื่นๆ

หากคุณไม่ได้ใช้ CMake หรือ ndk-build ให้ดูเอกสารสำหรับบิลด์ของคุณ เพื่อดูว่ามีวิธีที่แนะนำในการเปิดใช้ฟีเจอร์นี้หรือไม่ หากบิลด์ของคุณ จะไม่รองรับตัวเลือกนี้โดยค่าเริ่มต้น คุณสามารถเปิดใช้ฟีเจอร์ได้โดย การส่งแฟล็กต่อไปนี้เมื่อคอมไพล์

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

อย่างแรกจะกำหนดค่าส่วนหัว NDK เพื่อให้มีการอ้างอิงที่ไม่รัดกุม เลี้ยวที่สอง คำเตือนสำหรับการเรียก API ที่ไม่ปลอดภัยไปยังข้อผิดพลาด

ดูข้อมูลเพิ่มเติมได้ที่คู่มือสร้างโปรแกรมดูแลรักษาระบบ

การเรียก API ที่มีการป้องกัน

ฟีเจอร์นี้ไม่ได้เรียกใช้ API ใหม่อย่างปลอดภัยได้อย่างไร้ที่ติ สิ่งเดียวก็คือ ชะลอข้อผิดพลาดเกี่ยวกับเวลาโหลดให้เป็นข้อผิดพลาดเวลาการโทร ประโยชน์ที่คุณจะได้รับ สามารถป้องกันการเรียกใช้ดังกล่าวขณะรันไทม์และย้อนกลับได้อย่างสวยงาม ไม่ว่าจะโดยใช้ การใช้งานแบบทางเลือก หรือการแจ้งเตือนผู้ใช้ว่าฟีเจอร์นั้นของแอป ไม่พร้อมใช้งานในอุปกรณ์ หรือหลีกเลี่ยงเส้นทางของโค้ดนั้นโดยสิ้นเชิง

Clang จะปล่อยคำเตือน (unguarded-availability) เมื่อคุณทำท่าเพิกเฉย ไปยัง API ที่ใช้กับ minSdkVersion ของแอปไม่ได้ หากคุณ โดยใช้ ndk-build หรือไฟล์เครื่องมือ CMake ของเรา คำเตือนนั้นจะ เปิดใช้และเลื่อนระดับให้เป็นข้อผิดพลาดเมื่อเปิดใช้ฟีเจอร์นี้

ต่อไปนี้คือตัวอย่างของโค้ดที่ใช้ API อย่างมีเงื่อนไขโดยไม่มีเงื่อนไข ฟีเจอร์นี้เปิดใช้อยู่ โดยใช้ dlopen() และ dlsym():

void LogImageDecoderResult(int result) {
    void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
    CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
    auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
        dlsym(lib, "AImageDecoder_resultToString")
    );
    if (func == nullptr) {
        LOG(INFO) << "cannot stringify result: " << result;
    } else {
        LOG(INFO) << func(result);
    }
}

ทำให้อ่านยาก มีชื่อฟังก์ชันซ้ำกัน (และถ้า คุณต้องเขียน C ด้วย ลายเซ็นนี้ก็จะสร้างได้สำเร็จ แต่เสมอ ใช้ตัวเลือกสำรองขณะรันไทม์หากคุณพิมพ์ชื่อฟังก์ชันที่ส่งผิดโดยไม่ตั้งใจ เป็น dlsym และคุณจะต้องใช้รูปแบบนี้สำหรับ API ทุกรายการ

หากมีการอ้างอิง API ที่ไม่รัดกุม ฟังก์ชันข้างต้นสามารถเขียนใหม่เป็น

void LogImageDecoderResult(int result) {
    if (__builtin_available(android 31, *)) {
        LOG(INFO) << AImageDecoder_resultToString(result);
    } else {
        LOG(INFO) << "cannot stringify result: " << result;
    }
}

เบื้องหลัง __builtin_available(android 31, *) การโทร android_get_device_api_level() แคชผลลัพธ์และเปรียบเทียบกับ 31 (ซึ่งเป็นระดับ API ที่แนะนำ AImageDecoder_resultToString())

วิธีที่ง่ายที่สุดในการกำหนดค่าที่จะใช้สำหรับ __builtin_available คือ พยายามสร้างโดยไม่มียาม (หรือยามของ __builtin_available(android 1, *)) แล้วทำตามที่ข้อความแสดงข้อผิดพลาดแจ้งให้ทราบ ตัวอย่างเช่น การเรียกไปยัง AImageDecoder_createFromAAsset() ที่ไม่ได้ป้องกันด้วย minSdkVersion 24 จะสร้าง:

error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]

ในกรณีนี้ การโทรควรได้รับการปกป้องโดย __builtin_available(android 30, *) หากไม่มีข้อผิดพลาดของรุ่นแล้ว API จะพร้อมใช้งานสำหรับ minSdkVersion และไม่จำเป็นต้องมีการป้องกัน หรือบิลด์ของคุณมีการกำหนดค่าไม่ถูกต้องและ คำเตือน unguarded-availability ปิดใช้อยู่

หรืออีกทางเลือกหนึ่งคือ การอ้างอิง NDK API จะแสดงข้อความบางอย่างตลอดบรรทัดของ "เปิดตัวใน API 30" สำหรับ API แต่ละรายการ หากไม่มีข้อความดังกล่าว แสดงว่า API นี้จะใช้ได้กับ API ระดับที่รองรับทั้งหมด

การหลีกเลี่ยงการใช้การป้องกัน API ซ้ำ

หากคุณใช้โค้ดนี้ คุณอาจจะมีส่วนของโค้ดในแอปที่ ใช้ได้กับอุปกรณ์ใหม่ที่เพียงพอเท่านั้น แทนที่จะแสดง __builtin_available() ตรวจสอบแต่ละฟังก์ชัน คุณจะสามารถใส่คำอธิบายประกอบ ของตัวเองเพราะต้องใช้ API ระดับหนึ่ง ตัวอย่างเช่น ImageDecoder API มีการเพิ่มตัวเองใน API 30 ดังนั้นสำหรับฟังก์ชันที่ใช้ฟังก์ชันเหล่านั้นเป็นจำนวนมาก API ที่คุณสามารถทำได้ เช่น

#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)

void DecodeImageWithImageDecoder() REQUIRES_API(30) {
    // Call any APIs that were introduced in API 30 or newer without guards.
}

void DecodeImageFallback() {
    // Pay the overhead to call the Java APIs via JNI, or use third-party image
    // decoding libraries.
}

void DecodeImage() {
    if (API_AT_LEAST(30)) {
        DecodeImageWithImageDecoder();
    } else {
        DecodeImageFallback();
    }
}

ลักษณะพิเศษของการป้องกัน API

Clang มีความเฉพาะเจาะจงอย่างมากเกี่ยวกับวิธีใช้ __builtin_available มีลิเทอรัลเท่านั้น (แต่อาจแทนที่มาโครได้) if (__builtin_available(...)) ทำงานได้ เท่ากัน การดำเนินการเล็กๆ น้อยๆ เช่น if (!__builtin_available(...)) จะไม่ทำงาน (Clang จะแสดงคำเตือน unsupported-availability-guard และ unguarded-availability) ซึ่งอาจช่วยปรับปรุงใน Clang เวอร์ชันในอนาคต โปรดดู LLVM ปัญหา 33161 สำหรับข้อมูลเพิ่มเติม

การตรวจสอบสำหรับ unguarded-availability จะมีผลกับขอบเขตฟังก์ชันที่ Clang จะแสดงคำเตือนแม้ว่าฟังก์ชันที่มีการเรียก API จะเป็น ที่เคยโทรจากภายในขอบเขตที่มีการป้องกันเท่านั้น เพื่อหลีกเลี่ยงไม่ให้มียามซ้ำกันใน ดูการหลีกเลี่ยงการป้องกัน API ซ้ำ

เหตุใดค่านี้จึงไม่ใช่ค่าเริ่มต้น

หากไม่ใช้อย่างถูกต้อง ความแตกต่างระหว่างการอ้างอิง API ที่เข้มงวดกับ API ที่ไม่รัดกุม การอ้างอิงว่ารูปแบบแรกจะล้มเหลวอย่างรวดเร็วและเห็นได้ชัด ขณะที่ การดำเนินการหลังจะไม่ล้มเหลวจนกว่าผู้ใช้จะดำเนินการที่ทำให้ API หายไป ให้โทรหาได้ เมื่อเกิดกรณีนี้ ข้อความแสดงข้อผิดพลาดจะไม่ชัดเจน เวลาคอมไพล์ "AFoo_bar() ไม่พร้อมใช้งาน" แสดงว่าเป็นความผิดพลาด ด้วย ข้อความแสดงข้อผิดพลาดที่ชัดเจนขึ้นมาก และความล้มเหลวอย่างรวดเร็ว ค่าเริ่มต้นที่ปลอดภัยกว่า

เนื่องจากเป็นฟีเจอร์ใหม่ โค้ดที่มีอยู่จึงถูกเขียนเพื่อจัดการกับจำนวนน้อยมาก ทำงานนี้ได้อย่างปลอดภัย โค้ดของบุคคลที่สามที่ไม่ได้เขียนขึ้นโดยคำนึงถึง Android ก็น่าจะเจอปัญหานี้อยู่ดี จึงยังไม่มีแผนสำหรับ ที่เป็นค่าเริ่มต้นให้ไม่มีการเปลี่ยนแปลง

เราควรแนะนำให้คุณใช้ฟีเจอร์นี้ แต่เนื่องจากจะทำให้เกิดปัญหามากขึ้น ที่จะตรวจหาและแก้ไขข้อบกพร่องได้ยาก คุณควรยอมรับความเสี่ยงเหล่านั้นอย่างจงใจ มากกว่าที่พฤติกรรมเปลี่ยนไปโดยที่คุณไม่รู้ตัว

ข้อควรระวัง

ฟีเจอร์นี้ใช้งานได้กับ API ส่วนใหญ่ แต่มีบางกรณีที่ไม่สามารถ งาน

รายการที่มีแนวโน้มว่าจะมีปัญหาน้อยที่สุดคือ libc API ที่ใหม่กว่า แตกต่างจากวิดีโอที่เหลือ Android API ที่ป้องกันด้วย #if __ANDROID_API__ >= X ในส่วนหัว ไม่ใช่แค่ __INTRODUCED_IN(X) เท่านั้น ซึ่งช่วยป้องกันแม้กระทั่งการประกาศที่ไม่รัดกุมจาก ให้ผู้อื่นเห็น เนื่องจากการสนับสนุน NDK สมัยใหม่ระดับ API ที่เก่าที่สุดคือ r21 API ของ libc ที่จำเป็นต้องใช้โดยทั่วไปมีอยู่แล้ว เพิ่ม libc API ใหม่แต่ละรายการ Release (ดู status.md) แต่ยิ่งมีเวอร์ชันที่ใหม่กว่า ก็ยิ่งมีโอกาส ถือเป็นกรณีพิเศษที่นักพัฒนาซอฟต์แวร์จำนวนน้อยจะต้องใช้ อย่างไรก็ตาม หากคุณเป็นหนึ่งใน นักพัฒนาซอฟต์แวร์เหล่านั้น ตอนนี้คุณต้องใช้ dlsym() เพื่อเรียกนักพัฒนาแอปต่อไป API หาก minSdkVersion ของคุณเก่ากว่า API นี่คือปัญหาที่แก้ไขได้ แต่การดำเนินการดังกล่าวมีความเสี่ยงที่จะทำลายความเข้ากันได้ของแหล่งที่มาสำหรับทุกแอป ( โค้ดที่มี polyfills ของ libc API จะไม่สามารถคอมไพล์ได้เนื่องจาก แอตทริบิวต์ availability ไม่ตรงกันในการประกาศในท้องถิ่นและ libc) เราไม่แน่ใจว่าจะแก้ไขปัญหาได้ไหมหรือเมื่อใด

ส่วนกรณีที่นักพัฒนาซอฟต์แวร์มีแนวโน้มที่จะพบมากขึ้นก็คือเมื่อไลบรารีที่ มี API ใหม่ที่ใหม่กว่า minSdkVersion ของคุณ ฟีเจอร์นี้เท่านั้น เปิดใช้งานการอ้างอิงสัญลักษณ์ที่ไม่รัดกุม ไม่มีสิ่งที่เป็นไลบรารีที่ไม่รัดกุม ข้อมูลอ้างอิง ตัวอย่างเช่น หาก minSdkVersion ของคุณคือ 24 คุณสามารถลิงก์ libvulkan.so และโทรหา vkBindBufferMemory2 ที่มีการป้องกันเนื่องจาก libvulkan.so พร้อมใช้งานในอุปกรณ์ที่ขึ้นต้นด้วย API 24 ในทางกลับกัน หาก minSdkVersion มีอายุ 23 ปี คุณจะต้องกลับไปใช้ dlopen และ dlsym เนื่องจากไลบรารีจะไม่มีอยู่ในอุปกรณ์ที่สนับสนุนเฉพาะ API 23 เรายังไม่ทราบวิธีแก้ปัญหาที่ดีนักเพื่อแก้ไขปัญหานี้ แต่ คำนี้จะหายไปเอง เนื่องจากเรา (เมื่อเป็นไปได้) และไม่อนุญาต API สำหรับสร้างไลบรารีใหม่

สำหรับผู้เขียนห้องสมุด

หากคุณกำลังพัฒนาไลบรารีเพื่อใช้ในแอปพลิเคชัน Android คุณควร ให้หลีกเลี่ยงการใช้ฟีเจอร์นี้ในส่วนหัวสาธารณะ สามารถใช้อย่างปลอดภัยใน แบบไม่อยู่ในบรรทัด แต่หากคุณใช้ __builtin_available ในโค้ดใดก็ตามใน เช่น ฟังก์ชันในบรรทัด หรือคำจำกัดความของเทมเพลต คุณจะบังคับให้ เพื่อเปิดใช้ฟีเจอร์นี้ ด้วยเหตุผลเดียวกัน เราจึงไม่ได้เปิดใช้ โดยค่าเริ่มต้นใน NDK คุณควรหลีกเลี่ยงการเลือกตัวเลือกนั้น ของผู้บริโภคได้อย่างไร

ถ้าคุณต้องการให้มีลักษณะการทำงานนี้ในส่วนหัวสาธารณะ โปรด เพื่อให้ผู้ใช้ของคุณทราบว่าจะต้องเปิดใช้ฟีเจอร์นี้และ ตระหนักถึงความเสี่ยงของการดำเนินการดังกล่าว