over 4 years ago

在《關於Android App Architecture》文章提到利用MavenGradle來管理模組之間的關係,雖然我不太喜歡XML瑣碎的設定,不過先前曾實驗過用Maven管理單純的Android project搭配Pure Java模組,但是沒試過和Native模組搭配的組合,所以週末下午試了一下其實還蠻有意思的(花了些時間才發現有地方設定錯了),所以留個筆記,避免自己以後再犯這樣的錯誤。

要使用Maven管理有Native模組的Android專案,需安裝下列工具及SDK:

  1. Java SE 6 or later
  2. Eclipse
  3. Android SDK (包含Extras的部份)
  4. Android NDK
  5. Maven 3 (3.1.1 or later)

接著用Eclipse的安裝管理員安裝下列Plugins:

  1. C/C++ Development Tools
  2. ADT, Android Development Tools (Developers Tool和NDK Plugin都裝)
  3. Android for Maven Eclipse Connector (會一併安裝Maven Android Plugin)

然後如Figure 1及Figure 2將Android SDK及Android NDK的安裝路徑加到Eclipse的ADT設定頁中對應的地方。另外將路徑設定到PATH、ANDROID_HOME和ANDROID_NDK_HOME等環境變數。原則上開發時,Maven會根據POM檔將必要的JAR下載並放到到Local repository中,但如果想在Local repository取得大多數SDK的JAR檔,可以使用Maven Android SDK Deployer

Figure 1 - Setup Android SDK in Eclipse

Figure 2 - Setup Android NDK in Eclipse

先前的架構有提到Model、Android等區塊,所以會建立一個fun的POM專案,包含一個fun-core模組代表Model區塊,一個fun-android代表Android區塊,另外還有一個fun-android-jni模組代表Native的部份(例如Cocos2dx之類的程式),然後利用dependency的方式讓fun-android使用fun-android-jni模組,然後讓fun-android-jni實作fun-core裡的介面。結構大致上是這樣,開始動工,首先建立一個POM專案,如Figure 3在Eclipse中建立Maven專案,在對話框中按下一步,會有一個archetype的選擇頁面,在filter欄位中輸入pom,然後下方應該會出現一個pom-root的項目,選取該項目。然後在下一步設定Group ID (通常是公司網域名稱,本例中是tw.fun)及Artifact ID (產品ID,本例中是fun)。

Figure 3 - Create Maven Project in Eclipse

Figure 4 - Use the pom-root archetype

設定好後按下Finish後,Eclipse會建立一個只有一個pom.xml的專案,開啟pom.xml檔,將內容置換成Code List 1所列的內容。主要設定Android平台的版本(platform.version),共用的套件(android)和Maven plugin的設定。接下來建立的模組會共用這份設定。

Code List 1 - The updated content of the pom.xml in fun project
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>tw.fun</groupId>
    <artifactId>fun</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>
    <name>fun</name>
    <properties>
        <platform.version>4.1.1.4</platform.version>
        <android.plugin.version>3.6.0</android.plugin.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.google.android</groupId>
            <artifactId>android</artifactId>
            <version>${platform.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>com.jayway.maven.plugins.android.generation2</groupId>
                    <artifactId>android-maven-plugin</artifactId>
                    <version>${android.plugin.version}</version>
                    <configuration>
                        <sdk>
                            <platform>18</platform>
                        </sdk>
                    </configuration>
                </plugin>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.1</version>
                    <configuration>
                        <source>1.6</source>
                        <target>1.6</target>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

POM檔建立完成後,對剛剛建立的fun專案按右鍵,於選單中選擇Run As -> Maven install,將POM檔安裝到Local repository中,Local repository是一個共用的函式庫池,每個函式庫都會有一份POM檔,讓Maven分析跟載入有關係的函式庫,所以這一步要先做,讓後面建立的模組能找到歸屬的群組。

接著建立fun-core模組,在Eclipse的專案列表中,對剛剛建立的fun專案按右鍵,於選單中選擇Maven -> New Maven Module Project,在對話中模組名稱(Module Name)輸入fun-core,然後勾選Create a simple project (skip archetype selection),按下一步確認內容沒錯後按Finish,此時Eclipse會建立一個標準的Maven Java專案fun-core,而原本的fun專案則多了一個fun-core資料夾。一個標準的Maven Java專案會用src/main/java放production code及src/test/java放測試程式碼,所以在src/main/java中先建立一個tw.fun.core的package,接著建立一個內容如Code List 2的NativeService介面,這介面定義一個將字串反轉的函式。

Code List 2 - Create NativeService interface
package tw.fun.core;

public interface NativeService {

    public String reverse(String string);
}

同樣,對fun-core專案執行Run As -> Maven install,如果注意console介面會發現,除了安裝POM檔外,Maven會變編譯程式、跑測試,然後將編譯好的JAR檔也安裝到Local repository。基本上,已經沒什麼要對fun-core修改了,接著建立一個fun-android-jni的模組,此時選擇artifactId為android-library-quickstart的項目,在下一頁中可以設定package的名稱(為了和fun-core的規則一致,將package名稱改成tw.fun.android.jni)和SDK的API Level (預設是16),若不修改API Level可以按Finish,如此Eclipse會建立一個名為fun-android-jni的Android Library Project,接著修改fun-android-jni專案中的pom.xml如Code List 3,因為要新增一個dependency:fun-core,此外,在建置的plugin部分也做了些許修改,例如加入<goal>ndk-build</goal>這個建置目標。

Code List 3 - The updated content of the pom.xml in fun-android-jni project
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>fun</artifactId>
        <groupId>tw.fun</groupId>
        <version>1.0.0</version>
    </parent>
    <artifactId>fun-android-jni</artifactId>
    <packaging>apklib</packaging>
    <name>fun-android-jni</name>
    <dependencies>
        <dependency>
            <groupId>tw.fun</groupId>
            <artifactId>fun-core</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-clean-plugin</artifactId>
                <configuration>
                    <filesets>
                        <fileset>
                            <directory>libs</directory>
                        </fileset>
                        <fileset>
                            <directory>obj</directory>
                        </fileset>
                    </filesets>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.jayway.maven.plugins.android.generation2</groupId>
                <artifactId>android-maven-plugin</artifactId>
                <goals>
                    <goal>ndk-build</goal>
                </goals>
                <configuration>
                    <deleteConflictingFiles>true</deleteConflictingFiles>
                    <attachNativeArtifacts>true</attachNativeArtifacts>
                    <clearNativeArtifacts>false</clearNativeArtifacts>
                    <sign>
                        <debug>false</debug>
                    </sign>
                    <proguard>
                        <skip>true</skip>
                    </proguard>
                </configuration>
                <extensions>true</extensions>
            </plugin>
        </plugins>
    </build>
</project>

有時候Eclipse無法立即更新dependency,這時可以對fun專案(root專案)按右鍵,在選單中選取Maven -> Update Project,選取剛建立的模組後按Ok。將tw.fun.android.jni package中預設建立的Library給刪除,由於剛加入dependency的關係,fun-android-jni已經能看到NativeService的介面,於是建立一個實作NativeService介面的NativeServiceImpl的class,並將內容改成如Code List 4所示。注意,新增的reverseImpl(String)函式並沒有實作,並且前方多了一個native修飾字,這是表示此函式是一個JNI函式,也就是Android NDK用來建立Java程式和C/C++程式之間的窗口,所以reverse(String)呼叫reverseImpl(String)函式等於呼叫C/C++程式。

Code List 4 - The NativeServiceImpl implementation
package tw.fun.android.jni;

import tw.fun.core.NativeService;

public class NativeServiceImpl implements NativeService {

    public native String reverseImpl(String string);

    static {
        System.loadLibrary("fun-android-jni");
    }

    @Override
    public String reverse(String string) {
        return reverseImpl(string);
    }
}

但C/C++程式呢?接著對fun-android-jni專案按右鍵,於選單中選擇Android Tools -> Add Native Support,在跳出來的對話框中,會問native library的名稱,這裡的名稱必須和Code List 4中System.loadLibrary()傳入的名稱相同,按下Finish後,專案會多出一個jni的資料夾,和一個fun-android-jni.cpp檔,然後打開console於專案路徑下輸入指令,用javah產生對應的header檔,若沒有任何錯誤訊息及表示成功了,回到Eclipse對jni資料夾按F5更新外部資源,此時應該會看到多一個名稱怪異的.h檔,這個檔案不需要修改(每次執行javah都會重新產生一份新的)。

Code List 5 - Generate header for JNI methods
javah -d jni -classpath bin/classes tw.fun.android.jni.NativeServiceImpl 

需要修改的是剛剛產生的fun-android-jni.cpp,如Code List 6所示,將產生的.h檔引入,接著實作裡面定義的函式。

Code List 6 - The implementation of fun-android-jni.cpp
#include <jni.h>
#include <stdio.h>
#include <string.h>

#include "tw_fun_android_jni_NativeServiceImpl.h"

JNIEXPORT jstring JNICALL Java_tw_fun_android_jni_NativeServiceImpl_reverseImpl(JNIEnv* jenv, jobject obj, jstring str) {
    const char* cs = jenv->GetStringUTFChars (str, NULL);
    char* cstring = new char [strlen(cs) + 1];
    sprintf (cstring, "%s", cs);
    jenv->ReleaseStringUTFChars(str, cs);

    int len = strlen(cstring);
    char* reversed = new char [len+1];
    for(int i = 0; i < len; i++) {
        reversed[i] = cstring[len-i-1];
    }
    reversed[len] = 0;
    return jenv->NewStringUTF(reversed);
}

接著,這裡將fun-android-jni安裝到Local repository的方法和fun-core有點不一樣,一樣是對fun-android-jni按右鍵,然後執行Run As -> Maven build,在開啟對話框的Goals欄位中,輸入下列目標,第一個clean會將工作區清除,第二個 android:ndk-build指令會用NDK的編譯器編譯剛剛寫的C++程式並包裝成apklib,接著第三個install指令會將POM檔案和apklib都安裝到Local repository中,其中,-Dandroid.ndk.path="/Applications/android-ndk"是選用的,如果ANDROID_NDK_HOME有設定,基本上應該不需要,但如果在Eclipse中運作不正常,就需要加上這敘述,並將雙引號中的內容指向Android NDK安裝的路徑。

clean android:ndk-build -Dandroid.ndk.path="/Applications/android-ndk" install

Figure 5 - Customize Maven build goals

有了fun-core和fun-android-jni後,接著新增第三個模組:fun-android,步驟和前面相似,但選擇artifactId為android-quickstart的archetype,同樣記得改platform和package內容,按下Finish後,有了第一個Android專案。同樣,修改pom.xml如Code List 7所示,將fun-android-jni加到dependency中,因為fun-android-jni的dependency中有fun-core,所以fun-core也會被自動推導並加到dependency中。

Code List 7 - The updated content of the pom.xml in fun-android project
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>fun</artifactId>
        <groupId>tw.fun</groupId>
        <version>1.0.0</version>
    </parent>
    <artifactId>fun-android</artifactId>
    <packaging>apk</packaging>
    <name>fun-android</name>
    <dependencies>
        <dependency>
            <groupId>tw.fun</groupId>
            <artifactId>fun-android-jni</artifactId>
            <version>1.0.0</version>
            <type>apklib</type>
        </dependency>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>com.jayway.maven.plugins.android.generation2</groupId>
                    <artifactId>android-maven-plugin</artifactId>
                    <version>${android.plugin.version}</version>
                    <extensions>true</extensions>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>com.jayway.maven.plugins.android.generation2</groupId>
                <artifactId>android-maven-plugin</artifactId>
                <configuration>
                    <sdk>
                        <platform>18</platform>
                    </sdk>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

接著修改Maven自動產生的HelloAndroidActivity.java和res/layout/activity_main.xml如Code List 8和Code List 9所示,就快要可以看到成品了,先啟動Android模擬器(Maven Android plugin似乎還無法自動啟動模擬器),接著對fun-android專案按右鍵,執行Run As -> Maven build,在對話框的Goal欄位輸入下列目標,前兩個已經看過,不一樣的是第三個android:deploy,這會將編譯並打包好的apk安裝到啟動的模擬器中,第四個android:run會在模擬器中啟動App。

clean install android:deploy android:run
Code List 8 - The implementation of HelloAndroidActivity
package tw.fun.android;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.widget.TextView;

import tw.fun.android.jni.NativeServiceImpl;
import tw.fun.core.NativeService;

public class HelloAndroidActivity extends Activity {

    private TextView _inputTextView;
    private TextView _resultTextView;
    private NativeService _service;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        _service = new NativeServiceImpl();
        setContentView(R.layout.activity_main);
        _inputTextView = (TextView)findViewById(R.id.input);
        _resultTextView = (TextView)findViewById(R.id.result);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(tw.fun.android.R.menu.main, menu);
        return true;
    }

    public void reverseButtonClicked(View view) {
        String text = _inputTextView.getText().toString();
        _resultTextView.setText(_service.reverse(text));
    }
}
Code List 9 - The content of activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin" >
    <TextView
        android:id="@+id/input"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />
    <TextView
        android:id="@+id/result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/input"
        />
    <Button
        android:id="@+id/reverseButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/input"
        android:layout_below="@+id/result"
        android:layout_marginTop="76dp"
        android:onClick="reverseButtonClicked"
        android:text="@string/reverse" />
</RelativeLayout>

輸入完畢按下Run就會在模擬器中啟動App,如Figure 6所示,整個過程就像是原本用Eclipse開發Android程式並用Run執行一樣。最後,花了這麼多功夫,用Maven的好處是什麼?大概有四點,第一,許多Java常見的第三方函式庫都支援Maven的dependency管理,因此可以透過Maven使用並管理Android專案的第三方函式庫;第二,用Maven管理系統架構裡模組間的關係;第三,若有使用subversion做版本控管,Maven在Eclipse中有subversion的connector,透過connector從check out到重建所有專案只需要幾個步驟,很適合讓新人建立開發環境;最後,有Maven後可以和Jenkins之類的CI做整合,方便進行持續整合和測試。

Figure 6 - The app screenshot

ps. 想試玩的可以下載這個,解開到Eclipse的工作區,用Maven匯入專案。

← About Custom View and Runtime Attributes About the Optional Design in Swift & Java →