إرشادات التخزين المؤقت من جهة العميل لواجهات برمجة تطبيقات Android

تتطلّب عادةً طلبات بيانات واجهة برمجة تطبيقات Android وقت استجابة وعمليات حسابية كبيرة لكل عملية استدعاء. لذلك، فإنّ التخزين المؤقت من جهة العميل هو عامل مهم في تصميم واجهات برمجة التطبيقات المفيدة والصحيحة والفعّالة.

الحافز

غالبًا ما يتم تنفيذ واجهات برمجة التطبيقات المتاحة لمطوّري التطبيقات في حزمة تطوير البرامج (SDK) لنظام التشغيل Android على هيئة رمز العميل في إطار عمل Android الذي يُجري طلبًا بين العمليات باستخدام Binder للخدمة النظام في عملية النظام، والتي تتمثل وظيفتها في تنفيذ بعض العمليات الحسابية وعرض النتيجة للعميل. عادةً ما يُحدِّد ثلاثة عوامل وقت استجابة هذه العملية:

  • النفقات العامة لبروتوكول 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
  ...

الحقول

النتائج:

  • التعريف: عدد المرات التي تم فيها بنجاح العثور على جزء من البيانات المطلوبة في ذاكرة التخزين المؤقت.
  • الأهمية: يشير ذلك إلى استرداد البيانات بكفاءة وسرعة، ما يقلل من استرداد البيانات غير الضرورية.
  • وكلما ارتفعت هذه الأعداد، كان ذلك أفضل بشكل عام.

عمليات المحو:

  • التعريف: عدد المرات التي تم فيها محو ذاكرة التخزين المؤقت بسبب إبطالها.
  • أسباب محو البيانات:
    • الإبطال: بيانات قديمة من الخادم
    • إدارة المساحة: توفير مساحة للبيانات الجديدة عندما تكون ذاكرة التخزين المؤقت ممتلئة
  • قد تشير الأعداد العالية إلى تغيُّر البيانات بشكل متكرّر وإلى عدم كفاءة محتمل.

الأخطاء:

  • التعريف: عدد المرّات التي تعذّر فيها على ذاكرة التخزين المؤقت تقديم data المطلوبة.
  • الأسباب:
    • التخزين المؤقت غير الفعّال: ذاكرة التخزين المؤقت صغيرة جدًا أو لا تخزِّن البيانات الصحيحة.
    • البيانات التي تتغيّر بشكل متكرّر
    • الطلبات لأول مرة
  • تشير الأعداد العالية إلى مشاكل محتملة في التخزين المؤقت.

عمليات التخطّي:

  • التعريف: الحالات التي لم يتم فيها استخدام ذاكرة التخزين المؤقت على الإطلاق، على الرغم من أنّه كان من الممكن استخدامها
  • أسباب التخطّي:
    • إيقاف التخزين المؤقت: خاص بتعديلات "مدير حِزم Android"، ويعني ذلك عمدًا إيقاف التخزين المؤقت بسبب العدد الكبير من طلبات البيانات أثناء عملية التمهيد.
    • غير محدّد: تتوفّر ذاكرة التخزين المؤقت ولكن لم يتم إعدادها. لم يتم ضبط مفتاح ال 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();
  }
}

استخدام ميزة "تذكير" من جهة العميل لاستجابات الخادم الأخيرة

قد تطلب تطبيقات العميل بيانات من واجهة برمجة التطبيقات بمعدّل أسرع من معدّل إنتاج خادم واجهة برمجة التطبيقات لرسائل ردّ جديدة مفيدة. في هذه الحالة، من الأساليب الفعّالة الاحتفاظ بآخر استجابة من الخادم في ذاكرة التخزين المؤقت على جانب العميل مع الطابع الزمني، وعرض النتيجة المحفوظة في الذاكرة المؤقتة بدون طلب البيانات من الخادم إذا كانت النتيجة المحفوظة في الذاكرة المؤقتة حديثة بما يكفي. يمكن لمؤلف عميل واجهة برمجة التطبيقات تحديد مدة التخزين المؤقت.

على سبيل المثال، قد يعرض أحد التطبيقات إحصاءات عدد زيارات الشبكة للمستخدم من خلال البحث عن الإحصاءات في كل إطار مرسوم:

@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 البعيدة.

إزالة تكرار عمليات استدعاء الخادم في العميل

أخيرًا، يمكن لعميل واجهة برمجة التطبيقات تسجيل عمليات الاستدعاء مع خادم واجهة برمجة التطبيقات ليتم إعلامه بالأحداث.

من الشائع أن تسجِّل التطبيقات طلبات استدعاء متعددة للمعلومات الأساسية نفسها. بدلاً من أن يُعلم الخادم العميل مرة واحدة لكل معالجة تسجيل ناتجة عن أسلوب معيّن باستخدام 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"];
  }
}