2013 年 9 月 2 日

アンドロイドアプリのキャッシュ削除

2995-logo

アンドロイドアプリは、必要に応じてキャッシュデータを保存する。
アプリを使っている間に、いつのまにかキャッシュが増加して、ストレージを圧迫していくことになる。
アプリのキャッシュは設定のアプリケーション管理で、消去できる。またAndroid4.2からは一括でキャッシュを消去する機能が追加された。
4.2より前のOSで、全てのアプリのキャッシュを一括消去するには、キャッシュクリーナーやタスクキラーと呼ばれるユーティリティアプリを使うとよいだろう。

ゲームや役に立つアプリをいきなり作ろうと思っても、まずアイデアが浮かばないのが現実だろう。そんなときは、ユーティリティアプリから始めて見るのはいいかもしれない。

UI/UXやデザインなどには自信がないが、APIやOSをハックするのが好きな人にとってはうってつけではないだろうか。
しかし、こういったユーティリティアプリを作るには、既存のAPIの理解だけではなく、AndroidOSの深い知識が必要にもなってくるので、それなりの知識がある人でないと、これはこれで敷居が高いものである。知識がなければ、調査(検索?)力で知識を獲得するしかない。

たとえば、自アプリのキャッシュを消去するのは簡単だが、全てのアプリのキャッシュを消去することは簡単ではない。
消去するだけでなく、各アプリのキャッシュサイズを調べるのさえ、簡単にはいかない。
最初にアンドロイドの各アプリのキャッシュサイズを求める方法をいくつか試してみる。

キャッシュサイズ取得API

まずは正攻法的に、既存のAPIを調べるのが基本だろう。
キャッシュサイズが取得できそうなものとして、PackageStatsクラスが存在する。

はて、なんでクラスなんだ?と最初に感じた(結局その予感は的中するのだが)。パッケージネームを引数とするコンストラクタがあるので、これで生成してみる。例では、現在インストールされているアプリの一覧から、パッケージ名を取得して、PackageStatsを生成している。

    List<ApplicationInfo> application = new ArrayList<ApplicationInfo>();
    application = mPackageManager.getInstalledApplications(0);
    for (int i = 0; i < application.size(); i++) {
      ApplicationInfo app = application.get(i);
      
      PackageStats ps = new PackageStats(app.packageName);
      // PackageStatsのサイズで求めてみる。
      Log.d(TAG,  "[PackageStats] " + app.packageName + " cache size = " + ps.cacheSize);
    }

しかし、全て0となる。結局、このクラスはPOJOでしかなさそうだ。

直接キャッシュディレクトリを読む

色々検索してみると、キャッシュディレクトリを直接読めばいいだろうという記事もいくつかある。これも試しにやってみよう。

しかし、結果全く取得できない。createPackageContextはその名の通り生成メソッドなんで、既存のパッケージ情報を生成することはなさそうだ。やはり、他のアプリの情報を読むことは、そう簡単ではない。
キャッシュが削除できてしまうことより、読めてしまうことの方がセキュリティ上の問題大なんで、ホッとする結果だ(笑)。

    List<ApplicationInfo> application = new ArrayList<ApplicationInfo>();
    application = mPackageManager.getInstalledApplications(0);
    for (int i = 0; i < application.size(); i++) {
      ApplicationInfo app = application.get(i);
      Context con = null;
      try {
        con = mContext.createPackageContext(app.packageName, Context.CONTEXT_IGNORE_SECURITY);
        // 以下のエラーが発生する。
        // 09-01 16:00:36.990: W/ApplicationContext(25439): Unable to create cache directory
      } catch (NameNotFoundException e) {
        Log.d(TAG, e.toString());
      }
      // キャッシュディレクトリのサイズで求めてみる。
      File cacheDir = con.getCacheDir();
      if (cacheDir != null) {
        Log.d(TAG,  "[cacheDir] " + app.packageName + " cache size = " + cacheDir.length());
        long size = 0;
        // キャッシュディレクトリ下のファイルサイズから求めてみる。
        File[] files = cacheDir();
        if (files != null) {
          for (File f:files) {
              size = size + f.length();
          }
          Log.d(TAG,  "[cacheDir/] " + app.packageName + " cache size = " + size);
        }
      }
    }   

ハイドメソッド(hide method)

では、こういったことはrootedされたアプリでないとできないのだろうか。
しかし、ちまたにはキャッシュクリーナーやタスクキラーと称したアプリがたくさんある(もちろん、rootedされていない)。だから、何か手があるんだろうと推測できるわけである。

実はアンドロイドには、hide methodというものが存在し、ちょっと手間をかけるとこういった隠しメソッドを呼ぶことができる。この隠しメソッドに、全アプリのキャッシュサイズを取得するものが存在するのである。

もちろん、非公式・非サポートのメソッドであるし、いつまで使えるかもわからないAPIなので、利用するのは自己責任となる。
AndroidOSのオープン性とJavaのリフレクション機能の恩恵ともいえるが、今後はもっとセキュリティが強化されていくのは間違いないだろう。

grepcode.comなどでソースコードを検索すれば、多くのハイドメソッドに出会うことができる。

getPackageSizeInfoメソッド

PackageManagerの隠しメソッドにgetPackageSizeInfoというものがある。これを利用すると、各アプリのキャッシュサイズを得ることができる。

必要なパーミッション

Manifest.xmlには以下のパーミッションを指定する。

<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>

メソッドのインタフェース

public void getPackageSizeInfo(String packageName, IPackageStatsObserver observer)

このメソッドはEclipseから補完しようとしてもでてこない。これを利用するには、まずIPackageStatsOvserverインタフェースが必要となる。

こちらも含まれていないので、自力で書くのだが、ソースコードが公開されているので、それを持ってくればよいだろう。
このインタフェースはAIDLで書いてソースコードディレクトリに配置しておくと、./gen下に.javaファイルを生成してくれる。

AIDLはplatform_frameworks_baseから、得ればよいだろう。このAIDLをソースコードに追加すれば、このインタフェースが利用できるようになる。

PackageManagerのインスタンスから直接呼べないので、メソッドを取り出す。

      Method mGetPackageSizeInfoMethod = mPackageManager.getClass().getMethod("getPackageSizeInfo", String.class, IPackageStatsObserver.class);

後は、このメソッドを呼び出すだけである。

        mGetPackageSizeInfoMethod.invoke(mPackageManager, app.packageName, new IPackageStatsObserver.Stub() {   
            @Override  
            public void onGetStatsCompleted(PackageStats pStats, boolean succeeded) throws RemoteException {   
              if (pStats.cacheSize > 0) {
               Log.d(TAG, "cacheSize: " + pStats.packageName + "  " + pStats.cacheSize); 
              }
            }
         });

しかし、これを見るとわかるように非同期のコールバックを使うので、全アプリのキャッシュサイズを合計するには、一工夫必要であろう。

全アプリのキャッシュ削除

実は全アプリのキャッシュを消去する方が、もっと簡単だ。

同様にfreeStorage,freeStorageAndNotifyが存在する。なんで、CacheでなくStorageなんだという気がするが…

public void freeStorage(long freeStorageSize, IntentSender pi)
public void freeStorageAndNotify(long freeStorageSize, IPackageDataObserver observer);

AIDLを作成するのも面倒な人は、こんな方法もあるみたいだ。
How to get the exact size of cache directory : android

必要なパーミッション

Manifest.xmlには以下のパーミッションを指定する。

<uses-permission android:name="android.permission.CLEAR_APP_CACHE"/>

これに似たパーミッションとして、DELETE_CACHE_FILESがある。CLEAR_APP_CACHEはインストール中の全てのアプリのキャッシュを消去するパーミッションで、DELETE_CACHE_FILESは自アプリのキャッシュ消去のパーミッションである。

アプリごとにキャッシュをクリアするAPIもあるが、SecurityExceptionが発生して使えない。他アプリのキャッシュを個別に消去するパーミッションは存在しない。

ストレージの空き容量の計算

最後にキャッシュの消去の前と後でどれぐらいストレージが空いたか確認したいだろう。ストレージの空きはファイルシステムの空きで取得することができる。

    StatFs sf = new StatFs(Environment.getDataDirectory().getPath());
    long availableKBytes = (long)sf.getAvailableBlocks() * (long)sf.getBlockSize() / 1024l;
    long totalKBytes = (long)sf.getBlockCount () * (long)sf.getBlockSize() / 1024l;    

getBlockCount(),getBlockSize()はAPI Level18でdeprecatedされている。その代わりにgetBlockCountLong(),getBlockSizeLong()が用意されている。
getBlockCount(),getBlockSize()はint型で返すので、利用するなら、上記のようにlongにcastしておく方がよい。
昨今ではストレージ容量が大きくなっているので、計算結果がオーバフローしてしまうからだ。

adb shellコマンドを使って、dfを起動することでストレージ容量を得ることができるが、APIの値とは、多少誤差があるようだが、単位によるものだろうと推測する。

$ adb shell df /data
Filesystem             Size   Used   Free   Blksize
/data                   12G   681M    11G   4096

このdfコマンドは単位も指定できないし、はっきりしたことはわからないが、APIから得られる数値の方が正しいのではないかと推測している。また、設定などから得られる数値とも違っていることが多い。こちらも設定でどのような値を元に計算しているのかわからないので、なんともいえない。

アンドロイドのストレージやメモリに関する数値に関しては、歯切れが悪いことが多い(メモリはもっと複雑だ)。