Android 分割槽和記憶體監控

AronJudge發表於2023-02-24

Android 分割槽和記憶體監控

Andorid之所以是分割槽,是因為各自有對應的功能和用途的考量,可以進行單獨讀寫和格式化。

Android 裝置包含兩類分割槽:

  1. 一類是啟動分割槽,對啟動過程至關重要。
  2. 一類是使用者分割槽,用於儲存與啟動無關的資訊。

啟動分割槽

  • boot 分割槽

    一般的嵌入式Linux的裝置中.bootloader,核心,根檔案系統被分為三個不同分割槽。在Android做得比較複雜,從這個手機分割槽和來看,這裡boot分割槽是把核心和[ramdisk file](https://www.zhihu.com/search?q=ramdisk file&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra={"sourceType"%3A"answer"%2C"sourceId"%3A196267472})的根檔案系統打包在一起了,是編譯生成boot.img來燒錄的。

    如果沒有這個分割槽,手機通常無法啟動到安卓系統。只有必要的時候,才去透過Recovery軟體擦除(format)這個分割槽,一旦擦除,裝置只有再重新安裝一個新的boot分割槽,可以透過安裝一個包含boot分割槽的ROM來實現,否則無法啟動安卓系統。

標準分割槽

  • system 分割槽。此分割槽包含 Android 框架。

    這裡是掛載到/system目錄下的分割槽。這裡有 /system/bin 和 /system/sbin 儲存很多系統命令。它是由編譯出來的system.img來燒入。

    相當於你電腦的C盤,用來放系統。這個分割槽基本包含了整個安卓作業系統,除了核心(kernel)和ramdisk。包括安卓使用者介面、和所有預裝的系統應用程式。擦除這個分割槽,會刪除整個安卓系統。你可以透過進入Recovery程式或者bootloader程式中,安裝一個新ROM,也就是新安卓系統。

  • odm 分割槽

    此分割槽包含原始設計製造商 (ODM) 對系統晶片 (SoC) 供應商板級支援包 (BSP) 的自定義設定。利用此類自定義設定,ODM 可以替換或自定義 SoC 元件,並在硬體抽象層 (HAL) 上為板級元件、守護程式和 ODM 特定的功能實現核心模組。此分割槽是可選的;通常情況下,它用於儲存自定義設定,以便裝置可以針對多個硬體 SKU 使用單個供應商映像。如需瞭解詳情,請參閱 ODM 分割槽

  • recovery 分割槽。

    recovery 分割槽即恢復分割槽,在正常分割槽被破壞後,仍可以進入這一分割槽進行備份和恢復.我的理解是這個分割槽儲存一個簡單的OS或底層軟體,在Android的核心被破壞後可以用bootloader從這個分割槽引導進行操作。

    這個分割槽可以認為是一個boot分割槽的替代品,可以是你的手機進入Recovery程式,進行高階恢復或安卓系統維護工作。

  • cache 分割槽。

    它將掛載到 /cache 目錄下。這個分割槽是安卓系統快取區,儲存系統最常訪問的資料和應用程式。擦除這個分割槽,不會影響個人資料,只是刪除了這個分割槽中已經儲存的快取內容,快取內容會在後續手機使用過程中重新自動生成。

  • userdata 分割槽

    此分割槽包含使用者安裝的應用和資料,包括自定義資料。它將掛載到 /data 目錄下, 它是由編譯出來的userdata.img來燒入。

    這個分割槽也叫使用者資料區,包含了使用者的資料:聯絡人、簡訊、設定、使用者安裝的程式。擦除這個分割槽,本質上等同於手機恢復出廠設定,也就是手機系統第一次啟動時的狀態,或者是最後一次安裝官方或第三方ROM後的狀態。在Recovery程式中進行的“data/factory reset ”操作就是在擦除這個分割槽。

  • vendor 分割槽

    此分割槽包含所有無法分發給 AOSP 的二進位制檔案。如果裝置不包含專有資訊,則可以忽略此分割槽。

  • radio 分割槽

    此分割槽包含無線裝置映像,只有包含無線裝置且在專用分割槽中儲存無線裝置專用軟體的裝置才需要此分割槽。

檢視分割槽大小

cat /proc/partitions
major minor  #blocks  name

179        0  153672 mmcblk0

179        1      104 mmcblk0p1

179        2      124 mmcblk0p2

179        3      140 mmcblk0p3

179        4      208 mmcblk0p4

179        5      892 mmcblk0p5

259        4      2040 mmcblk0p12

259        5      124 mmcblk0p13

259        6    30700 mmcblk0p14

259        7      4060 mmcblk0p15

259        8    8900 mmcblk0p16



259        9    8100 mmcblk0p17

259      10  13186048 mmcblk0p18

179      16      406 mmcblk0boot1

179        8      496 mmcblk0boot0


第一個mmcblk0為emmc的塊裝置,大小為15388672 KB。

emmc : Embedded Multi Media Card : 內嵌式儲存器標準規格。
是MMC協會訂立、主要針對手機或平板電腦等產品的內嵌式儲存器標準規格。NAND Flash+快閃記憶體控制晶片+標準介面封裝。內建儲存器的基礎上,又額外加了一個控制晶片,最後再以統一的方式封裝,並預留一個標準介面,以便手機客戶拿來直接使用。

ls -l /dev/block/platform/soc/by-name
lrwxrwxrwx root    root              2019-03-13 16:38 baseparam -> /dev/block/mmcblk0p5

lrwxrwxrwx root    root              2019-03-13 16:38 bootargs -> /dev/block/mmcblk0p2

lrwxrwxrwx root    root              2019-03-13 16:38 cache -> /dev/block/mmcblk0p17

lrwxrwxrwx root    root              2019-03-13 16:38 deviceinfo -> /dev/block/mmcblk0p4

lrwxrwxrwx root    root              2019-03-13 16:38 fastboot -> /dev/block/mmcblk0p1

lrwxrwxrwx root    root              2019-03-13 16:38 fastplay -> /dev/block/mmcblk0p9

lrwxrwxrwx root    root              2019-03-13 16:38 fastplaybak -> /dev/block/mmcblk0p10

lrwxrwxrwx root    root              2019-03-13 16:38 kernel -> /dev/block/mmcblk0p11

lrwxrwxrwx root    root              2019-03-13 16:38 logo -> /dev/block/mmcblk0p7

lrwxrwxrwx root    root              2019-03-13 16:38 logobak -> /dev/block/mmcblk0p8

lrwxrwxrwx root    root              2019-03-13 16:38 misc -> /dev/block/mmcblk0p12

lrwxrwxrwx root    root              2019-03-13 16:38 pqparam -> /dev/block/mmcblk0p6

lrwxrwxrwx root    root              2019-03-13 16:38 qbboot -> /dev/block/mmcblk0p13

lrwxrwxrwx root    root              2019-03-13 16:38 qbdata -> /dev/block/mmcblk0p14

lrwxrwxrwx root    root              2019-03-13 16:38 recovery -> /dev/block/mmcblk0p3

lrwxrwxrwx root    root              2019-03-13 16:38 system -> /dev/block/mmcblk0p16

lrwxrwxrwx root    root              2019-03-13 16:38 trustedcore -> /dev/block/mmcblk0p15

lrwxrwxrwx root    root              2019-03-13 16:38 userdata -> /dev/block/mmcblk0p18

可以看到 UserData 對應的分割槽是 mmcblk0p18 大小為 13186048KB = 12877 M = 12.5 G

在android中,device裡面的BroadConfig.mk中可以修改userdata的大小,注意這裡面的單位為位元組。

TARGET_USERIMAGES_USE_EXT4 := true

BOARD_SYSTEMIMAGE_PARTITION_SIZE := 838860800

BOARD_USERDATAIMAGE_PARTITION_SIZE := 135013152

BOARD_CACHEIMAGE_PARTITION_SIZE := 838860800

BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4

BOARD_FLASH_BLOCK_SIZE := 4096

BOARD_HAVE_BLUETOOTH := true

StorageManager

在Android系統中,常用的儲存介質是Nand Flash。系統的二進位制映象、Android的檔案系統等通常都儲存在Nand Flash 中。

Nand-flash儲存器是flash儲存器的一種,Nand-flash儲存器具有容量較大,改寫速度快等優點,適用於大量資料的儲存,因而在業界得到了越來越廣泛的應用,如嵌入式產品中包括數位相機、MP3隨身聽等。

StorageManager用來管理外部儲存上的資料安全,

android.os.storage.StorageManager.getSystemService(Contxt.STORAGE_SERVICE)

們 可以透過這個服務獲取Android裝置上的所有儲存裝置。 系統提供了 StorageManager 類,它有一個方法叫getVolumeList(),這個方法的返回值是一個StorageVolume陣列,StorageVolume類中封裝了掛載路徑,掛載狀態,以及是否可以移除等資訊。

DeviceStorageMonitorService

DeviceStorageMonitorService和DeviceStorageManagerService是一個東西,只是在5.0以後,名字改為了DeviceStorageMonitorService。

簡介

Device storage monitor module is composed of device monitor service (Google default)The purposes of device storage monitor service are monitoring device storage status and handling low storage conditions.

服務的新增

// Create the system service manager.
mSystemServiceManager = new SystemServiceManager(mSystemContext);
...

mSystemServiceManager.startService(DeviceStorageMonitorService.class);

透過SystemServiceManager的startService方法啟動了DSMS,看一下這個startService方法做了什麼:

public SystemService startService(String className) {
    final Class<SystemService> serviceClass;
    try {
        serviceClass = (Class<SystemService>)Class.forName(className);
    }
...
    return startService(serviceClass);
}

public <T extends SystemService> T startService(Class<T> serviceClass) {
    ...
        final T service;
        try {
            Constructor<T> constructor = serviceClass.getConstructor(Context.class);
            service = constructor.newInstance
	...
        // 註冊到ServiceManager中
        mServices.add(service);

        
        try {
            service.onStart();//啟動服務
        } 
...
}

其實就是用過反射獲取例項,然後將Service註冊新增到ServiceManager中, 最後呼叫了DSMS的onStart方法,那接下來就看看DSMS的構造方法 以及 onStart方法。

public DeviceStorageMonitorService(Context context) {
         super(context);
    	  // 初始化HandlerThread後臺執行緒,做check()
          mHandlerThread = new HandlerThread(TAG, android.os.Process.THREAD_PRIORITY_BACKGROUND);
          mHandlerThread.start();
          mHandler = new Handler(mHandlerThread.getLooper()) {
              @Override
              public void handleMessage(Message msg) {
                  switch (msg.what) {
                      case MSG_CHECK:
                          check();
                          return;
                  }
              }
          };
}

    @Override
    public void onStart() {
        final Context context = getContext();
        //獲取通知服務,傳送通知
        mNotifManager = context.getSystemService(NotificationManager.class);

        //cacheFile通知
        mCacheFileDeletedObserver = new CacheFileDeletedObserver();
        mCacheFileDeletedObserver.startWatching();

        // Ensure that the notification channel is set up
        PackageManager packageManager = context.getPackageManager();

        //addService到BinderService,也新增到LocalService
        publishBinderService(SERVICE, mRemoteService);
        publishLocalService(DeviceStorageMonitorInternal.class, mLocalService);

        //開始check()
        // Kick off pass to examine storage state
        mHandler.removeMessages(MSG_CHECK);
        mHandler.obtainMessage(MSG_CHECK).sendToTarget();
    }

DeviceStorageMonitorInternal提供的介面

  //內部服務提供check():檢測介面   
  //isMemoryLow:是否LowStorage 
  //getMemoryLowThreshold:data的低儲存值
    private final DeviceStorageMonitorInternal mLocalService = new DeviceStorageMonitorInternal() {
        @Override
        //傳送Msg觸發Handler,check()
        public void checkMemory() {
            // Kick off pass to examine storage state
            mHandler.removeMessages(MSG_CHECK);
            mHandler.obtainMessage(MSG_CHECK).sendToTarget();
        }

        //data分割槽可使用空間<500M
        @Override
        public boolean isMemoryLow() {
            return Environment.getDataDirectory().getUsableSpace() < getMemoryLowThreshold();
        }

        //500M,具體專案在看
        @Override
        public long getMemoryLowThreshold() {
            return getContext().getSystemService(StorageManager.class)
                    .getStorageLowBytes(Environment.getDataDirectory());
        }
    };

check() /data分割槽

    @WorkerThread
    private void check() {
        final StorageManager storage = getContext().getSystemService(StorageManager.class);
        final int seq = mSeq.get();

        //本地列印只有/data目錄
        // Check every mounted private volume to see if they're low on space
        for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
            final File file = vol.getPath();
            final long fullBytes = storage.getStorageFullBytes(file);
            final long lowBytes = storage.getStorageLowBytes(file);//500M

            // Automatically trim cached data when nearing the low threshold;
            // when it's within 150% of the threshold, we try trimming usage
            // back to 200% of the threshold.
            if (file.getUsableSpace() < (lowBytes * 3) / 2) {
                final PackageManagerService pms = (PackageManagerService) ServiceManager
                        .getService("package");
                //lowBytes的1.5倍容量時觸發freeStorage
                try {
                    pms.freeStorage(vol.getFsUuid(), lowBytes * 2, 0);
                } catch (IOException e) {
                    Slog.w(TAG, e);
                }
            }

            // Send relevant broadcasts and show notifications based on any
            // recently noticed state transitions.
            final UUID uuid = StorageManager.convert(vol.getFsUuid());
            final State state = findOrCreateState(uuid);
            final long totalBytes = file.getTotalSpace();//data總大小
            final long usableBytes = file.getUsableSpace();//可使用大小

            int oldLevel = state.level;
            int newLevel;
            //判斷是LEVEL_LOW,LEVEL_FULL還是LEVEL_NORMAL
            if (mForceLevel != State.LEVEL_UNKNOWN) {
                // When in testing mode, use unknown old level to force sending
                // of any relevant broadcasts.
                oldLevel = State.LEVEL_UNKNOWN;
                newLevel = mForceLevel;
            } else if (usableBytes <= fullBytes) {
                newLevel = State.LEVEL_FULL;
            } else if (usableBytes <= lowBytes) {
                newLevel = State.LEVEL_LOW;
            } else if (StorageManager.UUID_DEFAULT.equals(uuid) && !isBootImageOnDisk()
                    && usableBytes < BOOT_IMAGE_STORAGE_REQUIREMENT) {
                newLevel = State.LEVEL_LOW;
            } else {
                newLevel = State.LEVEL_NORMAL;
            }

            // Log whenever we notice drastic storage changes
            if ((Math.abs(state.lastUsableBytes - usableBytes) > DEFAULT_LOG_DELTA_BYTES)
                    || oldLevel != newLevel) {
                //log
                EventLogTags.writeStorageState(uuid.toString(), oldLevel, newLevel,
                        usableBytes, totalBytes);
                state.lastUsableBytes = usableBytes;
            }
            //傳送通知
            updateNotifications(vol, oldLevel, newLevel);
            //傳送廣播
            updateBroadcasts(vol, oldLevel, newLevel, seq);

            state.level = newLevel;
        }

        //沒有check訊息,繼續30s檢測
        // Loop around to check again in future; we don't remove messages since
        // there might be an immediate request pending.
        if (!mHandler.hasMessages(MSG_CHECK)) {
            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHECK),
                    DEFAULT_CHECK_INTERVAL);
        }
    }

傳送廣播

    private void updateBroadcasts(VolumeInfo vol, int oldLevel, int newLevel, int seq) {
        if (!Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, vol.getFsUuid())) {
            // We don't currently send broadcasts for secondary volumes
            return;
        }
        //lowStorage廣播action  ACTION_DEVICE_STORAGE_LOW
        final Intent lowIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_LOW)
                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
                        | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
                        | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS)
                .putExtra(EXTRA_SEQUENCE, seq);
        //正常Storage廣播action  ACTION_DEVICE_STORAGE_OK
        final Intent notLowIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_OK)
                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
                        | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
                        | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS)
                .putExtra(EXTRA_SEQUENCE, seq);
        
        if (State.isEntering(State.LEVEL_LOW, oldLevel, newLevel)) {
            //  記憶體正在變得越來越小,只傳送一次廣播ACTION_DEVICE_STORAGE_LOW,粘性廣播,程式註冊肯定會收到廣播
            getContext().sendStickyBroadcastAsUser(lowIntent, UserHandle.ALL);
        } else if (State.isLeaving(State.LEVEL_LOW, oldLevel, newLevel)) {
            //記憶體正在變得越來越大,恢復正常移除lowIntent粘性廣播,傳送normal的普通廣播
            getContext().removeStickyBroadcastAsUser(lowIntent, UserHandle.ALL);
            getContext().sendBroadcastAsUser(notLowIntent, UserHandle.ALL);
        }

        final Intent fullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_FULL)
                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)
                .putExtra(EXTRA_SEQUENCE, seq);
        final Intent notFullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_NOT_FULL)
                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)
                .putExtra(EXTRA_SEQUENCE, seq);

        //傳送FULL Storage廣播ACTION_DEVICE_STORAGE_FULL
        if (State.isEntering(State.LEVEL_FULL, oldLevel, newLevel)) {
            getContext().sendStickyBroadcastAsUser(fullIntent, UserHandle.ALL);
        } else if (State.isLeaving(State.LEVEL_FULL, oldLevel, newLevel)) {
            getContext().removeStickyBroadcastAsUser(fullIntent, UserHandle.ALL);
            getContext().sendBroadcastAsUser(notFullIntent, UserHandle.ALL);
        }
    }

APP 監聽 lowStorage廣播

    public void registerLowStorageBroadcast() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
        filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
        mContext.registerReceiver(mReceiver, filter);
    }

    /** Receives events that might indicate a need to clean up files. */
    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
                Log.i(TAG, "handleStorageLow storage Low");
            } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
                Log.i(TAG, "handleStorageLow storage Ok");
            }
        }
    };

解決實際問題

目前Android 11 系統有儲存滿了的提示和介面, 現在提供兜底方案,防止Android Userdata分割槽被寫滿,導致Android無法啟動。

1、監控磁碟使用狀況,剩餘分割槽容量低於警戒值,則下次開機時格式化分割槽

2、監控磁碟使用狀況,剩餘分割槽容量低於危險值,則立即格式化分割槽

3、警戒值和危險值大小評估

經過上述內容可以瞭解到, Android 11 是存在自己的記憶體檢查機制的,當記憶體解決低閾值時自動修剪快取資料;

當它在閾值的150%以內時,我們嘗試調整清理快取返回閾值到的200%,同時傳送廣播。這裡有兩個疑問:

  1. Android 的原生閾值是多少呢?
  2. Android 原聲的 記憶體檢查機制能滿足上述問題嗎?

Android 的原生閾值是多少呢?

private static final int DEFAULT_THRESHOLD_PERCENTAGE = 5;
private static final long DEFAULT_THRESHOLD_MAX_BYTES = DataUnit.MEBIBYTES.toBytes(500);

public long getStorageLowBytes(File path) {
    final long lowPercent = Settings.Global.getInt(mResolver,
                                                   Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE);
    final long lowBytes = (path.getTotalSpace() * lowPercent) / 100;

    final long maxLowBytes = Settings.Global.getLong(mResolver,
                                                     Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);

    return Math.min(lowBytes, maxLowBytes); 
}

預設總記憶體的 5% 和 500M 選擇最小的(可配置)

專案上 UserData 對應的分割槽是 mmcblk0p18 大小為 13186048KB = 12877 M = 12.5 G

12877 * 5% = 643.84 。 所以選500M 為閾值。

解答疑問:

閾值 = 500M

Android 原聲的記憶體檢查機制能滿足上述問題嗎?

釋放記憶體

try {
    pms.freeStorage(vol.getFsUuid(), lowBytes * 2, 0);
} catch (IOException e) {
    // 捕獲異常未做處理
    Slog.w(TAG, e);
}

PackageManagerService.java

/**
       * Blocking call to clear various types of cached data across the system
       * until the requested bytes are available.
       */
public void freeStorage(String volumeUuid, long bytes, int storageFlags) throws IOException {
    final StorageManager storage = mInjector.getStorageManager();
    final File file = storage.findPathForUuid(volumeUuid);
    if (file.getUsableSpace() >= bytes) return;

    if (ENABLE_FREE_CACHE_V2) {
        final boolean internalVolume = Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL,volumeUuid);
        final boolean aggressive = (storageFlags
                                    & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0;
        final long reservedBytes = storage.getStorageCacheBytes(file, storageFlags);

        // 1. Pre-flight to determine if we have any chance to succeed
        // 確定我們是否有機會成功
        // 2. Consider preloaded data (after 1w honeymoon, unless aggressive)
        if (internalVolume && (aggressive || SystemProperties
                               .getBoolean("persist.sys.preloads.file_cache_expired", false))) {
            // 刪除預載入的檔案
            deletePreloadsFileCache();
            if (file.getUsableSpace() >= bytes) return;
        }

        // 3. Consider parsed APK data (aggressive only)
        // 已解析的APK資料
        if (internalVolume && aggressive) {
            FileUtils.deleteContents(mCacheDir);
            if (file.getUsableSpace() >= bytes) return;
        }

        // 4. Consider cached app data (above quotas)
        // 快取的應用資料
        try {
            mInstaller.freeCache(volumeUuid, bytes, reservedBytes,
                                 Installer.FLAG_FREE_CACHE_V2);
        } catch (InstallerException ignored) {
        }
        if (file.getUsableSpace() >= bytes) return;

        // 5. Consider shared libraries with refcount=0 and age>min cache period
        // 共享庫
        if (internalVolume && pruneUnusedStaticSharedLibraries(bytes,
                                                               android.provider.Settings.Global.getLong(mContext.getContentResolver(),
                                                                                                        Global.UNUSED_STATIC_SHARED_LIB_MIN_CACHE_PERIOD,
                                                                                                        DEFAULT_UNUSED_STATIC_SHARED_LIB_MIN_CACHE_PERIOD))) {
            return;
        }

        // 6. Consider dexopt output (aggressive only)
        // TODO: Implement

        // 7. Consider installed instant apps unused longer than min cache period
        // 考慮已安裝的即時應用未使用時間超過最小快取時間
        if (internalVolume && mInstantAppRegistry.pruneInstalledInstantApps(bytes,
                                                                            android.provider.Settings.Global.getLong(mContext.getContentResolver(),
                                                                                                                     Global.INSTALLED_INSTANT_APP_MIN_CACHE_PERIOD,
                                                                                                                     InstantAppRegistry.DEFAULT_INSTALLED_INSTANT_APP_MIN_CACHE_PERIOD))) {
            return;
        }

        // 8. Consider cached app data (below quotas)
        // 快取的應用資料
        try {
            mInstaller.freeCache(volumeUuid, bytes, reservedBytes,
                                 Installer.FLAG_FREE_CACHE_V2 | Installer.FLAG_FREE_CACHE_V2_DEFY_QUOTA);
        } catch (InstallerException ignored) {
        }
        if (file.getUsableSpace() >= bytes) return;

        // 9. Consider DropBox entries
        // TODO: Implement

        // 10. Consider instant meta-data (uninstalled apps) older that min cache period
        // 未安裝的應用程式
        if (internalVolume && mInstantAppRegistry.pruneUninstalledInstantApps(bytes,
                                                                              android.provider.Settings.Global.getLong(mContext.getContentResolver(),
                                                                                                                       Global.UNINSTALLED_INSTANT_APP_MIN_CACHE_PERIOD,
                                                                                                                       InstantAppRegistry.DEFAULT_UNINSTALLED_INSTANT_APP_MIN_CACHE_PERIOD))) {
            return;
        }
    } else {
        try {
            mInstaller.freeCache(volumeUuid, bytes, 0, 0);
        } catch (InstallerException ignored) {
        }
        if (file.getUsableSpace() >= bytes) return;
    }

    // 如果清除了所有快取, 還不滿足最, 丟擲異常
    throw new IOException("Failed to free " + bytes + " on storage device at " + file);
}

刪除預載入的檔案

@Override
public void deletePreloadsFileCache() {
    mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CLEAR_APP_CACHE,
                                            "deletePreloadsFileCache");
    File dir = Environment.getDataPreloadsFileCacheDirectory();
    Slog.i(TAG, "Deleting preloaded file cache " + dir);
    FileUtils.deleteContents(dir);
}

FileUtils.java
public static boolean deleteContents(File dir) {
    File[] files = dir.listFiles();
    boolean success = true;
    if (files != null) {
        for (File file : files) {
            if (file.isDirectory()) {
                // 遞迴
                success &= deleteContents(file);
            }
            if (!file.delete()) {
                Log.w(TAG, "Failed to delete " + file);
                success = false;
            }
        }
    }
    return success;
}

刪除快取的應用資料 快取

系統快取」由所有已安裝應用的 /data/data/packagename/cache 資料夾和 /sdcard/Android/data/packagename/cache 資料夾組成。

Installer.java
public void freeCache(String uuid, long targetFreeBytes, long cacheReservedBytes, int flags)
    throws InstallerException {
    if (!checkBeforeRemote()) return;
    try {
        mInstalld.freeCache(uuid, targetFreeBytes, cacheReservedBytes, flags);
    } catch (Exception e) {
        throw InstallerException.from(e);
    }
}


private boolean checkBeforeRemote() {
    if (mWarnIfHeld != null && Thread.holdsLock(mWarnIfHeld)) {
        Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding 0x"
                 + Integer.toHexString(System.identityHashCode(mWarnIfHeld)), new Throwable());
    }
    if (mIsolated) {
        Slog.i(TAG, "Ignoring request because this installer is isolated");
        return false;
    } else {
        return true;
    }
}
  

mInstalld.freeCache(uuid, targetFreeBytes, cacheReservedBytes, flags); 這個實際是呼叫

frameworks/native/cmds/installd/binder/android/os/IInstalld.aidl 中定義的方法。最後呼叫的是 InstalldNativeService.cpp

binder::Status InstalldNativeService::freeCache(const std::unique_ptr<std::string>& uuid,
                                                int64_t targetFreeBytes, int64_t    cacheReservedBytes, int32_t flags) {
    ENFORCE_UID(AID_SYSTEM);
    // 檢查UUID
    CHECK_ARGUMENT_UUID(uuid);
    std::lock_guard<std::recursive_mutex> lock(mLock);

    auto uuidString = uuid ? *uuid : "";
    const char* uuid_ = uuid ? uuid->c_str() : nullptr;
    auto data_path = create_data_path(uuid_);
    auto noop = (flags & FLAG_FREE_CACHE_NOOP);

    // 確定的可釋放的空間 = 還需要釋放的空間
    int64_t free = data_disk_free(data_path);
    if (free < 0) {
        return error("Failed to determine free space for " + data_path);
    }

    int64_t cleared = 0;
    // 目標釋放的目標空間 - 確定的可釋放的空間 = 還需要釋放的空間
    int64_t needed = targetFreeBytes - free;
    LOG(DEBUG) << "Device " << data_path << " has " << free << " free; requested "
        << targetFreeBytes << "; needed " << needed;

    // 確定的可釋放的空間 > 目標釋放的目標空間 return ok 
    if (free >= targetFreeBytes) {
        return ok();
    }

    if (flags & FLAG_FREE_CACHE_V2) {
        // This new cache strategy fairly removes files from UIDs by deleting
        // files from the UIDs which are most over their allocated quota
        // 這種新的快取策略透過從UID中刪除超出其分配配額最多的檔案,從UID中公平地刪除檔案
        // 1. Create trackers for every known UID
        ATRACE_BEGIN("create");
        std::unordered_map<uid_t, std::shared_ptr<CacheTracker>> trackers;
        for (auto user : get_known_users(uuid_)) {
            FTS *fts;
            FTSENT *p;
            auto ce_path = create_data_user_ce_path(uuid_, user);
            auto de_path = create_data_user_de_path(uuid_, user);
            auto media_path = findDataMediaPath(uuid, user) + "/Android/data/";
            char *argv[] = { (char*) ce_path.c_str(), (char*) de_path.c_str(),
                            (char*) media_path.c_str(), nullptr };
            if (!(fts = fts_open(argv, FTS_PHYSICAL | FTS_NOCHDIR | FTS_XDEV, nullptr))) {
                return error("Failed to fts_open");
            }
            while ((p = fts_read(fts)) != nullptr) {
                if (p->fts_info == FTS_D && p->fts_level == 1) {
                    uid_t uid = p->fts_statp->st_uid;
                    if (multiuser_get_app_id(uid) == AID_MEDIA_RW) {
                        uid = (multiuser_get_app_id(p->fts_statp->st_gid) - AID_EXT_GID_START)
                            + AID_APP_START;
                    }
                    auto search = trackers.find(uid);
                    if (search != trackers.end()) {
                        search->second->addDataPath(p->fts_path);
                    } else {
                        auto tracker = std::shared_ptr<CacheTracker>(new CacheTracker(
                            multiuser_get_user_id(uid), multiuser_get_app_id(uid), uuidString));
                        tracker->addDataPath(p->fts_path);
                        {
                            std::lock_guard<std::recursive_mutex> lock(mQuotasLock);
                            tracker->cacheQuota = mCacheQuotas[uid];
                        }
                        if (tracker->cacheQuota == 0) {
                            #if MEASURE_DEBUG
                            LOG(WARNING) << "UID " << uid << " has no cache quota; assuming 64MB";
                            #endif
                            tracker-> cacheQuota = 67108864;
                        }
                        trackers[uid] = tracker;
                    }
                    fts_set(fts, p, FTS_SKIP);
                }
            }
            fts_close(fts);
        }
        ATRACE_END();

        // 2. Populate tracker stats and insert into priority queue
        ATRACE_BEGIN("populate");
        int64_t cacheTotal = 0;
        auto cmp = [](std::shared_ptr<CacheTracker> left, std::shared_ptr<CacheTracker> right) {
            return (left->getCacheRatio() < right->getCacheRatio());
        };
        std::priority_queue<std::shared_ptr<CacheTracker>,
        std::vector<std::shared_ptr<CacheTracker>>, decltype(cmp)> queue(cmp);
        for (const auto& it : trackers) {
            it.second->loadStats();
            queue.push(it.second);
            cacheTotal += it.second->cacheUsed;
        }
        ATRACE_END();

        // 3. Bounce across the queue, freeing items from whichever tracker is
        // the most over their assigned quota
        // 在佇列中跳躍,從超出其分配配額最多的跟蹤器中釋放專案
        ATRACE_BEGIN("bounce");
        std::shared_ptr<CacheTracker> active;
        while (active || !queue.empty()) {
            // Only look at apps under quota when explicitly requested
            // 僅在明確請求時檢視配額下的應用
            if (active && (active->getCacheRatio() < 10000)
                && !(flags & FLAG_FREE_CACHE_V2_DEFY_QUOTA)) {
                LOG(DEBUG) << "Active ratio " << active->getCacheRatio()
                    << " isn't over quota, and defy not requested";
                break;
            }

            // Only keep clearing when we haven't pushed into reserved area
            if (cacheReservedBytes > 0 && cleared >= (cacheTotal - cacheReservedBytes)) {
                LOG(DEBUG) << "Refusing to clear cached data in reserved space";
                break;
            }

            // Find the best tracker to work with; this might involve swapping
            // if the active tracker is no longer the most over quota
            // 找到最佳的跟蹤器;這可能涉及交換 如果活動跟蹤器不再超出配額
            bool nextBetter = active && !queue.empty()
                && active->getCacheRatio() < queue.top()->getCacheRatio();
            if (!active || nextBetter) {
                if (active) {
                    // Current tracker still has items, so we'll consider it
                    // again later once it bubbles up to surface
                    queue.push(active);
                }
                active = queue.top(); queue.pop();
                active->ensureItems();
                continue;
            }

            // If no items remain, go find another tracker
            // 如果沒有剩餘專案,請查詢另一個跟蹤器
            if (active->items.empty()) {
                active = nullptr;
                continue;
            } else {
                auto item = active->items.back();
                active->items.pop_back();

                LOG(DEBUG) << "Purging " << item->toString() << " from " << active->toString();
                if (!noop) {
                    item->purge();
                }
                active->cacheUsed -= item->size;
                needed -= item->size;
                cleared += item->size;
            }

            // Verify that we're actually done before bailing, since sneaky
            // apps might be using hardlinks
            // 驗證我們在保釋之前是否已經完成,因為偷偷摸摸的應用程式可能正在使用硬連結
            if (needed <= 0) {
                free = data_disk_free(data_path);
                needed = targetFreeBytes - free;
                if (needed <= 0) {
                    break;
                } else {
                    LOG(WARNING) << "Expected to be done but still need " << needed;
                }
            }
        }
        ATRACE_END();

    } else {
        return error("Legacy cache logic no longer supported");
    }

    free = data_disk_free(data_path);
    if (free >= targetFreeBytes) {
        return ok();
    } else {
        return error(StringPrintf("Failed to free up %" PRId64 " on %s; final free space %" PRId64,
                                  targetFreeBytes, data_path.c_str(), free));
    }
}

什麼是Cache Tracker

單個UID的快取跟蹤器。
每個跟蹤器有兩種模式:
第一種模式 載入輕量級的“統計資料”,
第二種模式 載入詳細的“專案”

然後可以清除這些專案以釋放空間

6  
17  #ifndef ANDROID_INSTALLD_CACHE_TRACKER_H
18  #define ANDROID_INSTALLD_CACHE_TRACKER_H
19  
20  #include <memory>
21  #include <string>
22  #include <queue>
23  
24  #include <sys/types.h>
25  #include <sys/stat.h>
26  
27  #include <android-base/macros.h>
28  #include <cutils/multiuser.h>
29  
30  #include "CacheItem.h"
31  
32  namespace android {
33  namespace installd {
34  
35  /**
36   * Cache tracker for a single UID. Each tracker is used in two modes: first
37   * for loading lightweight "stats", and then by loading detailed "items"
38   * which can then be purged to free up space.
39   */
    
40  class CacheTracker {
41  public:
42      CacheTracker(userid_t userId, appid_t appId, const std::string& uuid);
43      ~CacheTracker();
44  
45      std::string toString();
46  
47      void addDataPath(const std::string& dataPath);
48  
49      void loadStats();
50      void loadItems();
51  
52      void ensureItems();
53  
54      int getCacheRatio();
55  
56      int64_t cacheUsed;
57      int64_t cacheQuota;
58  
59      std::vector<std::shared_ptr<CacheItem>> items;
60  
61  private:
62      userid_t mUserId;
63      appid_t mAppId;
64      bool mItemsLoaded;
65      const std::string& mUuid;
66  
67      std::vector<std::string> mDataPaths;
68  
69      bool loadQuotaStats();
70      void loadItemsFrom(const std::string& path);
71  
72      DISALLOW_COPY_AND_ASSIGN(CacheTracker);
73  };
74  
75  }  // namespace installd
76  }  // namespace android
77  
78  #endif  // ANDROID_INSTALLD_CACHE_TRACKER_H


留下疑問:Cache Tracker的記憶體釋放策略是什麼樣的,目前還未找到答案。

解答疑問:

透過上述原始碼,瞭解到Android 會從以上空間幫我們釋放記憶體:

  1. 刪除預載入的檔案
  2. 已解析的APK資料
  3. 快取的應用資料
  4. 共享庫
  5. 已安裝的即時應用未使用時間超過最小快取時間
  6. 快取的應用資料
  7. 未安裝的應用程式

但是如果清除了上述所有的地方 可用的記憶體空間 還是很小, 不滿足 2 倍的閾值, 丟擲

throw new IOException("Failed to free " + bytes + " on storage device at " + file);
try {
    pms.freeStorage(vol.getFsUuid(), lowBytes * 2, 0);
} catch (IOException e) {
    // 捕獲異常未做處理
    Slog.w(TAG, e);
}

我們看到 Android 在捕獲到異常後未做任何處理。那記憶體極限緊張的情況下,Android是怎麼處理的呢?

記憶體極限緊張的情況下,Android是怎麼處理的呢?

使用模擬器實驗驗證:

開機前 Data分割槽容量

Total Used Available 閾值
774 M 378 M 364 M 774 * 5% = 38 M

目前還是大於閾值, 開始寫入 380M 檔案

dd if=/dev/block/dm-0 of=13gbFile bs=1024 count=68640

Total Used Available 閾值
774 M 758 M 16M 774 * 5% = 38 M

現在已經小於閾值了, 重啟後:

Data分割槽還是滿的,系統還是可以起來。檢視Log

81253170 = 76M (閾值)= 38 * 2

我們可以看到當前已經沒有辦法清除到 2 倍的閾值, 並列印了Android 的原生log。 但是還是不影響系統啟動。但是為了防止記憶體極端緊張的情況下系統,還是增加兜底方案。

在系統原有機制中,定製空間不足時,小於 lowBytes / 2 寫一個屬性persist.vendor.data.lowspace ,下次重啟執行wipe data; 小於lowBytes / 3 M 直接清理 wipe dated。

try {
    pms.freeStorage(vol.getFsUuid(), lowBytes * 2, 0);
} catch (IOException e) {
    Slog.w(TAG, e);
    try {
        if (file.getUsableSpace() < lowBytes / 2) {  // only 500M
            if (vol.id != null)
                Slog.w(TAG, "id = " + vol.id);
            SystemProperties.set("persist.vendor.data.lowspace", "true");
        }
        if (file.getUsableSpace() < lowBytes / 3) {  // only 166M, wipedata directly
            SystemProperties.set("persist.vendor.data.lowspace", "false");
            Runtime.getRuntime().exec("reboot wipedata");
        }
    } catch (Exception e2) {
        Slog.w(TAG, e2);
    }
}
}

開機 - 雙清

static Result<Success> do_load_persist_props(const BuiltinArguments& args) {
    load_persist_props();
    if (::property_get_bool("persist.vendor.data.lowspace", 0)) {
        property_set("persist.vendor.data.lowspace", "false");  // reset to false
        property_set(ANDROID_RB_PROPERTY, "reboot,wipedata");
    }
    return Success();
}

雙清後:

記憶體回覆到出廠設定。

相關文章