عادةً ما تتضمّن طلبات البيانات من واجهة برمجة التطبيقات على Android وقت استجابة كبيرًا وعمليات حسابية لكل استدعاء. لذلك، يُعدّ التخزين المؤقت من جهة العميل من الاعتبارات المهمة عند تصميم واجهات برمجة التطبيقات التي تكون مفيدة وصحيحة وفعّالة.
الحافز
غالبًا ما يتم تنفيذ واجهات برمجة التطبيقات المتاحة لمطوّري التطبيقات في حزمة تطوير البرامج (SDK) لنظام التشغيل Android على شكل رمز برمجي للعميل في إطار عمل Android، ويتم إجراء طلب Binder IPC إلى خدمة نظام في عملية النظام، وتتمثل مهمة هذه الخدمة في إجراء بعض العمليات الحسابية وإرجاع نتيجة إلى العميل. تتأثر مدة استجابة هذه العملية عادةً بثلاثة عوامل:
- تكلفة IPC: تبلغ تكلفة مكالمة IPC الأساسية عادةً 10,000 ضعف وقت استجابة مكالمة طريقة أساسية داخل العملية.
 - التنازع من جهة الخادم: قد لا تبدأ العمليات التي تنفّذها خدمة النظام استجابةً لطلب العميل على الفور، مثلاً إذا كان أحد سلاسل الخادم مشغولاً بمعالجة طلبات أخرى وصلت في وقت سابق.
 - الحوسبة من جهة الخادم: قد يتطلّب العمل نفسه لمعالجة الطلب على الخادم جهدًا كبيرًا.
 
يمكنك التخلص من جميع عوامل وقت الاستجابة الثلاثة هذه من خلال تنفيذ ذاكرة تخزين مؤقت على جهة العميل، بشرط أن تكون ذاكرة التخزين المؤقت:
- صحيح: لا تعرض ذاكرة التخزين المؤقت من جهة العميل مطلقًا نتائج مختلفة عن تلك التي كان سيعرضها الخادم.
 - الفعالية: غالبًا ما يتم عرض طلبات العميل من ذاكرة التخزين المؤقت، على سبيل المثال، يكون معدل الوصول إلى ذاكرة التخزين المؤقت مرتفعًا.
 - فعّالة: تستخدم ذاكرة التخزين المؤقت من جهة العميل موارد من جهة العميل بشكل فعّال، مثلاً من خلال عرض البيانات المخزّنة مؤقتًا بطريقة مضغوطة وعدم تخزين الكثير من النتائج المخزّنة مؤقتًا أو البيانات القديمة في ذاكرة العميل.
 
النظر في تخزين نتائج الخادم مؤقتًا في العميل
إذا كان العملاء يكرّرون الطلب نفسه عدة مرات، ولم تتغير القيمة المعروضة بمرور الوقت، عليك تنفيذ ذاكرة تخزين مؤقت في مكتبة العميل مفهرسة حسب مَعلمات الطلب.
ننصحك باستخدام 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 عند الإمكان.
إبطال صلاحية ذاكرات التخزين المؤقت عند إجراء تغييرات من جهة الخادم
إذا كانت القيمة التي يعرضها الخادم قابلة للتغيير بمرور الوقت، عليك تنفيذ دالة ردّ نداء لمراقبة التغييرات، وتسجيل دالة ردّ نداء حتى تتمكّن من إبطال ذاكرة التخزين المؤقت من جهة العميل وفقًا لذلك.
إبطال صحة ذاكرات التخزين المؤقت بين حالات اختبار الوحدات
في مجموعة اختبارات الوحدات، يمكنك اختبار رمز العميل باستخدام بديل للاختبار بدلاً من الخادم الفعلي. إذا كان الأمر كذلك، احرص على محو أي ذاكرات تخزين مؤقت من جهة العميل بين حالات الاختبار. ويتم ذلك للحفاظ على إحكام إغلاق حالات الاختبار بشكل متبادل، ومنع إحدى حالات الاختبار من التدخّل في حالة أخرى.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
    @Before
    public void setUp() {
        BirthdayManager.clearCache();
    }
    @After
    public void tearDown() {
        BirthdayManager.clearCache();
    }
    ...
}
عند كتابة اختبارات CTS التي تستخدم برنامجًا لعميل واجهة برمجة التطبيقات يستعين بالتخزين المؤقت داخليًا، يكون التخزين المؤقت تفصيلاً تنفيذيًا لا يتم عرضه لمؤلف واجهة برمجة التطبيقات، وبالتالي لا تتطلّب اختبارات 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
  ...
الحقول
عدد النتائج المطابقة:
- التعريف: عدد المرات التي تم فيها العثور بنجاح على جزء من البيانات المطلوبة في ذاكرة التخزين المؤقت.
 - الأهمية: يشير إلى استرجاع البيانات بكفاءة وسرعة، ما يقلّل من استرجاع البيانات غير الضرورية.
 - كلما كان العدد أعلى، كان ذلك أفضل بشكل عام.
 
المسارات التي تم محوها:
- التعريف: عدد مرّات محو ذاكرة التخزين المؤقت بسبب إبطالها.
 - أسباب المحو:
- إبطال الصلاحية: بيانات قديمة من الخادم
 - إدارة المساحة: توفير مساحة لبيانات جديدة عندما تكون ذاكرة التخزين المؤقت ممتلئة
 
 - قد تشير الأعداد الكبيرة إلى أنّ البيانات تتغيّر بشكل متكرّر، ما قد يؤدي إلى عدم الكفاءة.
 
الكلمات غير المطابقة:
- التعريف: عدد المرّات التي تعذّر فيها على ذاكرة التخزين المؤقت توفير البيانات المطلوبة.
 - الأسباب:
- التخزين المؤقت غير الفعّال: ذاكرة التخزين المؤقت صغيرة جدًا أو لا تخزِّن البيانات الصحيحة.
 - البيانات التي تتغيّر بشكل متكرّر
 - الطلبات المقدَّمة للمرة الأولى
 
 - تشير الأعداد الكبيرة إلى مشاكل محتمَلة في التخزين المؤقت.
 
عمليات التخطّي:
- التعريف: الحالات التي لم يتم فيها استخدام ذاكرة التخزين المؤقت على الإطلاق، على الرغم من إمكانية استخدامها.
 - أسباب التخطّي:
- Corking: خاص بتحديثات "مدير حِزم Android"، ويتم فيه إيقاف التخزين المؤقت عمدًا بسبب العدد الكبير من عمليات الاستدعاء أثناء عملية التشغيل.
 - Unset: يعني أنّ ذاكرة التخزين المؤقت متوفّرة ولكن لم يتم إعدادها. لم يتم ضبط الرقم العشوائي، ما يعني أنّه لم يتم إبطال ذاكرة التخزين المؤقت مطلقًا.
 - التجاوز: قرار متعمّد بتخطّي ذاكرة التخزين المؤقت.
 
 - تشير الأعداد الكبيرة إلى أوجه قصور محتملة في استخدام ذاكرة التخزين المؤقت.
 
يبطل:
- التعريف: هي عملية وضع علامة على البيانات المخزّنة مؤقتًا للإشارة إلى أنّها قديمة أو غير صالحة.
 - الأهمية: يقدّم إشارة إلى أنّ النظام يعمل باستخدام أحدث البيانات، ما يمنع حدوث أخطاء وتناقضات.
 - يتم عادةً تفعيلها من خلال الخادم الذي يملك البيانات.
 
الحجم الحالي:
- التعريف: هو مقدار العناصر الحالي في ذاكرة التخزين المؤقت.
 - الأهمية: تشير إلى استخدام الموارد في ذاكرة التخزين المؤقت والتأثير المحتمل في أداء النظام.
 - تشير القيم الأعلى بشكل عام إلى أنّ ذاكرة التخزين المؤقت تستخدم المزيد من الذاكرة.
 
الحد الأقصى للحجم:
- التعريف: الحدّ الأقصى لمساحة التخزين المخصّصة لذاكرة التخزين المؤقت.
 - الأهمية: تحدّد سعة ذاكرة التخزين المؤقت وقدرتها على تخزين البيانات.
 - يساعد ضبط الحد الأقصى للحجم المناسب في تحقيق التوازن بين فعالية ذاكرة التخزين المؤقت واستخدام الذاكرة. وبعد الوصول إلى الحد الأقصى للحجم، تتم إضافة عنصر جديد عن طريق إزالة العنصر الأقدم استخدامًا، ما قد يشير إلى عدم الكفاءة.
 
الحدّ الأقصى:
- التعريف: الحد الأقصى لحجم ذاكرة التخزين المؤقت منذ إنشائها.
 - الأهمية: تقدّم هذه السمة إحصاءات حول الحد الأقصى لاستخدام ذاكرة التخزين المؤقت والضغط المحتمل على الذاكرة.
 - يمكن أن يساعد تتبُّع الحدّ الأقصى في تحديد المشاكل المحتملة أو المجالات التي يمكن تحسينها.
 
الفيضانات:
- التعريف: عدد المرات التي تجاوز فيها حجم ذاكرة التخزين المؤقت الحد الأقصى واضطر إلى إزالة البيانات لإفساح المجال لإدخالات جديدة.
 - الأهمية: تشير إلى ضغط ذاكرة التخزين المؤقت وانخفاض الأداء المحتمل بسبب إزالة البيانات.
 - تشير أعداد الفائض المرتفعة إلى أنّه قد يلزم تعديل حجم ذاكرة التخزين المؤقت أو إعادة تقييم استراتيجية التخزين المؤقت.
 
يمكن أيضًا العثور على الإحصاءات نفسها في تقرير الأخطاء.
تعديل حجم ذاكرة التخزين المؤقت
تتضمّن ذاكرات التخزين المؤقت حدًا أقصى للحجم. عند تجاوز الحد الأقصى لحجم ذاكرة التخزين المؤقت، يتم إخلاء الإدخالات بترتيب 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();
  }
}
استخدام التخزين المؤقت من جهة العميل لعمليات البحث الأخيرة على الخادم
قد تستعلم تطبيقات العميل عن واجهة برمجة التطبيقات بمعدل أسرع من المعدل الذي يمكن لخادم واجهة برمجة التطبيقات تقديم ردود جديدة ذات مغزى به. في هذه الحالة، يكون الأسلوب الفعّال هو تخزين آخر ردّ من الخادم تم رصده على جانب العميل مع طابع زمني، وعرض النتيجة المخزّنة بدون طلب البيانات من الخادم إذا كانت النتيجة المخزّنة حديثة بما يكفي. يمكن لمؤلف برنامج واجهة برمجة التطبيقات تحديد مدة التخزين المؤقت.
على سبيل المثال، قد يعرض تطبيق إحصاءات حول عدد الزيارات على الشبكة للمستخدم من خلال طلب الإحصاءات في كل إطار يتم رسمه:
@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 هرتز، ولكن من المفترض أنّ رمز العميل في
TrafficStats قد يختار طلب إحصاءات من الخادم مرة واحدة في الثانية على الأكثر،
وإذا تم طلبها في غضون ثانية من طلب سابق، سيعرض القيمة التي تم رصدها آخر مرة.
هذا مسموح به لأنّ مستندات واجهة برمجة التطبيقات لا تتضمّن أي عقد بشأن مدى حداثة النتائج المعروضة.
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
استخدام إنشاء الرموز من جهة العميل بدلاً من طلبات البحث من الخادم
إذا كانت نتائج الطلب معروفة للخادم في وقت الإنشاء، عليك تحديد ما إذا كانت معروفة للعميل أيضًا في وقت الإنشاء، وما إذا كان يمكن تنفيذ واجهة برمجة التطبيقات بالكامل من جهة العميل.
ضَع في اعتبارك رمز التطبيق التالي الذي يتحقّق مما إذا كان الجهاز ساعة (أي أنّ الجهاز يعمل بنظام التشغيل Wear OS):
public boolean isWatch(Context ctx) {
    PackageManager pm = ctx.getPackageManager();
    return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
تكون هذه السمة للجهاز معروفة في وقت الإنشاء، وتحديدًا في الوقت الذي تم فيه إنشاء إطار العمل لصورة التشغيل الخاصة بهذا الجهاز. يمكن أن يعرض الرمز البرمجي من جهة العميل الخاص بـ hasSystemFeature نتيجة معروفة على الفور، بدلاً من الاستعلام عن خدمة نظام PackageManager البعيدة.
إزالة تكرار عمليات معاودة الاتصال من الخادم في العميل
أخيرًا، يمكن لبرنامج API Client تسجيل عمليات ردّ الاتصال مع خادم API ليتم إعلامه بالأحداث.
من الشائع أن تسجّل التطبيقات عدة عمليات ردّ لطلب المعلومات الأساسية نفسها. بدلاً من أن يرسل الخادم إشعارًا إلى العميل مرة واحدة لكل دالة ردّ مسجّلة باستخدام IPC، يجب أن تتضمّن مكتبة العميل دالة ردّ مسجّلة واحدة باستخدام 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"];
  }
}