دستورالعمل های حافظه پنهان سمت سرویس گیرنده Android API

تماس‌های Android API معمولاً شامل تأخیر و محاسبه قابل توجهی در هر فراخوانی می‌شوند. بنابراین، حافظه پنهان سمت کلاینت در طراحی APIهای مفید، صحیح و کارآمد، یک نکته مهم است.

انگیزه

APIهایی که در معرض توسعه‌دهندگان برنامه در Android SDK قرار می‌گیرند، اغلب به‌عنوان کد کلاینت در Android Framework پیاده‌سازی می‌شوند که یک تماس IPC Binder را با یک سرویس سیستم در یک فرآیند پلتفرم برقرار می‌کند، که وظیفه آن انجام برخی محاسبات و بازگرداندن نتیجه به مشتری است. تأخیر این عملیات معمولاً تحت تأثیر سه عامل است:

  • سربار IPC: یک تماس اولیه IPC معمولاً 10000 برابر تأخیر یک فراخوانی روش اصلی در فرآیند است.
  • اختلاف سمت سرور: کاری که در سرویس سیستم در پاسخ به درخواست مشتری انجام می شود ممکن است بلافاصله شروع نشود، به عنوان مثال اگر یک موضوع سرور مشغول رسیدگی به درخواست های دیگر باشد که زودتر وارد شده اند.
  • محاسبات سمت سرور: خود کار برای رسیدگی به درخواست در سرور ممکن است به کار قابل توجهی نیاز داشته باشد.

شما می توانید هر سه عامل تأخیر را با اجرای یک کش در سمت کلاینت حذف کنید، مشروط بر اینکه حافظه پنهان:

  • درست: کش سمت سرویس گیرنده هرگز نتایجی را برمی گرداند که با آنچه سرور برمی گرداند متفاوت باشد.
  • موثر: درخواست‌های مشتری اغلب از حافظه پنهان ارائه می‌شوند، به‌عنوان مثال کش نرخ ضربه بالایی دارد.
  • کارآمد: کش سمت سرویس گیرنده از منابع سمت سرویس گیرنده استفاده موثری می کند، مانند نمایش داده های کش شده به روشی فشرده و با ذخیره نکردن بسیاری از نتایج کش شده یا داده های قدیمی در حافظه مشتری.

کش کردن نتایج سرور در مشتری را در نظر بگیرید

اگر کلاینت‌ها اغلب یک درخواست را چندین بار انجام می‌دهند، و مقدار بازگشتی در طول زمان تغییر نمی‌کند، باید یک حافظه پنهان در کتابخانه مشتری که توسط پارامترهای درخواست کلید می‌خورد، اجرا کنید.

استفاده از 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 ثبت کنید تا بر این اساس حافظه پنهان سمت سرویس گیرنده را باطل کنید.

حافظه پنهان بین موارد تست واحد را باطل کنید

در مجموعه تست واحد، ممکن است کد کلاینت را در مقابل یک تست دوبل تست کنید تا سرور واقعی. اگر چنین است، پس حتماً حافظه پنهان سمت سرویس گیرنده را بین موارد آزمایشی پاک کنید. این برای این است که کیس‌های آزمایشی متقابلاً هرمتیک باشند و از تداخل یک مورد آزمایشی با دیگری جلوگیری شود.

@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
  ...

فیلدها

بازدیدها:

  • تعریف: تعداد دفعاتی که یک قطعه داده درخواستی با موفقیت در حافظه پنهان پیدا شد.
  • اهمیت: بازیابی کارآمد و سریع داده ها را نشان می دهد که بازیابی غیر ضروری داده ها را کاهش می دهد.
  • شمارش بالاتر عموما بهتر است.

پاک می کند:

  • تعریف: تعداد دفعاتی که حافظه پنهان به دلیل نامعتبر بودن پاک شد.
  • دلایل پاکسازی:
    • Invalidation: داده های قدیمی از سرور.
    • مدیریت فضا: ایجاد فضا برای داده های جدید زمانی که حافظه پنهان پر است.
  • شمارش بالا می تواند نشان دهنده تغییر مکرر داده ها و ناکارآمدی بالقوه باشد.

خانم ها:

  • تعریف: تعداد دفعاتی که کش نتوانست داده های درخواستی را ارائه کند.
  • علل:
    • کش ناکارآمد: حافظه نهان خیلی کوچک است یا داده های مناسب ذخیره نمی شود.
    • داده‌ها در حال تغییر مکرر
    • درخواست های اولین بار
  • تعداد بالا نشان دهنده مشکلات احتمالی حافظه پنهان است.

پرش:

  • تعریف: مواردی که از حافظه نهان اصلاً استفاده نشده است، حتی اگر می توانسته باشد.
  • دلایل پرش:
    • Corking: مخصوص به‌روزرسانی‌های Android Package Manager، به دلیل حجم بالای تماس‌ها در حین راه‌اندازی، حافظه پنهان عمداً خاموش می‌شود.
    • Unset: حافظه پنهان وجود دارد اما اولیه نشده است. 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 هرتز بکشد. اما به طور فرضی، کد مشتری در TrafficStats ممکن است انتخاب کند که سرور را برای آمار حداکثر یک بار در ثانیه پرس و جو کند، و اگر در یک ثانیه از یک پرس و جو قبلی پرس و جو شود، آخرین مقدار مشاهده شده را برگرداند. این مجاز است زیرا اسناد 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

به جای پرس و جوهای سرور، کدژن سمت کلاینت را در نظر بگیرید

اگر نتایج پرس و جو در زمان ساخت برای سرور قابل دانستن است، پس در نظر بگیرید که آیا آنها در زمان ساخت برای کلاینت نیز قابل اطلاع هستند یا خیر، و در نظر بگیرید که آیا API می تواند به طور کامل در سمت کلاینت پیاده سازی شود یا خیر.

کد برنامه زیر را در نظر بگیرید که بررسی می کند آیا دستگاه ساعت است (یعنی دستگاه دارای سیستم عامل Wear OS است):

public boolean isWatch(Context ctx) {
    PackageManager pm = ctx.getPackageManager();
    return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}

این ویژگی دستگاه در زمان ساخت مشخص است، به ویژه در زمانی که Framework برای تصویر بوت این دستگاه ساخته شد. کد سمت کلاینت برای hasSystemFeature می‌تواند فوراً یک نتیجه شناخته شده را به جای پرس و جو از سرویس سیستم PackageManager از راه دور بازگرداند.

کال بک سرور را در کلاینت کپی کنید

در نهایت، سرویس گیرنده API ممکن است تماس‌های برگشتی را با سرور 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"];
  }
}