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

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

כדי לפתור את הבעיה הזו, ב-Android 7.0 (רמת 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, כדי להתאים את האפליקציה להגבלות החדשות האלה.

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

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

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

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

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

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

ההגבלות המדויקות שמוטלות על המכשיר נקבעות על ידי יצרן המכשיר. לדוגמה, בגרסאות של 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);
}

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

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

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

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

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

האפליקציה ממשיכה לקבל קריאות חוזרות (callback) עד שהיא יוצאת או עד שהיא קוראת ל-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 שמשויכים לעבודה, המערכת מספקת קריאה חוזרת גם אם היא מדווחת על שינוי רק באחד ממזהי ה-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 של התוכן שצוין, האפליקציה מקבלת קריאה חוזרת ואובייקט JobParameters מועבר לשיטה onStartJob() ב-MediaContentJob.class.

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

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

Uri[] getTriggeredContentUris()
מחזירה מערך של כתובות URI שהפעילו את העבודה. הערך יהיה null אם אף URI לא הפעיל את העבודה (לדוגמה, העבודה הופעלה בגלל תאריך יעד או סיבה אחרת), או אם מספר ה-URI שהשתנו גדול מ-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