読者です 読者をやめる 読者になる 読者になる

mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

Androidの機種依存問題を吸収するプロジェクトAndroid-Device-Compatibilityを公開したお話

Android compatibility Java library

こんにちは。Androidユニットで開発とスクラムマスターをしています、横幕です。すっかり寒くなって、朝起きるのが辛い季節になりました。

先日、Android(TM)の様々な機種に依存する問題を吸収するためのライブラリプロジェクトをmixi, IncのGitHubリポジトリで公開しました。

今回は、このライブラリプロジェクトを公開するに至った経緯をお話しようと思います。

様々な種類の端末に対応するために乗り越えてきた困難

現在、Androidを搭載した端末には、多種多様なものがあります。

そして、OSのバージョンごとの違いだけでなく、同じAndroidを搭載していても、端末ごとに微妙に挙動が異なることがあります。

mixi公式クライアントアプリでも、端末ごとに微妙に挙動が異なることで発生する問題にいくつか直面してきました。

特定の端末で、文字が9,000文字までしか入力できない

EditTextなど文字の入力を行うコンポーネントを使うときに、maxLengthの設定を何もしていないと起こります。maxLengthを設定していないと、特定の端末のOSが勝手に(!)maxLengthを9000に設定してしまうために発生します。

明示的にmaxLengthを設定しない場合は、内部的に-1が設定されるようになっているので、下記のようにXMLに書いてしまえば......と思いましたが、maxLengthに負数は設定できないため、この方法では解決できません。

<edittext android:layout_height="wrap_content" android:layout_width="fill_parent" android:maxlength="-1"></edittext>

どうしても制限したくない場合は、以下のように、@nullという値を設定して回避します(xmlの場合)。

<edittext android:layout_height="wrap_content" android:layout_width="fill_parent" android:maxlength="@null"></edittext>

mixi公式クライアントアプリでも、日記のように、10,000文字まで入力できるような長文の入力画面を作成している時にハマりました。

こちらのブログ記事で問題箇所を特定するに至り、無事に回避することができました。

特定の端末で、アプリのアンインストール後に再度インストールを行うと、設定が保存されなくなる

Androidでは、各アプリの設定情報などを保存するためのものとして、SharedPreferencesという仕組みが用意されています。

このSharedPreferencesの中の設定情報は、xmlファイルとして、以下のディレクトリの中に書き込まれます。

/data/data/$app_package_name/shared_prefs

通常、アプリをアンインストールすると、アプリ本体だけでなく、そのアプリだけで使っていた関連するファイルも削除されるため、SharedPreferencesのアプリごとのディレクトリも削除されます。

ところが、特定の端末では、SharedPreferencesの書き込み先が通常とは異なる場所に設定されています。

/dbdata/databases/$app_package_name/shared_prefs

そして悲しいことに、その端末でアプリのアンインストールを行っても、/dbdata/databasesの下にあるディレクトリは削除されません。

また、Androidでは、アプリケーションごとにUserIdが割り振られ、このUserIdに基いて、各ディレクトリやファイルへのアクセス権を管理しています。この仕組では、利用されなくなったUserIdが使い回されるようになっています。

このため、あるアプリをアンインストールしたあと、別のアプリをインストールすると、以前アンインストールしたアプリのUserIdが割当てられます。その後もう一度アンインストールしたアプリをインストールしなおすと、新しいUserIdが割当てられます。

このような状況になると、あるアプリをアンインストールしたあとも残り続ける/dbdata/databases以下のディレクトリのアクセス権は別のアプリのものとなるため、再インストールしたあとでも、自分のパッケージのディレクトリであるにもかかわらずアクセスできなくなってしまいます。

アクセス出来なくなってしまうので、当然設定を読み書きすることも、削除することもできなくなります。

権限がなくなってしまった以上はもうどうしようもないので、mixi公式クライアントアプリでは、権限がなくなったことを検知すると、/data/data以下のディレクトリに書き込むようにする回避策をとっています。

標準カメラで写真を撮影した後の、保存したデータの取得方法が端末ごとに異なる

Intentを使ってカメラを呼び出し、標準の(デフォルトでインストールされている)カメラアプリで写真を撮影した時、そのデータを取り出す方法が端末によって異なります。

標準のカメラアプリで撮影した写真データの取得方法は、様々なブログ記事などで紹介されています。

特定の端末でGsonライブラリを利用するとVerifyErrorでクラッシュしてしまう

Gsonライブラリは、JSON文字列とJavaのオブジェクトとの変換を、自分でJSON文字列をパースする処理を書かなくても実行してくれる便利なライブラリです。

とても便利なライブラリなので、mixi公式クライアントアプリでも利用していますし、各種端末でも利用されているようです。

しかし特定の端末では、以下のようなGsonライブラリを使った処理を記述しているクラスをロードすると、VerifyErrorでクラッシュしてしまいます。

package com.example.gsoncrash;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

public class MainActivity extends Activity {
    private static final String TAG = MainActivity.class.getSimpleName();
    private static final Gson GSON = new Gson();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        try {
            Entity entity = GSON.fromJson("{ \"hoge\" : \"hoge\" }", Entity.class);
            Log.v(TAG, entity.hoge);
        } catch (JsonSyntaxException e) {
            
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }

    private static class Entity {
        public String hoge;
    }
}

gson_crash_log.png

ログを見ると、java.lang.Class.newInstance()のなかでVerifyErrorとなってクラッシュしている事がわかりました。さらにログを追いかけると、dalvikVMが、Gsonライブラリのクラスのリンクに失敗したというログを吐いていました。

gson_verifying_log.png

この問題は、特定の端末で使っているGsonライブラリへのクラスパスを、各アプリをロードするクラスローダが知っているために、アプリが使おうとしているGsonライブラリとの名前空間が衝突して起こる問題でした(詳細はこちらで報告されています)。このため、Gsonを利用しているアプリをインストールすると、上記のキャプチャ画像のようなログが出力されます。また、この現象が発生している端末では、最新のGoogle Playも上手く動作しなくなるようです(アプリがインストールできなくなる)。

よって、回避するためには、各アプリで使うGsonライブラリの名前空間を無理やり変える必要があります。

特定の端末でネイティブコードがクラッシュしプロセスごとkillされてしまうことがある

こちらはまだ原因を究明中の問題ですが、最近発売された端末で、WebViewや通常のブラウザ等で、特定の条件がそろっているページを表示すると、WebKitのネイティブコード(libwebcore)でバグを踏んでしまい、セグメンテーション違反でプロセスごとkillされてしまうという問題が確認されています(何らかのレンダリング処理でクラッシュしているようです)。

他の端末では全く問題なく動作するため、その端末独自の拡張がWebKitのコードに施されていると思われますが、こちらはネイティブコードでSIGSEGVとなっているため、アプリがクラッシュした時に表示されるダイアログも表示されませんし、当然Google Playのクラッシュレポートにも上がらず、WebViewで表示しているアプリやブラウザが何も言わずに(!)閉じてしまいます。

ExifInterfaceの初期化でプロセスごとkillされてしまうことがある

OSのバージョンによってログに出てくる内容が異なりますが、ExifInterfaceのコンストラクタ引数(ファイルのパス名)にnullを渡すとSIGSEGVでプロセスごとkillされます。

package com.example.exif;

import java.io.IOException;

import android.app.Activity;
import android.media.ExifInterface;
import android.os.Bundle;

public class MainActivity extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		try {
			ExifInterface exif = new ExifInterface(null);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

android-compat-jni-crash-exif.png

こちらも、アプリが何も言わずに閉じてしまうため、初期化の前にnullチェックをする必要があります。

機種依存問題のノウハウを共有したい

このような問題は、自分たちだけでなく、他のアプリ開発をしている人たちにも起こり得るものだと思います。

特に、個人で開発をしているような場合だと、すべての端末で動作検証を行うのは現実的に不可能なことだと思います。

また、OSのアップデートでこれらの問題を解決しようとしても、多くの端末にそのアップデートが行き渡るまでには時間がかかりますし、すべての端末に完全に行き渡らせることは保証できません。

そこで、この端末だとうまくいかない、といったノウハウを一箇所で集めて共有できる場所があると良いのではないかという思いで、Androidの機種依存問題を吸収するプロジェクトをGitHubで公開することとなりました。

これがmixi-inc/Android-Device-Compatibilityです。

現在は、Intentでカメラを起動した時のデータ取得部分の依存問題を吸収する部分と、SharedPreferencesのアクセス権に関する問題の回避策に関する部分などがプロジェクトに含まれています。今後もこのような問題解決のために、プロジェクトの内容を充実させていく予定です。

このプロジェクトが、多くのAndroid開発者の助けになれば幸いです。