2015年8月11日火曜日

[安藤]android studio 1.3 with JNI(NDK) 初構除錯筆記

2017/07 補充:
本篇文章的gradle相關的修改,在android studio 2.3以後的版本已不需要。
請參考這篇:http://gigamine.blogspot.tw/2017/07/android-studio-23-with-jnicmake.html

===================================================================

跌跌撞撞了一整天之後總算讓JNI通了。沒啥可以參考的資料,也只能一直不斷的試誤。

作業環境:OSX with MBP. windows不保證通用。
事前準備:
  • File->Project Structure->SDK Location 檢查Android NDK Location是不是已經有路徑了。沒有的話,下面有一行「Download NDK」直接點下去,android studio就會幫你裝好NDK並補上路徑。
  • java的版本似乎影響不大。有人建議用1.7,不過我用1.6也可以成功。

接下來就是一連串的改寫:

首先改寫[project root]/gradle/wrapper/gradle-wrapper.properties
(改動的部分以紅色標記):

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip

請注意,只改了最後一行的紅色部分:distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip
因為JNI的支援必須要使用gradle 2.5以後的版本來build。



接著改寫[project root]/build.gradle(改動的部分以紅色標記):
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle-experimental:0.2.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
    gradle.projectsEvaluated {
        tasks.withType(JavaCompile) {
            options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
        }
    }
}

支援NDK的plugin被列為experimental。所以得把之前在用的1.2.3版改成這個plugin。



再改寫[project root]/[main module]/build.gradle:
因為experimental版的gradle語法跟舊的有不小的差距,因此會超大改。
apply plugin: 'com.android.model.application' //lib的話就是 com.android.model.library
model { // 之前的android區塊外面必須加上model區塊包住。
    android {
        compileSdkVersion = 19  //區塊內的所有參數都要加上等號。
        buildToolsVersion = "22.0.1"
        defaultConfig.with {  //掛在android 階層下的參數區塊都要加上".with"。但是第二層以下又不用加...    「.with」不加的話,gradle sync時會出現例外「com.android.build.gradle.managed.AndroidConfig_Impl」
            applicationId = 'com.hello.hellojni'
            minSdkVersion.apiLevel = 15
            targetSdkVersion.apiLevel = 19
            versionCode = 1
            versionName = '1.00.00.00'
            multiDexEnabled = true

        }

        buildTypes.with {
            debug {  //不用加".with"。 要是加了的話,gradle sync時會出現例外「Error:Attempt to read a write only view of model of typeorg.gradle.model.ModelMap」
                debuggable = true
            }
            release {
                proguardFiles += file('proguard-rules.pro')  //proguard的設定要這樣改。
            }
        }
    }



    android.productFlavors {  //擺在"android"tag的外面只是為了展示也可以這樣加gradle指令。
        create("flavor1") { //flavor的名稱必須被「create」給包住。
            minSdkVersion.apiLevel = 15 //.apilevel為必須指令。否則gradle編譯時會出現例外「com.android.build.gradle.managed.ProductFlavor_Impl」
            applicationId = 'com.hello.hellojni.flavor1'
            targetSdkVersion.apiLevel = 19
            versionCode = 0
            versionName = '0.00.00.00'
        }
        create("flavor2") {
            minSdkVersion.apiLevel = 15
            applicationId = 'com.hello.hellojni.flavor2'
            targetSdkVersion.apiLevel = 19
            versionCode = 0
            versionName = '0.00.00.00'
        }
    }

    android.ndk {  //新參數。modulename將會是native c code編譯出來的.so檔案的主檔名。在此例,編譯成功之後將會輸出「libapp.so」的檔案。
        moduleName = "app"
    }

    android.packagingOptions { //這個參數下面的指令皆不需要"="。規則有點雜亂...
        exclude 'META-INF/DEPENDENCIES'
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/NOTICE.txt'
    }
    android.lintOptions {
        ignoreWarnings = true
        disable += 'MissingTranslation'    //disable的設定要這樣改。
    }


    android.signingConfigs {  //signingConfig不支援。
        config {
            keyPassword = '123456'
            storeFile = file('chacha.keystore')
            storePassword = '123456'
            keyAlias = 'chacha.keystore'
        }
    }

}

dependencies {  //model區塊之外的語法都照舊。
    compile project(':project1')
    compile project(':project2')
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'junit:junit:4.12'  //androidTestCompile / TestCompile指令皆不支援。所以測試的部分要再想辦法...
}



到此,gradle sync沒有出現錯誤的話,恭喜你已經成功一半。
接下來就是加c程式碼然後讓java能夠抓到c的function。

在[project root]/src/main 加上一個新的目錄「jni」。
在裡面新建一個檔名「hellojni.cpp」(主檔名可任意取,無任何關聯性。副檔名一定要是.c或是.cpp)

#include <string.h>
#include <jni.h>

/**
 * 函式命名規則: "Java_" + [Java的packagename(只允許英文小寫)把點換成底線]
 * + "_" + java程式碼裡面call此函式的class name(注意大小寫) + "_" + 函式名稱.
 * 第一個參數是執行此函式的整個java的task環境。可以用來取得java class的變數
 *    ,static class,function pointer...
 * 第二個參數,若是該函式在java 裡面的定義為static method, 這裡要改成 「jclass」。
 * 第三個參數以後的就是java端依序定義的傳入資料。
 */

extern "C" {  //一定要加. 要不然java會找不到函式,一直跳 Native method not found exception.
    JNIEXPORT jstring JNICALL Java_com_hello_hellojni_TestActicity_stringFromJNI(JNIEnv *env, jobject instance)     {

        return env->NewStringUTF("Hello from JNI ! ");  //切記,函式宣告有回傳資料的話,一定要回傳。不回傳的話編譯器不會告訴你錯誤,只有在執行的時候會送你SIGABRT 6...
    }
}


對應的Java caller:
package com.hello.hellojni;

import android.app.Activity;
import android.widget.TextView;
import android.os.Bundle;


public class TestActicity extends Activity
{
    static // 在使用JNI的函式之前必須使用System.loadLibrary把lib讀進來。
    {
        System.loadLibrary("app");  //對應android.ndk裡面的「modulename」參數。
    }
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        /* Create a TextView and set its content.
         * the text is retrieved by calling a native
         * function.
         */
        TextView  tv = new TextView(this);
        tv.setText( stringFromJNI() );
        setContentView(tv);
    }

    /* A native method that is implemented by the
     * 'hellojni' native library, which is packaged
     * with this application.
     */
    public native String  stringFromJNI();  //宣告使用JNI過來的函式的時候要加上「native」。
}


參考文件:
http://tools.android.com/tech-docs/new-build-system/gradle-experimental
https://github.com/googlesamples/android-ndk
http://qiita.com/eaglesakura/items/c4af7989b03904d66ebe
http://ph0b.com/new-android-studio-ndk-support/
http://www.slideshare.net/ph0b/mastering-the-ndk-with-android-studio-and-the-gradleexperimental-plugin

補充:
後來做系統測試的時候,發現這樣的編譯環境在使用context.obtainStyledAttributes()之後導致
NoClassDefFoundError: [package name].R$styleable 出現。
看來這個編譯環境似乎不太穩定,有必要把所使用的app的所有功能都做過較詳細的測試,以策安全。


16/01/06 補充:

目前的gradle-experimental最新版本為0.6.0-alpha1。前一版是0.4。
對應的gradle distributon分別為2.9跟2.8。
需要用新版的話,請對應做修改。

=====================0.4.0==========================

classpath 'com.android.tools.build:gradle-experimental:0.4.0'

distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip


=====================0.6.0-alpha1==========================

classpath 'com.android.tools.build:gradle-experimental:0.6.0-alpha1'

distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip

使用jdk7來編譯是不會卡死了,但是之前使用
    android.packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/NOTICE.txt'
    }
解決apache 裡面的幾個jar的duplicatefileexception的方法變成無效。
目前尚無解決方案。(猜測是不打算修了要硬逼大家回去用httpurlConnection)

另外一個衝擊是,gradle script的“+="指令失效。
proguardFiles += file('proguard-rules.pro')
必須修正為
proguardFiles.add(file('proguard-rules.pro'))


16/03/29 補充:
切記,函式宣告有回傳資料的話,一定要回傳資料。不回傳的話,編譯器不會告訴你錯誤,只有在執行的時候會送你SIGABRT 6...