引言
在现在的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的计算。