芸軌社株式会社
RSS 2.0

芸軌社公式ブログ

2012年12月11日(火)20:19(+0900)

NativeActivityからのJavaの呼び出し[Android]

このエントリーをはてなブックマークに追加

ここ2日間くらい、AndroidでNativeActivityから自作Javaクラスのメソッドが呼び出せずに悩んでいましたが、無事解決できたのでNativeActivityのCのコードからJavaを呼び出すサンプルを載せます。

Javaクラスの作成

まずは、EclipseでAndroid NDKのサンプルからNativeActivityのプロジェクトを作ります。

Android NDKのサンプルからNativeActivityのプロジェクトを読み込み
Android NDKのサンプルからNativeActivityのプロジェクトを読み込み

NativeActivity/srcフォルダにandroid.app.NativeActivityを継承して適当な名前のクラス(ここでは"TestJNI")を作ります。

NativeActivityを継承してクラスを作成
NativeActivityを継承してクラスを作成

ここに使いたいコードを書いていきます。

// TestJNI.java

package com.example.native_activity;

import android.app.Activity;



public class TestJNI extends NativeActivity{
	
	static {
		System.loadLibrary("native-activity");
	}
	
	private static final String FILE_NAME = "PreferenceSampleFIle";

	public void TestPreference() {
		SharedPreferences preference = getSharedPreferences(FILE_NAME, MODE_PRIVATE);
		SharedPreferences.Editor editor = preference.edit();
		editor.putString("DAT", "abc");
		editor.commit();
	}
} 

TestJNIクラス内ではnative-activityライブラリをロードしておく必要があります。これをしないとJava内のメソッドがCから呼び出せません。(これはJavaからCを呼び出す場合にも必要で、そっちのほうが必要性は素直に理解できるが、逆もまた真なりとでも言うべきか今の場合も必須な模様。)

ここでは、プリファレンスに適当なデータを保存するTestPreference()というメソッドを作っています。なお、ToastはNativeActivityでは使えません。

Javaを呼び出すネイティブ関数の作成

次に、NativeActivity/jni内にCからJavaを呼び出す関数(ここでは"jniTest")を作ります。

// jnitest.h

void jniTest(struct android_app* state);
// jnitest.c

#include <android_native_app_glue.h>

void jniTest(struct android_app* state)
{
	JNIEnv* env;
	JavaVM* vm = state->activity->vm;
	(*vm)->AttachCurrentThread(vm, &env, NULL);
	jobject thiz = state->activity->clazz;

	jclass test = (*env)->GetObjectClass(env, thiz);
	jmethodID methodj = (*env)->GetMethodID(env, test,
		"TestPreference", "()V");

	(*env)->CallVoidMethod(env, thiz, methodj);

	(*vm)->DetachCurrentThread(vm);
}

ここで、jniTestの引数には、android_mainの引数のstruct android_app* stateを持ってきます。stateからJavaVM*が取得でき、これからAttachCurrentThreadでJNIEnv*が取得できます。また、jobjectもstateから取得でき、これでJavaからCを呼び出す時に渡される引数が得られます。

次にメソッドの呼び出しです。TestJNIクラスはGetObjectClassで呼び出すことができ、得られたクラスからGetMethodIDで必要なメソッド(TestPreference)が得られます。メソッドが得られたら、今の場合は戻り値が無いのでCallVoidMethodを用いてメソッドを実行します。

メソッドを実行した後は、DetachCurrentThreadを実行します。これをしておかないと、下手をすると忘れた頃にオーバーフローでアプリが落ちます。

以上の関数をmain.cの適当な場所に組み込みます。

/* 
 * 前略
 */

//BEGIN_INCLUDE(all)
#include <jni.h>
#include <errno.h>

#include <EGL/egl.h>
#include <GLES/gl.h>

#include <android/sensor.h>
#include <android/log.h>
#include <android_native_app_glue.h>

// ヘッダをインクルード
#include "jnitest.h"

/*
 * 中略
 */

static void engine_draw_frame(struct engine* engine) {
    if (engine->display == NULL) {
        // No display.
        return;
    }

    jniTest(engine->app);

/*
 * 後略
 */

ここでは、とりあえずengine_draw_frame内に置くことにします。

Android.mkのソースにjnitest.cを加え、

# Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := native-activity
LOCAL_SRC_FILES := main.c jnitest.c
LOCAL_LDLIBS    := -llog -landroid -lEGL -lGLESv1_CM
LOCAL_STATIC_LIBRARIES := android_native_app_glue

include $(BUILD_SHARED_LIBRARY)

$(call import-module,android/native_app_glue)

ndk-buildをすればコンパイルできます。

AndroidManifestの修正

最後に、AndroidManifest.xmlを修正します。

<?xml version="1.0" encoding="utf-8"?>
<!-- BEGIN_INCLUDE(manifest) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.native_activity"
        android:versionCode="1"
        android:versionName="1.0">

    <!-- This is the platform API where NativeActivity was introduced. -->
    <uses-sdk android:minSdkVersion="9" />

    <!-- This .apk has no Java code itself, so set hasCode to false. -->
    <application android:label="@string/app_name" android:hasCode="true">

        <!-- Our activity is the built-in NativeActivity framework class.
             This will take care of integrating with our NDK code. -->
        <activity android:name=".TestJNI"
                ndroid:label="@string/app_name"
                android:configChanges="orientation|keyboardHidden">
            <!-- Tell NativeActivity the name of or .so -->
            <meta-data android:name="android.app.lib_name"
                    android:value="native-activity" />
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
<!-- END_INCLUDE(manifest) -->

Javaを使っているので、applicationのandroid:hasCodeをtrueにします(android:hasCode自体を削除してもOKです)。activityのandroid.nameは、NativeActivityを使わない場合と同様の.クラス名(今の場合は".TestJNI")にします。

動作の確認

以上を実行し、エミュレータ上でプログラムが動くことを確認します。

エミュレータ上でプログラムが動くことを確認
エミュレータ上でプログラムが動くことを確認

ddmsを起動し、デバイス選択を選択してDevice→File Explorerを起動、/data/data/パッケージ名/shared_prefs以下にファイル(PreferenceSampleFile.xml)が生成されていることを確認します。

PreferenceSampleFile.xmlが生成されている
PreferenceSampleFile.xmlが生成されている

なお、今の場合はengine_draw_frame内でプリファレンスに書き込んでいるので、プログラム起動中はPreferenceSampleFile.xmlは削除してもすぐに生成されます(常に新しいものが生成され続けています)。

最後に、プリファレンスの中身を見てみます。

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="DAT">abc</string>
</map>

TestJNI.javaのTestPreference()で書いたものに相当する内容になっていることが確認できます。