โดยปกติแล้วการเรียก API ของ Android จะมีเวลาในการตอบสนองและการคำนวณต่อการเรียกใช้ที่สำคัญ ดังนั้นการแคชฝั่งไคลเอ็นต์จึงเป็นสิ่งที่ควรพิจารณาอย่างยิ่งในการออกแบบ API ที่มีประโยชน์ ถูกต้อง และมีประสิทธิภาพ
แรงจูงใจ
API ที่แสดงแก่นักพัฒนาแอปใน Android SDK มักจะใช้เป็นโค้ดฝั่งไคลเอ็นต์ใน Android Framework ซึ่งทำการเรียก IPC ของ Binder ไปยังบริการของระบบในกระบวนการของแพลตฟอร์ม โดยมีหน้าที่ในการคำนวณบางอย่างและส่งคืนผลลัพธ์ไปยังไคลเอ็นต์ โดยปกติแล้วเวลาในการตอบสนองของการดำเนินการนี้จะขึ้นอยู่กับ 3 ปัจจัยต่อไปนี้
- ค่าใช้จ่ายของ IPC: การเรียก IPC พื้นฐานมักมีเวลาในการตอบสนองมากกว่าการเรียกเมธอดพื้นฐานในกระบวนการ 10,000 เท่า
- การแย่งชิงฝั่งเซิร์ฟเวอร์: งานที่ทำในบริการของระบบเพื่อตอบสนองต่อคำขอของไคลเอ็นต์อาจไม่เริ่มทันที เช่น หากเธรดของเซิร์ฟเวอร์กำลังจัดการคำขออื่นๆ ที่มาถึงก่อนหน้านี้
- การคำนวณฝั่งเซิร์ฟเวอร์: งานที่ต้องทำเพื่อจัดการคำขอในเซิร์ฟเวอร์ อาจต้องใช้เวลามาก
คุณสามารถขจัดปัจจัยด้านเวลาในการตอบสนองทั้ง 3 อย่างนี้ได้โดยการใช้แคชในฝั่งไคลเอ็นต์ โดยมีเงื่อนไขว่าแคชต้องมีคุณสมบัติดังนี้
- ถูกต้อง: แคชฝั่งไคลเอ็นต์จะไม่แสดงผลลัพธ์ที่แตกต่าง จากที่เซิร์ฟเวอร์จะแสดง
- มีประสิทธิภาพ: คำขอของไคลเอ็นต์มักจะแสดงจากแคช เช่น แคชมีอัตราการเข้าชมสูง
- มีประสิทธิภาพ: แคชฝั่งไคลเอ็นต์ใช้ทรัพยากรฝั่งไคลเอ็นต์อย่างมีประสิทธิภาพ เช่น โดยการแสดงข้อมูลที่แคชไว้ในรูปแบบที่กะทัดรัด และโดยการไม่ จัดเก็บผลลัพธ์ที่แคชไว้หรือข้อมูลที่ล้าสมัยมากเกินไปในหน่วยความจำของไคลเอ็นต์
พิจารณาแคชผลลัพธ์ของเซิร์ฟเวอร์ในไคลเอ็นต์
หากไคลเอ็นต์มักส่งคำขอเดียวกันซ้ำหลายครั้ง และค่าที่ส่งคืนไม่เปลี่ยนแปลงเมื่อเวลาผ่านไป คุณควรใช้แคชในไลบรารีไคลเอ็นต์ที่คีย์ตามพารามิเตอร์คำขอ
ลองใช้ IpcDataCache ในการติดตั้งใช้งาน
public class BirthdayManager {
private final IpcDataCache.QueryHandler<User, Birthday> mBirthdayQuery =
new IpcDataCache.QueryHandler<User, Birthday>() {
@Override
public Birthday apply(User user) {
return mService.getBirthday(user);
}
};
private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache
private static final String BDAY_API = "getUserBirthday";
private final IpcDataCache<User, Birthday> mCache
new IpcDataCache<User, Birthday>(
BDAY_CACHE_MAX, MODULE_SYSTEM, BDAY_API, BDAY_API, mBirthdayQuery);
/** @hide **/
@VisibleForTesting
public static void clearCache() {
IpcDataCache.invalidateCache(MODULE_SYSTEM, BDAY_API);
}
public Birthday getBirthday(User user) {
return mCache.query(user);
}
}
ดูตัวอย่างทั้งหมดได้ที่ android.app.admin.DevicePolicyManager
IpcDataCache พร้อมใช้งานกับโค้ดระบบทั้งหมด รวมถึงโมดูล Mainline
นอกจากนี้ยังมี PropertyInvalidatedCache ซึ่งแทบจะเหมือนกันทุกประการ แต่จะแสดงต่อเฟรมเวิร์กเท่านั้น ขอแนะนำให้ใช้ IpcDataCache หากเป็นไปได้
ล้างข้อมูลในแคชเมื่อมีการเปลี่ยนแปลงฝั่งเซิร์ฟเวอร์
หากค่าที่แสดงผลจากเซิร์ฟเวอร์อาจเปลี่ยนแปลงไปตามเวลา ให้ใช้ Callback เพื่อสังเกตการเปลี่ยนแปลง และลงทะเบียน Callback เพื่อให้คุณอาจล้างแคชฝั่งไคลเอ็นต์ ตามความเหมาะสม
ล้างแคชระหว่างกรอบการทดสอบหน่วย
ในชุดการทดสอบหน่วย คุณอาจทดสอบโค้ดไคลเอ็นต์กับเทสต์ดับเบิล แทนที่จะเป็นเซิร์ฟเวอร์จริง หากเป็นเช่นนั้น โปรดล้างแคชฝั่งไคลเอ็นต์ ระหว่างกรณีทดสอบ เพื่อรักษากรณีทดสอบให้แยกต่างหากจากกันและป้องกันไม่ให้กรณีทดสอบหนึ่ง รบกวนอีกกรณีหนึ่ง
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
@Before
public void setUp() {
BirthdayManager.clearCache();
}
@After
public void tearDown() {
BirthdayManager.clearCache();
}
...
}
เมื่อเขียนการทดสอบ CTS ที่ใช้ไคลเอ็นต์ API ที่ใช้แคชภายใน แคชคือรายละเอียดการใช้งานที่ไม่ได้เปิดเผยต่อผู้เขียน API ดังนั้นการทดสอบ CTS จึงไม่ควรต้องมีความรู้พิเศษเกี่ยวกับการแคชที่ใช้ใน โค้ดไคลเอ็นต์
ศึกษาการเข้าชมและการไม่เข้าชมแคช
IpcDataCache และ PropertyInvalidatedCache สามารถพิมพ์สถิติแบบเรียลไทม์ได้
adb shell dumpsys cacheinfo
...
Cache Name: cache_key.is_compat_change_enabled
Property: cache_key.is_compat_change_enabled
Hits: 1301458, Misses: 21387, Skips: 0, Clears: 39
Skip-corked: 0, Skip-unset: 0, Skip-bypass: 0, Skip-other: 0
Nonce: 0x856e911694198091, Invalidates: 72, CorkedInvalidates: 0
Current Size: 1254, Max Size: 2048, HW Mark: 2049, Overflows: 310
Enabled: true
...
ช่อง
Hit:
- คำจำกัดความ: จำนวนครั้งที่พบข้อมูลที่ขอ ในแคชสำเร็จ
- ความสำคัญ: บ่งบอกถึงการเรียกข้อมูลที่รวดเร็วและมีประสิทธิภาพ ซึ่งช่วยลด การเรียกข้อมูลที่ไม่จำเป็น
- โดยทั่วไปแล้ว ยิ่งมีจำนวนมากยิ่งดี
การเคลียร์:
- คำจำกัดความ: จำนวนครั้งที่มีการล้างแคชเนื่องจาก การลบล้าง
- เหตุผลในการล้างข้อมูล
- การลบล้าง: ข้อมูลที่ล้าสมัยจากเซิร์ฟเวอร์
- การจัดการพื้นที่: เพิ่มพื้นที่สำหรับข้อมูลใหม่เมื่อแคชเต็ม
- จำนวนที่สูงอาจบ่งบอกถึงข้อมูลที่มีการเปลี่ยนแปลงบ่อยและอาจไม่มีประสิทธิภาพ
ไม่ตรงกัน:
- คำจำกัดความ: จำนวนครั้งที่แคชไม่สามารถให้ข้อมูลที่ขอได้
- สาเหตุ
- การแคชที่ไม่มีประสิทธิภาพ: แคชมีขนาดเล็กเกินไปหรือไม่ได้จัดเก็บข้อมูลที่ถูกต้อง
- ข้อมูลที่มีการเปลี่ยนแปลงบ่อย
- คำขอครั้งแรก
- จำนวนที่สูงบ่งบอกถึงปัญหาการแคชที่อาจเกิดขึ้น
ข้าม:
- คำจำกัดความ: กรณีที่ไม่ได้ใช้แคชเลย แม้ว่าแคชจะใช้ได้ก็ตาม
- เหตุผลในการข้าม
- การปิดกั้น: เฉพาะการอัปเดต Android Package Manager โดยตั้งใจ ปิดการแคชเนื่องจากมีการเรียกใช้จำนวนมากในระหว่างการบูต
- ไม่ได้ตั้งค่า: มีแคชแต่ยังไม่ได้เริ่มต้น ไม่ได้ตั้งค่า Nonce ซึ่งหมายความว่าแคชไม่เคยถูกทำให้ใช้ไม่ได้
- ข้าม: การตัดสินใจโดยเจตนาที่จะข้ามแคช
- จำนวนที่สูงแสดงให้เห็นถึงความไม่มีประสิทธิภาพที่อาจเกิดขึ้นในการใช้แคช
ใช้ไม่ได้แล้ว:
- คำจำกัดความ: กระบวนการทำเครื่องหมายข้อมูลที่แคชว่าล้าสมัยหรือเก่า
- ความสำคัญ: ให้สัญญาณว่าระบบทำงานกับข้อมูลล่าสุด ซึ่งจะช่วยป้องกันข้อผิดพลาดและความไม่สอดคล้องกัน
- โดยปกติแล้วจะทริกเกอร์โดยเซิร์ฟเวอร์ที่เป็นเจ้าของข้อมูล
ขนาดปัจจุบัน:
- คำจำกัดความ: จำนวนองค์ประกอบปัจจุบันในแคช
- ความสำคัญ: ระบุการใช้ทรัพยากรของแคชและผลกระทบที่อาจเกิดขึ้นต่อประสิทธิภาพของระบบ
- โดยทั่วไปแล้ว ค่าที่สูงขึ้นหมายความว่าแคชจะใช้หน่วยความจำมากขึ้น
ขนาดสูงสุด:
- คำจำกัดความ: พื้นที่สูงสุดที่จัดสรรไว้สำหรับแคช
- ความสำคัญ: กำหนดความจุของแคชและความสามารถในการจัดเก็บข้อมูล
- การตั้งค่าขนาดสูงสุดที่เหมาะสมจะช่วยรักษาสมดุลระหว่างประสิทธิภาพของแคชกับการใช้หน่วยความจำ เมื่อถึงขนาดสูงสุด ระบบจะเพิ่มองค์ประกอบใหม่โดยการนำองค์ประกอบที่ใช้ล่าสุดออก ซึ่งอาจบ่งบอกถึงความไม่มีประสิทธิภาพ
จุดสูงสุด:
- คำจำกัดความ: ขนาดสูงสุดที่แคชมีตั้งแต่สร้าง
- ความสำคัญ: ให้ข้อมูลเชิงลึกเกี่ยวกับการใช้แคชสูงสุดและแรงกดดันด้านหน่วยความจำที่อาจเกิดขึ้น
- การตรวจสอบจุดสูงสุดจะช่วยระบุจุดคอขวดที่อาจเกิดขึ้นหรือ ส่วนที่ควรเพิ่มประสิทธิภาพ
การล้น:
- คำจำกัดความ: จำนวนครั้งที่แคชมีขนาดเกินขนาดสูงสุดและต้อง นำข้อมูลออกเพื่อให้มีพื้นที่สำหรับรายการใหม่
- ความสำคัญ: บ่งบอกถึงแรงกดดันของแคชและประสิทธิภาพที่อาจลดลง เนื่องจากการนำข้อมูลออก
- จำนวนการล้นสูงแสดงว่าอาจต้องปรับขนาดแคชหรือประเมินกลยุทธ์การแคชใหม่
คุณยังดูสถิติเดียวกันนี้ได้ในรายงานข้อบกพร่องด้วย
ปรับขนาดแคช
แคชมีขนาดสูงสุด เมื่อเกินขนาดแคชสูงสุด ระบบจะ นำรายการออกตามลำดับ LRU
- การแคชรายการน้อยเกินไปอาจส่งผลเสียต่ออัตราการเข้าชมแคช
- การแคชรายการมากเกินไปจะเพิ่มการใช้หน่วยความจำของแคช
ค้นหาสมดุลที่เหมาะสมสำหรับกรณีการใช้งานของคุณ
ลดการเรียกไคลเอ็นต์ที่ซ้ำซ้อน
ไคลเอ็นต์อาจส่งคำค้นหาเดียวกันไปยังเซิร์ฟเวอร์หลายครั้งในช่วงเวลาสั้นๆ
public void executeAll(List<Operation> operations) throws SecurityException {
for (Operation op : operations) {
for (Permission permission : op.requiredPermissions()) {
if (!permissionChecker.checkPermission(permission, ...)) {
throw new SecurityException("Missing permission " + permission);
}
}
op.execute();
}
}
พิจารณานำผลลัพธ์จากการเรียกก่อนหน้ามาใช้ซ้ำ
public void executeAll(List<Operation> operations) throws SecurityException {
Set<Permission> permissionsChecked = new HashSet<>();
for (Operation op : operations) {
for (Permission permission : op.requiredPermissions()) {
if (!permissionsChecked.add(permission)) {
if (!permissionChecker.checkPermission(permission, ...)) {
throw new SecurityException(
"Missing permission " + permission);
}
}
}
op.execute();
}
}
พิจารณาการบันทึกการตอบกลับล่าสุดของเซิร์ฟเวอร์ฝั่งไคลเอ็นต์
แอปไคลเอ็นต์อาจส่งคำค้นหาไปยัง API ในอัตราที่เร็วกว่าที่เซิร์ฟเวอร์ของ API จะสร้างการตอบกลับใหม่ที่มีความหมายได้ ในกรณีนี้ วิธีที่มีประสิทธิภาพคือการบันทึกการตอบสนองของเซิร์ฟเวอร์ที่เห็นล่าสุดไว้ที่ฝั่งไคลเอ็นต์พร้อมกับการประทับเวลา และส่งคืนผลลัพธ์ที่บันทึกไว้โดยไม่ต้องค้นหาเซิร์ฟเวอร์หากผลลัพธ์ที่บันทึกไว้เป็นข้อมูลล่าสุด ผู้เขียนไคลเอ็นต์ API สามารถกำหนดระยะเวลาการจดจำได้
ตัวอย่างเช่น แอปอาจแสดงสถิติการเข้าชมเครือข่ายต่อผู้ใช้โดย การค้นหาสถิติในทุกเฟรมที่วาด
@UiThread
private void setStats() {
mobileRxBytesTextView.setText(
Long.toString(TrafficStats.getMobileRxBytes()));
mobileRxPacketsTextView.setText(
Long.toString(TrafficStats.getMobileRxPackages()));
mobileTxBytesTextView.setText(
Long.toString(TrafficStats.getMobileTxBytes()));
mobileTxPacketsTextView.setText(
Long.toString(TrafficStats.getMobileTxPackages()));
}
แอปอาจวาดเฟรมที่ 60 Hz แต่ในทางทฤษฎีแล้ว โค้ดไคลเอ็นต์ใน
TrafficStatsอาจเลือกที่จะค้นหาสถิติจากเซิร์ฟเวอร์อย่างมาก 1 ครั้งต่อวินาที
และหากค้นหาภายใน 1 วินาทีของการค้นหาก่อนหน้า ก็จะแสดงค่าที่เห็นล่าสุด
ซึ่งทำได้เนื่องจากเอกสารประกอบ API ไม่ได้ระบุสัญญาใดๆ
เกี่ยวกับการอัปเดตผลลัพธ์ที่แสดง
participant App code as app
participant Client library as clib
participant Server as server
app->clib: request @ T=100ms
clib->server: request
server->clib: response 1
clib->app: response 1
app->clib: request @ T=200ms
clib->app: response 1
app->clib: request @ T=300ms
clib->app: response 1
app->clib: request @ T=2000ms
clib->server: request
server->clib: response 2
clib->app: response 2
พิจารณาใช้ Codegen ฝั่งไคลเอ็นต์แทนการค้นหาฝั่งเซิร์ฟเวอร์
หากเซิร์ฟเวอร์ทราบผลการค้นหาในเวลาที่สร้าง ให้พิจารณาว่าไคลเอ็นต์ทราบผลการค้นหาในเวลาที่สร้างด้วยหรือไม่ และพิจารณาว่าสามารถใช้ API ในฝั่งไคลเอ็นต์ทั้งหมดได้หรือไม่
พิจารณารหัสแอปต่อไปนี้ที่ตรวจสอบว่าอุปกรณ์เป็นนาฬิกาหรือไม่ (กล่าวคือ อุปกรณ์ใช้ Wear OS)
public boolean isWatch(Context ctx) {
PackageManager pm = ctx.getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
พร็อพเพอร์ตี้นี้ของอุปกรณ์จะทราบในเวลาที่สร้าง โดยเฉพาะเวลาที่สร้างเฟรมเวิร์กสำหรับรูปภาพการบูตของอุปกรณ์นี้ โค้ดฝั่งไคลเอ็นต์
สำหรับ hasSystemFeature สามารถแสดงผลลัพธ์ที่ทราบได้ทันที แทนที่จะ
ค้นหาบริการระบบ PackageManager จากระยะไกล
ขจัดข้อมูลที่ซ้ำกันในการเรียกกลับของเซิร์ฟเวอร์ในไคลเอ็นต์
สุดท้ายนี้ ไคลเอ็นต์ API อาจลงทะเบียนการเรียกกลับกับเซิร์ฟเวอร์ API เพื่อรับการแจ้งเตือน ของเหตุการณ์
โดยปกติแล้ว แอปจะลงทะเบียนการเรียกกลับหลายรายการสำหรับข้อมูลพื้นฐานเดียวกัน ไลบรารีของไคลเอ็นต์ควรมีการเรียกกลับที่ลงทะเบียนไว้ 1 รายการโดยใช้ IPC กับเซิร์ฟเวอร์ จากนั้นจึงแจ้งการเรียกกลับที่ลงทะเบียนแต่ละรายการในแอป แทนที่จะให้เซิร์ฟเวอร์แจ้งไคลเอ็นต์ 1 ครั้งต่อการเรียกกลับที่ลงทะเบียนโดยใช้ IPC
digraph d_front_back {
rankdir=RL;
node [style=filled, shape="rectangle", fontcolor="white" fontname="Roboto"]
server->clib
clib->c1;
clib->c2;
clib->c3;
subgraph cluster_client {
graph [style="dashed", label="Client app process"];
c1 [label="my.app.FirstCallback" color="#4285F4"];
c2 [label="my.app.SecondCallback" color="#4285F4"];
c3 [label="my.app.ThirdCallback" color="#4285F4"];
clib [label="android.app.FooManager" color="#F4B400"];
}
subgraph cluster_server {
graph [style="dashed", label="Server process"];
server [label="com.android.server.FooManagerService" color="#0F9D58"];
}
}