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...

2015年6月11日木曜日

[日本旅遊]2015-01-03 大井川鐵道(下)

續。

買了便當,卻沒有設想該在哪裡用餐。坐在這板凳上吃,戶外的氣溫有點低... (刺骨等級)
而且這裡是山區,比平地更冷... (抖抖抖)




左看看右看看。鐵軌旁的路是之後要走到下一站的通路。




候車亭。

現充專用鐘。 (戴上太陽眼鏡)

舊井川線的特寫。

先好好的享受湖上風光吧。


抬頭一開,才發現原來有個更大的候車亭... 看來是用餐的好地方。

所以就在這裡用餐啦。


蠻微妙的便當。不過這些菜色就算是冰的也OK是真的。
裡面讓我比較不能適應的是左下角的紫蘇山藥...酸得一塌糊塗XD
在便當裡面放蛋糕也是第一次看到...


吃到一半,下山的車子出現。來拍個幾張。




在這裡含用餐洗手,停留約1小時。繼續移動。
 



走這條橋還是怕怕的...

回頭一拍。

接下來要上階梯。

離車站越來越遠。





上階梯之後,面對的是步行山道。因為還蠻陡的,需要扶手,有點後悔沒帶手套。
這是往右側看的狀況。


先往左走,才能到奧大井湖上站的最佳攝影地點。

只是旁邊的樹擋了不少視野。現在可以體會到為什麼攝影廚會想要鋸樹了...
我也很想鋸。

怎麼喬都沒辦法全收... Orz

變焦不夠長~~~


逆光逆光...

偏光鏡用力喬... 還好有喬到比較ok的角度。

亂喬一陣拍完往回走步道,剛好遇到下山的車。後來才發現其實不用再走步道,沿著公路即可...

途中的鐵軌就是上一張圖的同一段。上一張圖是站在這張圖的右側的樹林裡的步道拍的...





沿著縣道388號前進。

這邊感覺也是還不錯的攝影點。


看到一座吊橋。橋的另一邊感覺上只有步道...



走到底就是溫泉啦

除了大吊橋之外,對面還有五座小的吊橋。

路的左側是一個還不小的停車場。照片右端的亭子是洗手間,不過冬天沒水可用也就不能大號... Orz
這時慶幸在剛剛用餐的時候有上過洗手間,要是突然想上卻不能用,真的是要人命...

圖上的輕型車剛好可以用來當比例尺... 這停車場還不小。

停車場旁的一個小神社。門前有綁線就不踩進去了。

從神社往右看,也可以看到到大吊橋。

比較喜歡沿著河岸走。一方面是不知道步道能不能到達目的地...

圖上又可以看到兩座吊橋。

冬天全枯了... 感覺上夏天的時節會是草地。

泡湯的時候到了。要是又沒開就...Orz




從公路橋上拍。


這時發現只要看到自販機就有一種安心感...
啊,原來這裡還不夠鄉野


今天的目的地。看到「營業中」的旗子就更安心了...
這裏的泉質跟鬼怒川一樣是碳酸泉。不過非常滑,滑到走路的時候會滑倒...
剛泡過弱弱的鬼怒川,這裏的泉質我已經很滿意了。只要400隻羊。

準備搭車回東京。回程的車輛已經在月台上準備好了。

距離開車時間還有30分鐘左右,還是不能大意,趕緊過去。



站前的平交道。

看來這回是搭井川號回去。

小小的車站,擠了不少人在等車。


上車。來的時候在車廂內拍不過癮,回程決定要坐觀景車。
車掌小姐提醒:「會很冷喔,真的沒關係嗎?」
咬牙給他撐下去啦!!!

請好好地欣賞以下這些用命換來的照片... 冬天來真的會遭難。




又回到湖上了。出入口只有鐵橋步道,秘境站當之無愧。




來時沒拍到跟失敗的照片,回程給他補回來。



一樣在這裡掛上齒軌輔助車頭,下坡也是要靠電力煞車的。


這張圖是水平狀態拍的。 更能看出齒軌區間的斜度。


水壩的兩側,在夏天似乎是露營勝地。

邊寫邊搜尋資料,上游的井川水壩好像更壯觀。希望有機會去。


很明顯的是露營區。




上篇沒提到,這個站是不能下客的。只有鐵道跟電力公司的職員可以出站。

看到販賣機又安心了...

アプトいちしろ站的唯一出口就是這個通往水力發電廠的吊橋。

補拍齒軌特寫。



在旁邊待命的齒軌輔助車。


新舊線的分支。




這個站是往寸又峽溫泉的巴士接駁站,來的時候有不少旅客在這裡下車。
同樣的,回程也有不少人上車。




廣角鏡的好處就是相機不用伸出去太遠...  很怕被樹打到相機。
















井川線上少見的平交道。

回到千頭站了。補拍井川線的站牌。




SL車頭。



這時才發現大井川鐵道的架線柱實在是鏽得厲害,哪天斷了都不知道的感覺...





有展示鐵道娘的印章,不過賣店在我來的時候沒開,回程也沒開...
看來今天沒開。還好買便當的時候順便買了。








距離往金谷的開車時間還有一點,到站外看看才發現這裡也有溫泉啊...
好像沒必要泡兩次...

但是一上車之後就後悔了。其實可以再晚一小時回去的...
想泡溫泉~~~

上車嘍。去回是不同的車型,讚啦~

一整個莒光號的feel...

補圖。

接下來光線不足就沒辦法在車上拍了。5小時回東京的車程...
在車上看到東海道新幹線因為大雪影響晚點的告示,青春18大勝利! 還好東海道線沒掛...


在井川線那段真的被冷到了,晚餐來個熱呼呼的火鍋啦~