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

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

الحافز

غالبًا ما يتم تنفيذ واجهات برمجة التطبيقات المعروضة على مطوّري التطبيقات في Android SDK كرمز من جهة العميل في Android Framework، ما يؤدي إلى إجراء استدعاء 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
  ...

الحقول

النتائج التي تم العثور عليها:

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

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

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

النتائج التي لم يتم العثور عليها:

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

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

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

عمليات الإبطال:

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

الحجم الحالي:

  • التعريف: عدد العناصر الحالية في ذاكرة التخزين المؤقت
  • الأهمية: يشير ذلك إلى استخدام ذاكرة التخزين المؤقت للموارد وتأثيرها المحتمل في أداء النظام.
  • تعني القيم الأعلى بشكل عام أنّ ذاكرة التخزين المؤقت تستخدم مساحة أكبر من الذاكرة.

الحد الأقصى للحجم:

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

الحد الأعلى:

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

عمليات تجاوز الحد الأقصى:

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

يمكن أيضًا العثور على الإحصاءات نفسها في تقرير عن الخطأ.

تعديل حجم ذاكرة التخزين المؤقت

تتضمّن ذاكرات التخزين المؤقت حدًا أقصى للحجم. عند تجاوز الحد الأقصى لحجم ذاكرة التخزين المؤقت، تتم إزالة الإدخالات بترتيب الأقل استخدامًا مؤخرًا.

  • قد يؤثر تخزين عدد قليل جدًا من الإدخالات مؤقتًا سلبًا في معدّل نتائج ذاكرة التخزين المؤقت.
  • يؤدي تخزين عدد كبير جدًا من الإدخالات مؤقتًا إلى زيادة استخدام ذاكرة التخزين المؤقت.

عليك إيجاد التوازن المناسب لحالة الاستخدام.

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

قد يرسل العملاء الاستعلام نفسه إلى الخادم عدة مرات في فترة قصيرة:

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"];
  }
}