AndroidとiOSプラットフォームのクラッシュキャプチャと収集


クラッシュキャプチャと収集により,パブリッシュされたアプリケーション(ゲーム)の異常を収集することができ,開発者がバグを発見し修正することができ,ソフトウェアの品質向上に大きく役立つ.iOSとandroidプラットフォームの下でのクラッシュキャプチャと収集の原理と手順を紹介したが、個人開発アプリケーションや特別な制限がなければ、下を見る必要はなく、友盟sdk(統計分析sdk)を直接工事に組み込むと万事順調であり、その中のエラーログ機能は完全に需要を満たすことができ、受信サーバを追加する必要はない.しかし、その原理にもっと興味があるか、私のように会社の既存のバグ収集システムと互換性を持たなければならないなら、次のものは見る価値があります.
クラッシュのキャプチャと収集を実現するには、主にいくつかの困難があります.
1、どのようにクラッシュをキャプチャするか(例えばc++のよくある野ポインタエラーやメモリの読み書き限界、これらの状況が発生した場合、プログラムは異常に終了したのではないでしょうか.私たちはどのようにキャプチャしますか)
2、スタック情報の取得方法(クラッシュがどの関数であるか、さらには数行目で発生しているかを教えてくれ、問題を再現して修正することができます)
3、指定したサーバーにエラーログをアップロードする(これが一番いい)
まず簡単な総説を行います.クラッシュを引き起こすコードは本質的に2種類あり、1つはc++言語レベルのエラーであり、例えば野ポインタ、ゼロ除去、メモリアクセス異常などである.もう1つは未キャプチャ例外(Uncautor Exception)で、iOSの下で最も一般的なのはobjective-cのNSException(@throwで投げ出す、例えばNSArrayアクセス要素が境界を越えている)、androidの下にjavaが投げ出す異常があります.これらの異常が最上階tryに住んでいなければ,プログラムはクラッシュする.iOSでもandroidシステムでも、その下層はunixまたはクラスunixシステムであり、第1の言語レベルのエラーに対しては、信号メカニズムによってキャプチャ(signalまたはsigaction、qtの信号スロットと混同しないでください)することができます.つまり、どのシステムエラーもエラー信号を放出し、コールバック関数を設定し、コールバック関数にエラーログを印刷して送信することができます.
一、iOSプラットフォームのクラッシュのキャプチャと収集
1、オープンクラッシュキャプチャの設定
static int s_fatal_signals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGSEGV,
    SIGTRAP,
	SIGTERM,
	SIGKILL,
};

static const char* s_fatal_signal_names[] = {
	"SIGABRT",
	"SIGBUS",
	"SIGFPE",
	"SIGILL",
	"SIGSEGV",
	"SIGTRAP",
	"SIGTERM",
	"SIGKILL",
};

static int s_fatal_signal_num = sizeof(s_fatal_signals) / sizeof(s_fatal_signals[0]);

void InitCrashReport()
{
        // 1     linux      
	for (int i = 0; i < s_fatal_signal_num; ++i) {
		signal(s_fatal_signals[i], SignalHandler);
	}
	
        // 2      objective-c        
	NSSetUncaughtExceptionHandler(&HandleException);
}

ゲームの最初にInitCrashReport()関数を呼び出してクラッシュキャプチャを開始します.注記1では前述の第1クラスのクラッシュに対応し、注記2ではobjective-c(またはUImit Framework)に対応して放出されるが処理されない異常に対応する.
2、スタック情報の印刷
+ (NSArray *)backtrace
{
	void* callstack[128];
	int frames = backtrace(callstack, 128);
	char **strs = backtrace_symbols(callstack, frames);
	
	int i;
	NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
	for (i = kSkipAddressCount;
		 i < __min(kSkipAddressCount + kReportAddressCount, frames);
		 ++i) {
	 	[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
	}
	free(strs);
	
	return backtrace;
}

幸い、アップルのiOSシステムはbacktraceをサポートしており、この関数でプログラムクラッシュの呼び出しスタックを直接印刷することができます.利点は、シンボル関数テーブルを必要とせず、パブリッシュされた対応バージョンを保存したり、クラッシュスタックを直接表示したりする必要がないことです.欠点は,どの行がクラッシュしたのかを具体的に印刷できず,どの関数がクラッシュしたのかを知る問題が多いが,クラッシュしたのは何なのかは調べられないことである.
3、ログのアップロード、これは実際の需要を見る必要があります.例えば、うちの会社はクラッシュ情報http postをphpサーバーに送ります.ここではあまり声明をしません.
4、テクニック---クラッシュ後、プログラムは終了せずに実行状態を保つ
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
	CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
	
	while (!dismissed)
	{
		for (NSString *mode in (__bridge NSArray *)allModes)
		{
			CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.001, false);
		}
	}
	
	CFRelease(allModes);

クラッシュ処理関数がログ情報をアップロードした後、上記のコードを呼び出し、プログラムのメインループを再構築できます.これにより、プログラムはクラッシュしても正常に動作する(もちろん、この時点では不安定な状態であるが、ハンドヘルドゲームやアプリケーションの多くは短期操作であるため、ハングアップという言い方はないので、安定するかどうかは関係ない).プレイヤーは崩壊さえ感じない.
ここで一つの感想を説明します.それは「再入可能(reentrant)」です.簡単に言えば、私たちのクラッシュコール関数が再入力可能である場合、再びクラッシュが発生した場合、この新しい関数を正常に実行することができます.しかし再入不可であれば運転できない(この時点では徹底的に死んでしまう).上記の効果を実現するには、コールバック関数が再入力可能であることを保証する必要があります.だから、私がテストした結果、objective-cの異常トリガは何度でも正常に動作します.しかし、エラー信号が何度もトリガーされると、プログラムが詰まってしまいます.だから、このテクニックを適用するかどうかを慎重に決めなければなりません.
二、androidクラッシュ捕獲と収集
1、androidクラッシュキャプチャを開く
まずjavaコードのクラッシュキャプチャで、これは一番下の完全なコードに倣ってUncaughtExceptionHandlerを書くことができて、それからすべてのActivityのonCreate関数で最初にThreadを呼び出します.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(this));
これにより、クラッシュが発生すると、UncaughtExceptionHandlerのpublic void uncaughtException(Thread thread,Throwable exception)関数が自動的に呼び出され、その中のexceptionにはスタック情報が含まれており、この関数に必要な情報を印刷し、エラーログをアップロードすることができます.
次に、jniのc++コードがどのようにクラッシュキャプチャされるかを重視します.
void InitCrashReport()
{
	CCLOG("InitCrashReport");

    // Try to catch crashes...
    struct sigaction handler;
    memset(&handler, 0, sizeof(struct sigaction));

    handler.sa_sigaction = android_sigaction;
    handler.sa_flags = SA_RESETHAND;

#define CATCHSIG(X) sigaction(X, &handler, &old_sa[X])
    CATCHSIG(SIGILL);
    CATCHSIG(SIGABRT);
    CATCHSIG(SIGBUS);
    CATCHSIG(SIGFPE);
    CATCHSIG(SIGSEGV);
    CATCHSIG(SIGSTKFLT);
    CATCHSIG(SIGPIPE);
}
singalの設定により、クラッシュが発生するとandroid_が呼び出されます.Sigaction関数.これもlinuxの信号メカニズムです.ここで信号コールバック関数を設定するコードはiOSとは少し異なり、これは同じ機能の2つの異なる書き方にすぎず、本質的な違いはありません.興味があるのはgoogleで両者の違いを決めることができます.
2、印刷スタック
JAva構文はexceptionでスタック情報を直接取得できますが、jniコードはbacktraceをサポートしていません.では、スタック情報をどのように取得しますか?ここで私が試したい新しい方法は、google breakpadを使用することです.今は完全にプラットフォームをまたいでいるようです(windows、mac、linux、iOS、androidなどをサポートしています).それは自分でminidumpを実現し、androidの上で制限が小さくなります.しかし、このライブラリは少し大きく、私たちの工事に追加するのは容易ではないと思います.そのため、簡潔な「伝統」案を使用しました.考え方は,クラッシュが発生したとき,Activityで書いた静的関数をコールバック関数で呼び出すことである.この関数ではコマンドを実行することでlogcatの出力情報(出力情報にはjniのクラッシュアドレスが含まれている)を取得し、このクラッシュ情報をアップロードします.クラッシュ情報を取得すると、arm-linux-androideabi-addr 2 line(具体的にはこの名前ではないかもしれませんが、android ndkで*addr 2 lineを検索し、実際のプログラムを見つける)でクラッシュ情報を解析できます.
jniのクラッシュコールバック関数は次のとおりです.
void android_sigaction(int signal, siginfo_t *info, void *reserved)
{
	if (!g_env)	{
		return;
	}

    jclass classID = g_env->FindClass(CLASS_NAME);
    if (!classID) {
    	return;
    }

    jmethodID methodID = g_env->GetStaticMethodID(classID, "onNativeCrashed", "()V");
    if (!methodID) {
        return;
    }

    g_env->CallStaticVoidMethod(classID, methodID);

    old_sa[signal].sa_handler(signal);
}

jniによってjavaの関数を呼び出しただけで、すべての処理がjavaレベルで完了していることがわかります.
JAvaに対応する関数は以下のように実現されます.
public static void onNativeCrashed() {
        // http://stackoverflow.com/questions/1083154/how-can-i-catch-sigsegv-segmentation-fault-and-get-a-stack-trace-under-jni-on-a
		Log.e("handller", "handle");
        new RuntimeException("crashed here (native trace should follow after the Java trace)").printStackTrace();
        s_instance.startActivity(new Intent(s_instance, CrashHandler.class));
    }

jniがクラッシュしたとき、元のactivityが終わった可能性があるため、新しいactivityを開きました.この新しいactivityは次のように実現されます.
public class CrashHandler extends Activity
{
    public static final String TAG = "CrashHandler";
    protected void onCreate(Bundle state)
    {
        super.onCreate(state);
        setTitle(R.string.crash_title);
        setContentView(R.layout.crashhandler);
        TextView v = (TextView)findViewById(R.id.crashText);
        v.setText(MessageFormat.format(getString(R.string.crashed), getString(R.string.app_name)));
        final Button b = (Button)findViewById(R.id.report),
              c = (Button)findViewById(R.id.close);
        b.setOnClickListener(new View.OnClickListener(){
            public void onClick(View v){
                final ProgressDialog progress = new ProgressDialog(CrashHandler.this);
                progress.setMessage(getString(R.string.getting_log));
                progress.setIndeterminate(true);
                progress.setCancelable(false);
                progress.show();
                final AsyncTask task = new LogTask(CrashHandler.this, progress).execute();
                b.postDelayed(new Runnable(){
                    public void run(){
                        if (task.getStatus() == AsyncTask.Status.FINISHED)
                            return;
                        // It's probably one of these devices where some fool broke logcat.
                        progress.dismiss();
                        task.cancel(true);
                        new AlertDialog.Builder(CrashHandler.this)
                            .setMessage(MessageFormat.format(getString(R.string.get_log_failed), getString(R.string.author_email)))
                            .setCancelable(true)
                            .setIcon(android.R.drawable.ic_dialog_alert)
                            .show();
                    }}, 3000);
            }});
        c.setOnClickListener(new View.OnClickListener(){
            public void onClick(View v){
                finish();
            }});
    }

    static String getVersion(Context c)
    {
        try {
            return c.getPackageManager().getPackageInfo(c.getPackageName(),0).versionName;
        } catch(Exception e) {
            return c.getString(R.string.unknown_version);
        }
    }
}

class LogTask extends AsyncTask
{
    Activity activity;
    String logText;
    Process process;
    ProgressDialog progress; 

    LogTask(Activity a, ProgressDialog p) {
        activity = a;
        progress = p;
    }

    @Override
    protected Void doInBackground(Void... v) {
        try {
        	Log.e("crash", "doInBackground begin");
            process = Runtime.getRuntime().exec(new String[]{"logcat","-d","-t","500","-v","threadtime"});
            logText = UncaughtExceptionHandler.readFromLogcat(process.getInputStream());
        	Log.e("crash", "doInBackground end");
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(activity, e.toString(), Toast.LENGTH_LONG).show();
        }
        return null;
    }

    @Override
    protected void onCancelled() {
    	Log.e("crash", "onCancelled");
        process.destroy();
    }

    @Override
    protected void onPostExecute(Void v) {
    	Log.e("crash", "onPostExecute");
        progress.setMessage(activity.getString(R.string.starting_email));
        UncaughtExceptionHandler.sendLog(logText, activity);
        progress.dismiss();
        activity.finish();
        Log.e("crash", "onPostExecute over");
    }

最も主要な点はdoInBackground関数であり,この関数はlogcatによってクラッシュ情報を取得した.AndroidManifestでxml LOGを読み込む権限を追加

3、エラーログを取得したら、sdカード(同様に権限の追加を忘れないでください)に書くか、アップロードすることができます.コードは簡単にgoogleに着いて、あまり言わないでください.最後に、このエラーログをどのように解析するかについてお話しします.
取得したエラー・ログでは、次の情報を切り取ることができます.
12-12 20:41:31.807 24206 24206 I DEBUG   : 
12-12 20:41:31.847 24206 24206 I DEBUG   :          #00  pc 004931f8  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
12-12 20:41:31.847 24206 24206 I DEBUG   :          #01  pc 005b3a5e  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
12-12 20:41:31.847 24206 24206 I DEBUG   :          #02  pc 005aab68  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
12-12 20:41:31.847 24206 24206 I DEBUG   :          #03  pc 005ad8aa  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
12-12 20:41:31.847 24206 24206 I DEBUG   :          #04  pc 005924a4  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
12-12 20:41:31.847 24206 24206 I DEBUG   :          #05  pc 005929b6  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
004931f8
これが私たちの崩壊関数のアドレスです.libhelloworld.soはクラッシュしたダイナミックライブラリです.このダイナミックライブラリをaddr 2 lineを使用して解析します(obj/localディレクトリの下の方が大きい場合、シンボルファイルを含むダイナミックライブラリは、Libsディレクトリの下の方が小さいのではなく、バージョンをリリースする場合は、このダイナミックライブラリも保存し、後でログを調べるには対応するダイナミックライブラリが必要です).コマンドは次のとおりです.
arm-linux-androideabi-addr2line.exe-eダイナミックライブラリ名クラッシュアドレス
例:
$ /cygdrive/d/devandroid/android-ndk-r8c-windows/android-ndk-r8c/toolchains/arm-linux-androideabi-4.6/prebuilt/windows/bin/arm-linux-androideabi-addr2line.exe -e obj/local/armeabi-v7a/libhelloworld.so 004931f8
で得られた結果は、どのcppファイルが何行目にクラッシュしたかです.ダイナミックライブラリの情報が間違っている場合は、次のように返されます.0