אופטימיזציה ברקע

תהליכים ברקע יכולים להיות עתירי זיכרון וסוללה. לדוגמה, שידור משתמע עשוי להפעיל הרבה תהליכי רקע שנרשמו להאזין לו, גם אם התהליכים האלה לא מבצעים הרבה עבודה. הדבר יכול להשפיע באופן משמעותי על ביצועי המכשיר ועל חוויית המשתמש.

כדי לצמצם את הבעיה הזו, בגרסה 7.0 של Android (רמת API 24) חלות ההגבלות הבאות:

  • אפליקציות שמטרגטות את Android מגרסה 7.0 (רמת API‏ 24) ואילך לא מקבלות שידורי CONNECTIVITY_ACTION אם הן מצהירות על מקלט השידור שלהן במניפסט. אפליקציות עדיין יקבלו שידורים של CONNECTIVITY_ACTION אם הן ירשמו את BroadcastReceiver שלהן אצל Context.registerReceiver() וההקשר עדיין בתוקף.
  • אפליקציות לא יכולות לשלוח או לקבל שידורים של ACTION_NEW_PICTURE או ACTION_NEW_VIDEO. האופטימיזציה הזו משפיעה על כל האפליקציות, ולא רק על אלה שמטרגטות ל-Android 7.0 (רמת API ‏24).

אם האפליקציה שלכם משתמשת באחת מהכוונות האלה, עליכם להסיר את התלות בהן בהקדם האפשרי כדי שתוכלו לטרגט כראוי מכשירים עם Android מגרסה 7.0 ואילך. מסגרת Android מספקת כמה פתרונות כדי לצמצם את הצורך בשידורים מרומזים כאלה. לדוגמה, JobScheduler ו-WorkManager החדש מספקים מנגנונים חזקים לתזמון פעולות ברשת כשמתקיימים תנאים מסוימים, כמו חיבור לרשת ללא חיוב לפי שימוש. עכשיו אפשר גם להשתמש ב-JobScheduler כדי להגיב לשינויים בספקי התוכן. אובייקטים של JobInfo מכילים את הפרמטרים שבהם JobScheduler משתמש כדי לתזמן את המשימה. כשהתנאים של המשימה מתקיימים, המערכת מבצעת אותה ב-JobService של האפליקציה.

בדף הזה נסביר איך להשתמש בשיטות חלופיות, כמו JobScheduler, כדי להתאים את האפליקציה להגבלות החדשות האלה.

הגבלות שמשתמשים יוזמים

בדף שימוש בסוללה בהגדרות המערכת, המשתמש יכול לבחור באחת מהאפשרויות הבאות:

  • ללא הגבלה: מתן הרשאה לכל הפעולות ברקע, שעשויות לצרוך יותר סוללה.
  • אופטימיזציה (ברירת מחדל): אופטימיזציה ליכולת של אפליקציה לבצע פעולות ברקע, בהתאם לאינטראקציה של המשתמש עם האפליקציה.
  • מוגבל: מונעת לחלוטין את ההפעלה של האפליקציה ברקע. יכול להיות שהאפליקציות לא יפעלו כצפוי.

אם האפליקציה מציגה חלק מההתנהגויות הלא תקינות שמתוארות במופעים של Android, המערכת עשויה לבקש מהמשתמש להגביל את הגישה של האפליקציה למשאבי המערכת.

אם המערכת מזהה שאפליקציה צורכת משאבים מיותרים, היא תודיע למשתמש ותאפשר לו להגביל את הפעולות של האפליקציה. התנהגויות שעשויות להפעיל את ההודעה כוללות:

  • חסימות מצב שינה מוגזמות: חסימה מצב שינה חלקית אחת שנמשכת שעה כשהמסך כבוי
  • יותר מדי שירותי רקע: אם האפליקציה מטרגטת רמות API נמוכות מ-26 ויש לה כמות גדולה מדי של שירותי רקע

ההגבלות המדויקות שהוטלו נקבעות על ידי יצרן המכשיר. לדוגמה, בגרסאות build של AOSP עם Android מגרסה 9 (רמת API 28) ואילך, על אפליקציות שפועלות ברקע ובמצב 'מוגבל' חלות המגבלות הבאות:

  • לא ניתן להפעיל שירותים שפועלים בחזית
  • שירותים קיימים שפועלים בחזית יוסרו מהחזית
  • ההתראות לא מופעלות
  • משימות לא מתבצעות

בנוסף, אם אפליקציה מטרגטת ל-Android 13 (רמת API 33) ואילך והיא נמצאת במצב 'מוגבל', המערכת לא מעבירה את השידור BOOT_COMPLETED או את השידור LOCKED_BOOT_COMPLETED עד שהאפליקציה מופעלת מסיבות אחרות.

ההגבלות הספציפיות מפורטות ב הגבלות על ניהול צריכת החשמל.

הגבלות על קבלת שידורים של פעילות ברשת

אפליקציות שמטרגטות ל-Android 7.0 (רמת API 24) לא מקבלות שידורים של CONNECTIVITY_ACTION אם הן נרשמות לקבל אותם במניפסט, ותהליכים שתלויים בשידור הזה לא יתחילו. המצב הזה עלול להוות בעיה לאפליקציות שרוצות להקשיב לשינויים ברשת או לבצע פעילויות מרובות ברשת כשהמכשיר מחובר לרשת ללא חיוב לפי שימוש. כבר קיימים ב-Android כמה פתרונות לעקיפת ההגבלה הזו, אבל חשוב לבחור את הפתרון המתאים בהתאם למה שרוצים שהאפליקציה תבצע.

הערה: מכשיר BroadcastReceiver שמשויך ל-Context.registerReceiver() ממשיך לקבל את השידור הזה בזמן שהאפליקציה פועלת.

תזמון משימות רשת בחיבורים ללא מדידה

כשמשתמשים בכיתה JobInfo.Builder כדי ליצור את האובייקט JobInfo, מחילים את השיטה setRequiredNetworkType() ומעבירים את JobInfo.NETWORK_TYPE_UNMETERED כפרמטר של המשימה. בקוד לדוגמה הבא מתוזמן שירות להפעלה כשהמכשיר מחובר לרשת ללא חיוב ומחובר לחשמל לטעינה:

Kotlin

const val MY_BACKGROUND_JOB = 0
...
fun scheduleJob(context: Context) {
    val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
    val job = JobInfo.Builder(
            MY_BACKGROUND_JOB,
            ComponentName(context, MyJobService::class.java)
    )
            .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
            .setRequiresCharging(true)
            .build()
    jobScheduler.schedule(job)
}

Java

public static final int MY_BACKGROUND_JOB = 0;
...
public static void scheduleJob(Context context) {
  JobScheduler js =
      (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
  JobInfo job = new JobInfo.Builder(
    MY_BACKGROUND_JOB,
    new ComponentName(context, MyJobService.class))
      .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
      .setRequiresCharging(true)
      .build();
  js.schedule(job);
}

כשהתנאים של המשימה מתקיימים, האפליקציה מקבלת קריאה חוזרת (callback) להרצת השיטה onStartJob() ב-JobService.class שצוין. כדי לראות דוגמאות נוספות של הטמעת JobScheduler, אפשר להיכנס לאפליקציה לדוגמה של JobScheduler.

חלופה חדשה ל-JobScheduler היא WorkManager, ממשק API שמאפשר לתזמן משימות רקע שצריך להבטיח את השלמתן, גם אם תהליך האפליקציה לא פועל. ‏WorkManager בוחר את הדרך המתאימה להפעלת העבודה (באופן ישיר בשרשור בתהליך האפליקציה, וגם באמצעות JobScheduler, ‏ FirebaseJobDispatcher או AlarmManager) על סמך גורמים כמו רמת ה-API של המכשיר. בנוסף, לא צריך את Play Services כדי להשתמש ב-WorkManager, והוא מספק כמה תכונות מתקדמות, כמו קישור משימות או בדיקת סטטוס של משימה. למידע נוסף, ראו WorkManager.

מעקב אחר החיבור לרשת בזמן שהאפליקציה פועלת

אפליקציות שפועלות עדיין יכולות להאזין ל-CONNECTIVITY_CHANGE באמצעות BroadcastReceiver רשום. עם זאת, ב-ConnectivityManager API יש שיטה חזקה יותר לבקשה של קריאה חוזרת רק כשמתקיימים תנאי הרשת שצוינו.

אובייקטים מסוג NetworkRequest מגדירים את הפרמטרים של הקריאה החוזרת (callback) מהרשת במונחים של NetworkCapabilities. יוצרים אובייקטים מסוג NetworkRequest באמצעות הכיתה NetworkRequest.Builder. לאחר מכן, registerNetworkCallback() מעביר את האובייקט NetworkRequest למערכת. כשהתנאים ברשת מתקיימים, האפליקציה מקבלת קריאה חוזרת (callback) כדי להריץ את השיטה onAvailable() שהוגדרה בכיתה ConnectivityManager.NetworkCallback שלה.

האפליקציה ממשיכה לקבל קריאות חוזרות עד שהיא יוצאת או קוראת ל-unregisterNetworkCallback().

הגבלות על קבלת שידורים של תמונות וסרטונים

ב-Android 7.0 (רמת API 24), אפליקציות לא יכולות לשלוח או לקבל שידורים של ACTION_NEW_PICTURE או ACTION_NEW_VIDEO. ההגבלה הזו מצמצמת את הביצועים ואת ההשפעות על חוויית המשתמש במקרים שבהם מספר אפליקציות צריכות לצאת ממצב שינה כדי לעבד תמונה או סרטון חדשים. ב-Android 7.0 (רמת API ‏24) יש הרחבה של JobInfo ו-JobParameters כדי לספק פתרון חלופי.

הפעלת משימות כשיש שינויים ב-URI של תוכן

כדי להפעיל משימות בעקבות שינויים ב-URI של תוכן, Android 7.0 (רמת API 24) מרחיב את ה-API JobInfo באמצעות השיטות הבאות:

JobInfo.TriggerContentUri()
עטיפת הפרמטרים הנדרשים להפעלת משימה במקרה של שינויים במזהי URI של תוכן.
JobInfo.Builder.addTriggerContentUri()
מעביר אובייקט TriggerContentUri אל JobInfo. ContentObserver עוקב אחר ה-URI של התוכן המקודד. אם יש כמה אובייקטים מסוג TriggerContentUri שמשויכים למשימה, המערכת מספקת קריאה חוזרת (callback) גם אם היא מדווחת על שינוי רק באחד ממזהי ה-URI של התוכן.
מוסיפים את הדגל TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS כדי להפעיל את המשימה אם יש שינוי באחד מהצאצאים של ה-URI הנתון. הדגל הזה תואם לפרמטר notifyForDescendants שמוענק ל-registerContentObserver().

הערה: לא ניתן להשתמש ב-TriggerContentUri() בשילוב עם setPeriodic() או עם setPersisted(). כדי לעקוב באופן שוטף אחרי שינויים בתוכן, צריך לתזמן JobInfo חדש לפני ש-JobService של האפליקציה מסיים לטפל בקריאה החוזרת האחרונה.

הקוד לדוגמה הבא מתזמן משימה שתופעל כשהמערכת תדווח על שינוי ב-URI של התוכן, MEDIA_URI:

Kotlin

const val MY_BACKGROUND_JOB = 0
...
fun scheduleJob(context: Context) {
    val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
    val job = JobInfo.Builder(
            MY_BACKGROUND_JOB,
            ComponentName(context, MediaContentJob::class.java)
    )
            .addTriggerContentUri(
                    JobInfo.TriggerContentUri(
                            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                            JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS
                    )
            )
            .build()
    jobScheduler.schedule(job)
}

Java

public static final int MY_BACKGROUND_JOB = 0;
...
public static void scheduleJob(Context context) {
  JobScheduler js =
          (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
  JobInfo.Builder builder = new JobInfo.Builder(
          MY_BACKGROUND_JOB,
          new ComponentName(context, MediaContentJob.class));
  builder.addTriggerContentUri(
          new JobInfo.TriggerContentUri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
          JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
  js.schedule(builder.build());
}

כשהמערכת מדווחת על שינוי ב-URI של התוכן שצוין, האפליקציה מקבלת קריאה חוזרת(callback) ואובייקט JobParameters מועבר לשיטה onStartJob() ב-MediaContentJob.class.

איך בודקים אילו רשויות תוכן הפעילו משימה

ב-Android 7.0 (רמת API 24) הורחבה גם ההגדרה JobParameters כדי לאפשר לאפליקציה לקבל מידע שימושי על רשויות התוכן וכתובות ה-URI שהפעילו את המשימה:

Uri[] getTriggeredContentUris()
מערך של מזהי URI שהפעילו את המשימה. הערך יהיה null אם אף URI לא הפעיל את המשימה (לדוגמה, המשימה הופעלה עקב מועד הגשה או מסיבה אחרת), או אם מספר ה-URIs שהשתנו גדול מ-50.
String[] getTriggeredContentAuthorities()
החזרת מערך מחרוזות של רשויות תוכן שהפעילו את המשימה. אם המערך המוחזר הוא לא null, משתמשים ב-getTriggeredContentUris() כדי לאחזר את הפרטים של מזהי ה-URI שהשתנו.

הקוד לדוגמה הבא מבטל את השיטה JobService.onStartJob() ומתעדה את רשויות התוכן ואת מזהי ה-URI שהפעילו את המשימה:

Kotlin

override fun onStartJob(params: JobParameters): Boolean {
    StringBuilder().apply {
        append("Media content has changed:\n")
        params.triggeredContentAuthorities?.also { authorities ->
            append("Authorities: ${authorities.joinToString(", ")}\n")
            append(params.triggeredContentUris?.joinToString("\n"))
        } ?: append("(No content)")
        Log.i(TAG, toString())
    }
    return true
}

Java

@Override
public boolean onStartJob(JobParameters params) {
  StringBuilder sb = new StringBuilder();
  sb.append("Media content has changed:\n");
  if (params.getTriggeredContentAuthorities() != null) {
      sb.append("Authorities: ");
      boolean first = true;
      for (String auth :
          params.getTriggeredContentAuthorities()) {
          if (first) {
              first = false;
          } else {
             sb.append(", ");
          }
           sb.append(auth);
      }
      if (params.getTriggeredContentUris() != null) {
          for (Uri uri : params.getTriggeredContentUris()) {
              sb.append("\n");
              sb.append(uri);
          }
      }
  } else {
      sb.append("(No content)");
  }
  Log.i(TAG, sb.toString());
  return true;
}

אופטימיזציה נוספת של האפליקציה

אופטימיזציה של האפליקציות כך שיפעלו במכשירים עם נפח זיכרון נמוך, או בתנאים של נפח זיכרון נמוך, יכולה לשפר את הביצועים ואת חוויית המשתמש. הסרת יחסי התלות בשירותי רקע ובמקלטים משתמעים של שידורים רשומים במניפסט יכולה לעזור לאפליקציה לפעול טוב יותר במכשירים כאלה. אמנם ב-Android 7.0 (רמת API 24) ננקטים צעדים כדי לצמצם חלק מהבעיות האלה, אבל מומלץ לבצע אופטימיזציה של האפליקציה כך שתופעל ללא שימוש בתהליכי הרקע האלה.

הפקודות הבאות של Android Debug Bridge‏ (ADB) יכולות לעזור לכם לבדוק את התנהגות האפליקציה כשתהליכי הרקע מושבתים:

  • כדי לדמות תנאים שבהם שידורים משתמעים ושירותי רקע לא זמינים, מזינים את הפקודה הבאה:
  • $ adb shell cmd appops set <package_name> RUN_IN_BACKGROUND ignore
    
  • כדי להפעיל מחדש שידורים מרומזים ושירותים ברקע, מזינים את הפקודה הבאה:
  • $ adb shell cmd appops set <package_name> RUN_IN_BACKGROUND allow
    
  • אתם יכולים לדמות מצב שבו המשתמש מעביר את האפליקציה שלכם למצב 'מוגבל' בנוגע לשימוש בסוללה ברקע. ההגדרה הזו מונעת מהאפליקציה לפעול ברקע. כדי לעשות זאת, מריצים את הפקודה הבאה בחלון מסוף:
  • $ adb shell cmd appops set <PACKAGE_NAME> RUN_ANY_IN_BACKGROUND deny