Export to Jar

      在〈Export to Jar〉中尚無留言

為何要匯出成 Jar

將所撰寫的程式碼包裝成 library(或是SDK), 日後有新專案時即可 import 重複使用. 此SDK如同積木, 可以直接堆疊使用. 將代碼製作成SDK,要考慮的因素非常多, 也相當費時. 但對日後的專案可省下非常多的開發時間. 在Java or Android中, SDK檔都是以jar形式儲存.

1.  SDK可賣錢

寫出來的程式碼, 可以編譯成 apk 放在google play 讓人下載, 收取下載費用. 若是免費下載, 也通常會帶著廣告取得收益.

除了編譯成 apk 之外, 另一種方式就是把自己開發的獨特演算引擎編譯成 SDK, 再賣給需要的廠商. 通常這種 SDK 的工程相當浩大, 也需投入大量的人力跟時間. 但真正能賺到錢的就是這種模式, 比如 Skype, Line, Facebook, Pokemon, 丹青中文掃瞄, 金山office, 他們最大的收益就是由此而來. 可惜台灣沒有任何的公司有此遠見, 通常一年沒回收, 就直接裁員撤部門.

2. 保護自已的智慧財產權

專案是否提供原始碼給客戶, 有著不同的價格. 通常客戶都不會有這種Sense. 所以可以提供UI的原始碼給客戶, 但演算法的部份則加密成SDK, 這樣客戶端就無法任意更改使用並散佈我們辛苦寫出來的成果.

3. 建立自己的模組

如果沒有上面所述的偉大志願, 在個人開發上, 也需將日常開發的類別編譯成 SDK, 待日後需要時, 再 import SDK直接使用, 如此就不用再去想當初這些演算法到底是怎麼寫出來的.

舉例來說, 比如你現在正在開發一個照相機的功能, 因照相機相當繁雜, 所以就可以創造一個叫作MahalCamera的類別, 把拍照, 對焦, 聲音開關, 閃光燈, 放大縮小等功能寫在此類別之中, 然後編譯成 MahalCamera.jar. 待日後又有專案來了, 需要啟動照相機時, 只需呼叫jar裏面的方法, 完全不用去想照相機該怎麼啟動, 該用什麼SurfaceView之類的.

開啟專案

Add Activity 畫面中, 選取 No Activity

jar_3

匯出sd, 需先對整体的命名進行規畫. 以本人為例, 本人的App皆為 mahaljsp.asuscomm.com.XXX , 其中 XXX 為App的專案名稱, 而 mahaljsp.asuscomm.com為本人的網域.

若要開發自己的sdk, 比如class MahalCamera , 則日後會在App中 import com.asuscomm.mahaljsp.MahalCamera這個類別.

所以在開啟專案時, Company Domain 只要輸入 asuscomm.com即可, 然後Application name 則為mahaljsp

jar_1

接下來的Miniimum SDK 則選擇最小可適用的版本

jar_2

修改Gradle Script

開啟 build.gradle(Module:app) 如下圖

jar_5

gradle是編譯時的script檔, 需依如下說明進行修改. 將刪除線的部份移除, 再增加藍色字体的部份

//apply plugin: 'com.android.application'
apply plugin: 'com.android.library'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.asuscomm.mahaljsp"
        minSdkVersion 19
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    lintOptions {
        abortOnError false
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.2.0'
    testCompile 'junit:junit:4.12'
}
//刪除舊有的jar檔
task deleteOldJar(type: Delete) {
    delete 'release/MahalSdk.jar'
}

//匯出jar檔
task exportJar(type: Copy) {
    from('build/intermediates/packaged-classes/release/')
    into('release/')
    include('classes.jar')
    // 將匯出的 jar 檔重新命名
    rename('classes.jar', 'MahalSdk.jar')
}
//匯出到另一個新專案
task exportJarCar(type: Copy){
    from('build/intermediates/packaged-classes/release/')
    into ('d:/thomas/PatrolScooter/app/libs/')
    include('classes.jar')
    rename('classes.jar', 'mahalsdk-1.2.0.jar')
}
exportJar.dependsOn(deleteOldJar, build, exportJarCar)

修改完後, 再按右上方的 Sync Now

編譯 Jar

經過上述的設定後, 選取右方工具列的Gradle, 再選取專案的Tasks/other , 就會多出 exportJar 這個編譯任務,  將exportJar按二下即可開始編譯.

若編譯成功, 在專案下的 app\release\ 目錄, 就會有MahalSdk.jar了

jar_6

套用

新專案需使用到上述的 jar 時, 先將新專案切換到Project, 再將 jar copy到 libs之下. copy之後還不能用, 需在 jar 處按右鍵/Add as library, 如此就可以開始套用了.

jar_7

經由上述Add to Libiray後, build.gradle就會增加如下藍色的部份

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    compile files('libs/mysdk-1.0.0.jar')
}

Jar 與混淆

混淆的作法, 是將變數及方法編譯成 a, b, c, d等人類不易看懂的代號. 比如有一個方法叫
void setLevel(int x, int y), 混淆後會變成 void a(int x, int y).

在外部專案上, 直接呼叫jar的 setLevel() 即可調用. 但一但翻譯成a(), 那專案不就找不到了嗎. 還好, 只有public方法才會被外部專案調用, 而private方法只能在 jar 內被使用, 所以只要忽略public方法的混淆即可.

啟動混淆, 更改gradle設定如下

buildTypes {
    release {
        minifyEnabled true //啟動混淆
        zipAlignEnabled true //優化
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

上述的混淆會遵循proguard-rules.pro 設定檔的規定. 所以可以在 proguard-rules.pro 設定不混淆 public 方法. 請注意下面的寫法, com.asuscomm.xxx  是 jar 中的 package 名稱.

-ignorewarnings
-keep class com.asuscomm.mahaljsp.mahalsdk.**{public *;}

Jar與resource

此問題有點複雜:
1. jar 可否自帶 resource ?
2. jar 可否調用新專案的 res ?

第一個問題 : Jar是 Java規範的打包檔, 絕對無法將 resource 一同打包進去.
但不能包入resource 是會死人的. 為了解決這個問題, 就必需使用aar檔. 作法請參照下一篇 Export to aar

第二個問題 : jar 可以調用專案的 res. 但如何呼叫呢? 這有如下的問題要解決~~~
專案的 R.id.XXX在編譯時, 會被轉成一個 int 常數. 但這個常數是在 jar 數百年之後才形成的, 所以 jar 根本不知這個專案的常數是多少, 所以也就無法在 jar 取用 R.id.XXX, 所以會出現classdefnotfound exception.

聽不懂嗎? 其實是真的不好懂.  jar 就好比如諸葛亮, 新的專案就像劉伯溫. 諸葛亮是如何知道在千年之後, 會有一個自以為是的劉伯溫這傢伙去挖他的墳呢!!! 嗯,諸葛亮真的很聰明, 使用了反射機制(Reflection). 諸葛亮先算出幾百年後會有戰亂, 然後再查那一年會有誰想挑戰他.

ps : 諺語~~
三分天下諸葛亮, 一統江山劉伯溫; 前朝軍師諸葛亮, 後朝軍師劉伯溫.
劉伯溫自命不凡, 更勝孔明. 待掘墳之後, 羞愧難當.

java的反射機制,通過String s1找到其類別(年代),再從 s1 找到變量值 s2(劉伯溫), 相關的方法如下

public static int getId(Context context, String s1, String s2){
    try{
        Class localClass = Class.forName(context.getPackageName() + ".R$" + s1);
        Field localField = localClass.getField(s2);
        return Integer.parseInt(localField.get(localField.getName()).toString());
    }
    catch (Exception e){
        Log.e("getIdByReflection error", e.getMessage());
    }
    return 0;
}

舉例來說, 在 jar 中, 若要使用 R.raw.camera_click時, 就會出現上述的問題 : classdefnotfound exception.
soundId[0]=soundPool.load(context, R.raw.camera_click, 1);

所以需改為如下
soundId[0]=soundPool.load(context, getId(context, “raw“, “camera_click“), 1);

其中 “raw” 由 String s1 接收, “camera_click”由 String s2 接收

較簡易的方式

上述的方式, 確實會比較麻煩困難一些. 在此說明另一個較簡單的方式.

1. 在新專案中的res/drawable放入圖檔, 如R.drawable.icon
然後 new MahalMap(context, handle, R.drawable.icon);

2. 在sdk中, 使用如下方式取得Icon

Bitmap bitmap= BitmapFactory.decodeResource(context.getResources(), locationMark);

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *