การเรียกใช้ Android API มักจะมีความล่าช้าและการคำนวณที่สำคัญต่อการเรียกใช้แต่ละครั้ง ดังนั้น การแคชฝั่งไคลเอ็นต์จึงเป็นสิ่งที่ต้องพิจารณาอย่างสำคัญในการออกแบบ API ที่มีประโยชน์ ถูกต้อง และมีประสิทธิภาพ
แรงจูงใจ
API ที่แสดงต่อนักพัฒนาแอปใน Android SDK มักมีการใช้งานเป็นโค้ดไคลเอ็นต์ในเฟรมเวิร์ก Android ที่จะเรียก Binder IPC ไปยังบริการของระบบในกระบวนการของแพลตฟอร์ม ซึ่งมีหน้าที่ดำเนินการคํานวณบางอย่างและแสดงผลลัพธ์ต่อไคลเอ็นต์ โดยปกติแล้วเวลาในการตอบสนองของการดำเนินการนี้จะขึ้นอยู่กับปัจจัย 3 อย่างต่อไปนี้
- ค่าใช้จ่ายเพิ่มเติมของ IPC: การเรียก IPC พื้นฐานมักจะมีความล่าช้ามากกว่าการเรียกเมธอดพื้นฐานในกระบวนการ 10,000 เท่า
- การแย่งชิงทรัพยากรฝั่งเซิร์ฟเวอร์: งานที่ทำในบริการของระบบเพื่อตอบสนองคำขอของไคลเอ็นต์อาจไม่เริ่มทันที เช่น หากเธรดเซิร์ฟเวอร์ไม่ว่างเนื่องจากจัดการกับคำขออื่นๆ ที่เข้ามาก่อนหน้านี้
- การคํานวณฝั่งเซิร์ฟเวอร์: งานที่จัดการคําขอในเซิร์ฟเวอร์ อาจต้องใช้การทํางานอย่างมาก
คุณกำจัดปัจจัยเวลาในการตอบสนองทั้ง 3 ประการนี้ได้โดยใช้แคชฝั่งไคลเอ็นต์ โดยที่แคชต้องมีลักษณะดังนี้
- ถูกต้อง: แคชฝั่งไคลเอ็นต์จะไม่แสดงผลลัพธ์ที่แตกต่างจากที่เซิร์ฟเวอร์จะแสดง
- มีประสิทธิภาพ: คำขอของไคลเอ็นต์มักแสดงจากแคช เช่น แคชมีอัตรา Hit สูง
- มีประสิทธิภาพ: แคชฝั่งไคลเอ็นต์ใช้ทรัพยากรฝั่งไคลเอ็นต์อย่างมีประสิทธิภาพ เช่น แสดงข้อมูลที่แคชไว้อย่างกระชับและไม่จัดเก็บผลการค้นหาที่แคชไว้หรือข้อมูลที่ล้าสมัยในหน่วยความจําของไคลเอ็นต์มากเกินไป
ลองแคชผลลัพธ์ของเซิร์ฟเวอร์ในไคลเอ็นต์
หากลูกค้าส่งคําขอเดียวกันหลายครั้งบ่อยครั้ง และค่าที่แสดงผลไม่เปลี่ยนแปลงเมื่อเวลาผ่านไป คุณควรใช้แคชในคลังไลบรารีไคลเอ็นต์ที่มีคีย์เป็นพารามิเตอร์คําขอ
ลองใช้ 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
พร้อมใช้งานสำหรับโค้ดระบบทั้งหมด รวมถึงโมดูลหลัก
นอกจากนี้ยังมี PropertyInvalidatedCache
ซึ่งแทบจะเหมือนกันทุกประการ แต่เฟรมเวิร์กเท่านั้นที่จะเห็น โปรดใช้ IpcDataCache
หากเป็นไปได้
ทำให้แคชเป็นโมฆะเมื่อมีการเปลี่ยนแปลงฝั่งเซิร์ฟเวอร์
หากค่าที่แสดงผลจากเซิร์ฟเวอร์มีการเปลี่ยนแปลงเมื่อเวลาผ่านไป ให้ใช้ Callback เพื่อสังเกตการเปลี่ยนแปลง และลงทะเบียน Callback เพื่อให้คุณทำให้แคชฝั่งไคลเอ็นต์เป็นโมฆะได้ตามความเหมาะสม
ทำให้แคชระหว่างเฟรมทดสอบโมดูลใช้งานไม่ได้
ในชุดการทดสอบยูนิต คุณอาจทดสอบโค้ดไคลเอ็นต์กับ Test Double แทนเซิร์ฟเวอร์จริง หากใช่ อย่าลืมล้างแคชฝั่งไคลเอ็นต์ระหว่างแต่ละกรณีทดสอบ วิธีนี้ช่วยให้เฟรมเวิร์กการทดสอบแยกกันอยู่และป้องกันไม่ให้เฟรมเวิร์กการทดสอบหนึ่งรบกวนอีกเฟรมเวิร์กหนึ่ง
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
@Before
public void setUp() {
BirthdayManager.clearCache();
}
@After
public void tearDown() {
BirthdayManager.clearCache();
}
...
}
เมื่อเขียนการทดสอบ CTS ที่ทดสอบไคลเอ็นต์ API ที่ใช้การแคชภายใน แคชคือรายละเอียดการใช้งานที่ไม่ได้แสดงต่อผู้เขียน API ดังนั้นการทดสอบ CTS จึงไม่ควรต้องใช้ความรู้พิเศษเกี่ยวกับการแคชที่ใช้ในโค้ดไคลเอ็นต์
ศึกษาการ Hit และ Miss ของแคช
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 โดยจงใจปิดใช้การแคชเนื่องจากมีการเรียกใช้จำนวนมากระหว่างการบูต
- ไม่ได้ตั้งค่า: มีแคชอยู่แต่ยังไม่ได้เริ่มต้นใช้งาน ไม่ได้ตั้งค่า Nonce ซึ่งหมายความว่าแคชไม่เคยถูกทำให้ใช้งานไม่ได้
- ข้าม: การตัดสินใจที่จะข้ามแคช
- จํานวนสูงบ่งบอกถึงการใช้งานแคชที่ไม่มีประสิทธิภาพ
ทำให้ใช้งานไม่ได้:
- คําจํากัดความ: กระบวนการทําเครื่องหมายข้อมูลที่แคชไว้ว่าล้าสมัยหรือไม่เป็นปัจจุบัน
- ความสำคัญ: แสดงสัญญาณว่าระบบทำงานกับข้อมูลล่าสุด ซึ่งช่วยป้องกันข้อผิดพลาดและความไม่สอดคล้อง
- โดยปกติจะทริกเกอร์โดยเซิร์ฟเวอร์ที่เป็นเจ้าของข้อมูล
ขนาดปัจจุบัน:
- คําจํากัดความ: จํานวนองค์ประกอบปัจจุบันในแคช
- ความสำคัญ: บ่งบอกถึงการใช้ทรัพยากรของแคชและผลกระทบที่อาจเกิดขึ้นกับประสิทธิภาพของระบบ
- โดยปกติแล้ว ค่าที่สูงขึ้นหมายความว่าแคชใช้หน่วยความจํามากขึ้น
ขนาดสูงสุด:
- คําจํากัดความ: พื้นที่สูงสุดที่จัดสรรสําหรับแคช
- ความสำคัญ: กําหนดความจุของแคชและความสามารถในการจัดเก็บข้อมูล
- การตั้งค่าขนาดสูงสุดที่เหมาะสมจะช่วยรักษาสมดุลระหว่างประสิทธิภาพของแคชกับการใช้หน่วยความจํา เมื่อถึงขนาดสูงสุด ระบบจะเพิ่มองค์ประกอบใหม่โดยการนำองค์ประกอบที่ไม่ได้ใช้ล่าสุดออก ซึ่งอาจบ่งบอกถึงความไม่มีประสิทธิภาพ
High Water Mark:
- คําจํากัดความ: ขนาดสูงสุดที่แคชมีตั้งแต่สร้าง
- ความสำคัญ: ให้ข้อมูลเชิงลึกเกี่ยวกับการใช้งานแคชสูงสุดและปัญหาการแย่งกันใช้หน่วยความจําที่อาจเกิดขึ้น
- การตรวจสอบจุดสูงสุดที่ทำได้จะช่วยระบุจุดคอขวดหรือส่วนที่ควรเพิ่มประสิทธิภาพได้
การล้น:
- คําจํากัดความ: จํานวนครั้งที่แคชมีขนาดใหญ่เกินขีดจํากัดสูงสุดและต้องย้ายข้อมูลออกเพื่อเพิ่มพื้นที่ให้กับรายการใหม่
- ความสำคัญ: บ่งบอกถึงแรงกดดันของแคชและประสิทธิภาพที่อาจลดลงเนื่องจากการนำข้อมูลออก
- จํานวนการล้นสูงบ่งชี้ว่าอาจต้องปรับขนาดแคชหรือประเมินกลยุทธ์การแคชอีกครั้ง
สถิติเดียวกันนี้ยังดูได้ในรายงานข้อบกพร่อง
ปรับขนาดแคช
แคชมีขนาดใหญ่ได้สูงสุด เมื่อเกินขนาดแคชสูงสุด ระบบจะลบรายการออกตามลําดับ LRU
- การแคชรายการน้อยเกินไปอาจส่งผลเสียต่ออัตรา Hit ของแคช
- การแคชรายการมากเกินไปจะเพิ่มปริมาณการใช้หน่วยความจําของแคช
ค้นหาสมดุลที่เหมาะสมสำหรับกรณีการใช้งานของคุณ
กำจัดการเรียกใช้ไคลเอ็นต์ที่ซ้ำซ้อน
ไคลเอ็นต์อาจส่งคําค้นหาเดียวกันไปยังเซิร์ฟเวอร์หลายครั้งในช่วงเวลาสั้นๆ ดังนี้
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 ครั้งต่อ 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"];
}
}