Android StorageManager实现原理剖析

引言

在现在的Android手机中,EMMC已经从32G,64G开始了向128G, 512G的快速转变。
随着5G时代的到来,以及近些年Camera的兴起,越来越多数据将会在本地进行运算和存储。
那么,对于存储的管理就会越来越受人重视。
下图是一个AOSP Pixel的Storage截图,当然,这个界面各个厂商也是修改的最凶的。

我们这里主要分析的是原生的Storage manager的清理逻辑,以及下方各类型数据存储记录的规则。

Storage manager

Storage manager实现逻辑分析

在点击Storage manager的界面后,我们可以看到如下的界面:

那么点击移除照片和视频,将会有30天,60天,90天这几个选项。
代码实现的路径为src/com/android/settings/deletionhelper/AutomaticStorageManagerSettings.java
在进来之后,初始化的方法为:

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = super.onCreateView(inflater, container, savedInstanceState);

        initializeDaysToRetainPreference();
        initializeSwitchBar();

        return view;
    }

可以看到时进行了初始化Perference和SwitchBar的初始化设置。

    private void initializeDaysToRetainPreference() {
        mDaysToRetain = (DropDownPreference) findPreference(KEY_DAYS);
        mDaysToRetain.setOnPreferenceChangeListener(this);

        ContentResolver cr = getContentResolver();
        int photosDaysToRetain =
                Settings.Secure.getInt(
                        cr,
                        Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN,
                        Utils.getDefaultStorageManagerDaysToRetain(getResources()));
        String[] stringValues =
                getResources().getStringArray(R.array.automatic_storage_management_days_values);
        mDaysToRetain.setValue(stringValues[daysValueToIndex(photosDaysToRetain, stringValues)]);
    }

在这边,将会从数据库中取出保存的AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN的值,并进行显示。

    private void initializeSwitchBar() {
        final SettingsActivity activity = (SettingsActivity) getActivity();
        mSwitchBar = activity.getSwitchBar();
        mSwitchBar.setSwitchBarText(R.string.automatic_storage_manager_master_switch_title,
                R.string.automatic_storage_manager_master_switch_title);
        mSwitchBar.show();
        mSwitchController =
                new AutomaticStorageManagerSwitchBarController(
                        getContext(),
                        mSwitchBar,
                        mMetricsFeatureProvider,
                        mDaysToRetain,
                        getFragmentManager());
    }

取到的值,将会通过AutomaticStorageManagerSwitchBarController对象来进行初始化的保存。
而当每次用户操作数据有改变的时候,我们将会通过监听来获得:

    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) {
        if (KEY_DAYS.equals(preference.getKey())) {
            Settings.Secure.putInt(
                    getContentResolver(),
                    Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN,
                    Integer.parseInt((String) newValue));
        }
        return true;
    }

这边其实就是会将对应的值写到数据库中。那么AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN这个变量,就会非常的重要。
定义如下:

        /**
         * How many days of information for the automatic storage manager to retain on the device.
         *
         * @hide
         */
        public static final String AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN =
                "automatic_storage_manager_days_to_retain";

那么定义出来的天数是怎么检测的呢?这就从一个广播说起:

        <!-- Automatic storage management tasks. -->
        <service
            android:name=".automatic.AutomaticStorageManagementJobService"
            android:label="@string/automatic_storage_manager_service_label"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:enabled="@bool/enable_automatic_storage_management"
            android:exported="false"/>

        <receiver android:name=".automatic.AutomaticStorageBroadcastReceiver"
                  android:enabled="@bool/enable_automatic_storage_management">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

因为我们前面设置的是天数,所以这边我们将会起一个receiver来接收boot_complete的广播。

    @Override
    public void onReceive(Context context, Intent intent) {
        // Automatic deletion service
        JobScheduler jobScheduler =
                (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        ComponentName component = new ComponentName(context,
                AutomaticStorageManagementJobService.class);
        long periodicOverride = SystemProperties.getLong(DEBUG_PERIOD_FLAG, PERIOD);
        JobInfo job = new JobInfo.Builder(AUTOMATIC_STORAGE_JOB_ID, component)
                .setRequiresCharging(true)
                .setRequiresDeviceIdle(true)
                .setPeriodic(periodicOverride)
                .build();
        jobScheduler.schedule(job);
    }

这边可以看到,其实就是根据boot complete的时间,设置了jobscheduler的service去定时执行AutomaticStorageManagementJobService。
实现的文件为AutomaticStorageManagementJobService.java
在这边我们关注的是onStartJob的方法:

    @Override
    public boolean onStartJob(JobParameters args) {
        // We need to double-check the preconditions here because they are not enforced for a
        // periodic job.
        if (!preconditionsFulfilled()) {
            // By telling the system to re-schedule the job, it will attempt to execute again at a
            // later idle window -- possibly one where we are charging.
            jobFinished(args, true);
            return false;
        }

        mProvider = FeatureFactory.getFactory(this).getStorageManagementJobProvider();
        if (maybeDisableDueToPolicy(mProvider, this, getClock())) {
            jobFinished(args, false);
            return false;
        }

        if (!volumeNeedsManagement()) {
            Log.i(TAG, "Skipping automatic storage management.");
            Settings.Secure.putLong(getContentResolver(),
                    Settings.Secure.AUTOMATIC_STORAGE_MANAGER_LAST_RUN,
                    System.currentTimeMillis());
            jobFinished(args, false);
            return false;
        }

        if (!Utils.isStorageManagerEnabled(getApplicationContext())) {
            Intent maybeShowNotificationIntent =
                    new Intent(NotificationController.INTENT_ACTION_SHOW_NOTIFICATION);
            maybeShowNotificationIntent.setClass(getApplicationContext(),
                    NotificationController.class);
            getApplicationContext().sendBroadcast(maybeShowNotificationIntent);
            jobFinished(args, false);
            return false;
        }

        if (mProvider != null) {
            return mProvider.onStartJob(this, args, getDaysToRetain());
        }

        jobFinished(args, false);
        return false;
    }

当系统在低存储的模式下,并且打开了automatic storage management的功能,那么才会最后去执行mProvider的onStartJob的工作。
mProvider在AOSP中的实现就基本上终止了,pixel会使用文件管理器替换掉这个功能。
而华为,小米,Oppo,Vivo等厂商也是使用不同的定制化的apk去overlay storageManager。
这里需要注意,这个其实可以被overlay,override掉,所以也方便了各大厂商在这边的定制。

各种类型计算方案实现

计算这边我们分为两部分,第一部分是我们点击Settings的Perference后,进行的Activity跳转。

    @Override
    public boolean handlePreferenceTreeClick(Preference preference) {
        if (preference == null) {
            return false;
        }

        Intent intent = null;
        if (preference.getKey() == null) {
            return false;
        }
        switch (preference.getKey()) {
            case PHOTO_KEY:
                intent = getPhotosIntent();
                break;
            case AUDIO_KEY:
                intent = getAudioIntent();
                break;
            case GAME_KEY:
                intent = getGamesIntent();
                break;
            case MOVIES_KEY:
                intent = getMoviesIntent();
                break;
            case OTHER_APPS_KEY:
                // Because we are likely constructed with a null volume, this is theoretically
                // possible.
                if (mVolume == null) {
                    break;
                }
                intent = getAppsIntent();
                break;
            case FILES_KEY:
                intent = getFilesIntent();
                FeatureFactory.getFactory(mContext).getMetricsFeatureProvider().action(
                        mContext, SettingsEnums.STORAGE_FILES);
                break;
            case SYSTEM_KEY:
                final SystemInfoFragment dialog = new SystemInfoFragment();
                dialog.setTargetFragment(mFragment, 0);
                dialog.show(mFragment.getFragmentManager(), SYSTEM_FRAGMENT_TAG);
                return true;
        }

        if (intent != null) {
            intent.putExtra(Intent.EXTRA_USER_ID, mUserId);

            Utils.launchIntent(mFragment, intent);
            return true;
        }

        return super.handlePreferenceTreeClick(preference);
    }

其实在这边,就可以很容易的定义不同的perference以及将会出发的intent。
以点击Photo为例:

    private Intent getPhotosIntent() {
        Bundle args = getWorkAnnotatedBundle(2);
        args.putString(
                ManageApplications.EXTRA_CLASSNAME, Settings.PhotosStorageActivity.class.getName());
        args.putInt(
                ManageApplications.EXTRA_STORAGE_TYPE,
                ManageApplications.STORAGE_TYPE_PHOTOS_VIDEOS);
        return new SubSettingLauncher(mContext)
                .setDestination(ManageApplications.class.getName())
                .setTitleRes(R.string.storage_photos_videos)
                .setArguments(args)
                .setSourceMetricsCategory(mMetricsFeatureProvider.getMetricsCategory(mFragment))
                .toIntent();
    }

这里就会丁志伟相应跳转的activity了。


计算的方式如下,仍然以Photo的类型来进行说明:
这里其实是会有一个多用户的概念:

    public void onLoadFinished(SparseArray<StorageAsyncLoader.AppsStorageResult> result,
            int userId) {
        final StorageAsyncLoader.AppsStorageResult data = result.get(userId);
        final StorageAsyncLoader.AppsStorageResult profileData = result.get(
                Utils.getManagedProfileId(mContext.getSystemService(UserManager.class), userId));

        mPhotoPreference.setStorageSize(getPhotosSize(data, profileData), mTotalSize);
        mAudioPreference.setStorageSize(getAudioSize(data, profileData), mTotalSize);
        mGamePreference.setStorageSize(getGamesSize(data, profileData), mTotalSize);
        mMoviesPreference.setStorageSize(getMoviesSize(data, profileData), mTotalSize);
        mAppPreference.setStorageSize(getAppsSize(data, profileData), mTotalSize);
        mFilePreference.setStorageSize(getFilesSize(data, profileData), mTotalSize);

        if (mSystemPreference != null) {
            // Everything else that hasn't already been attributed is tracked as
            // belonging to system.
            long attributedSize = 0;
            for (int i = 0; i < result.size(); i++) {
                final StorageAsyncLoader.AppsStorageResult otherData = result.valueAt(i);
                attributedSize +=
                        otherData.gamesSize
                                + otherData.musicAppsSize
                                + otherData.videoAppsSize
                                + otherData.photosAppsSize
                                + otherData.otherAppsSize;
                attributedSize += otherData.externalStats.totalBytes
                        - otherData.externalStats.appBytes;
            }

            final long systemSize = Math.max(TrafficStats.GB_IN_BYTES, mUsedBytes - attributedSize);
            mSystemPreference.setStorageSize(systemSize, mTotalSize);
        }
    }

这里其实就是拿两个data:

        final StorageAsyncLoader.AppsStorageResult data = result.get(userId);
        final StorageAsyncLoader.AppsStorageResult profileData = result.get(
                Utils.getManagedProfileId(mContext.getSystemService(UserManager.class), userId));

然后进行后续photo,games等内容的传递。

    private long getPhotosSize(StorageAsyncLoader.AppsStorageResult data,
            StorageAsyncLoader.AppsStorageResult profileData) {
        if (profileData != null) {
            return data.photosAppsSize + data.externalStats.imageBytes
                    + data.externalStats.videoBytes
                    + profileData.photosAppsSize + profileData.externalStats.imageBytes
                    + profileData.externalStats.videoBytes;
        } else {
            return data.photosAppsSize + data.externalStats.imageBytes
                    + data.externalStats.videoBytes;
        }
    }

在photo中,其实只是对data的photo的size进行了相加,

    public static class AppsStorageResult {
        public long gamesSize;
        public long musicAppsSize;
        public long photosAppsSize;
        public long videoAppsSize;
        public long otherAppsSize;
        public long cacheSize;
        public StorageStatsSource.ExternalStorageStats externalStats;
    }

因为在AppsStorageResult类中,已经对其进行了计算。
这里就要提一下StorageAsyncLoader的实现了,在函数初始化后,将会对app进行loadapp的操作。

    @Override
    public SparseArray<AppsStorageResult> loadInBackground() {
        return loadApps();
    }

    private SparseArray<AppsStorageResult> loadApps() {
        mSeenPackages = new ArraySet<>();
        SparseArray<AppsStorageResult> result = new SparseArray<>();
        List<UserInfo> infos = mUserManager.getUsers();
        // Sort the users by user id ascending.
        Collections.sort(
                infos,
                new Comparator<UserInfo>() {
                    @Override
                    public int compare(UserInfo userInfo, UserInfo otherUser) {
                        return Integer.compare(userInfo.id, otherUser.id);
                    }
                });
        for (int i = 0, userCount = infos.size(); i < userCount; i++) {
            UserInfo info = infos.get(i);
            result.put(info.id, getStorageResultForUser(info.id));
        }
        return result;
    }

这边会去调用getStorageResultForUser进行统计,然后put到result中。

    private AppsStorageResult getStorageResultForUser(int userId) {
        Log.d(TAG, "Loading apps");
        List<ApplicationInfo> applicationInfos =
                mPackageManager.getInstalledApplicationsAsUser(0, userId);
        AppsStorageResult result = new AppsStorageResult();
        UserHandle myUser = UserHandle.of(userId);
        for (int i = 0, size = applicationInfos.size(); i < size; i++) {
            ApplicationInfo app = applicationInfos.get(i);

            StorageStatsSource.AppStorageStats stats;
            try {
                stats = mStatsManager.getStatsForPackage(mUuid, app.packageName, myUser);
            } catch (NameNotFoundException | IOException e) {
                // This may happen if the package was removed during our calculation.
                Log.w(TAG, "App unexpectedly not found", e);
                continue;
            }

            final long dataSize = stats.getDataBytes();
            final long cacheQuota = mStatsManager.getCacheQuotaBytes(mUuid, app.uid);
            final long cacheBytes = stats.getCacheBytes();
            long blamedSize = dataSize;
            // Technically, we could overages as freeable on the storage settings screen.
            // If the app is using more cache than its quota, we would accidentally subtract the
            // overage from the system size (because it shows up as unused) during our attribution.
            // Thus, we cap the attribution at the quota size.
            if (cacheQuota < cacheBytes) {
                blamedSize = blamedSize - cacheBytes + cacheQuota;
            }

            // This isn't quite right because it slams the first user by user id with the whole code
            // size, but this ensures that we count all apps seen once.
            if (!mSeenPackages.contains(app.packageName)) {
                blamedSize += stats.getCodeBytes();
                mSeenPackages.add(app.packageName);
            }

            switch (app.category) {
                case CATEGORY_GAME:
                    result.gamesSize += blamedSize;
                    break;
                case CATEGORY_AUDIO:
                    result.musicAppsSize += blamedSize;
                    break;
                case CATEGORY_VIDEO:
                    result.videoAppsSize += blamedSize;
                    break;
                case CATEGORY_IMAGE:
                    result.photosAppsSize += blamedSize;
                    break;
                default:
                    // The deprecated game flag does not set the category.
                    if ((app.flags & ApplicationInfo.FLAG_IS_GAME) != 0) {
                        result.gamesSize += blamedSize;
                        break;
                    }
                    result.otherAppsSize += blamedSize;
                    break;
            }
        }

        Log.d(TAG, "Loading external stats");
        try {
            result.externalStats = mStatsManager.getExternalStorageStats(mUuid,
                    UserHandle.of(userId));
        } catch (IOException e) {
            Log.w(TAG, e);
        }
        Log.d(TAG, "Obtaining result completed");
        return result;
    }

针对不同APP的category来进行类别的划分,并且进行size的计算。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值