Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> App的打磨之路(下)

App的打磨之路(下)

編輯:關於Android編程

前言:該文接上兩篇博文App的打磨之路(上)和App的打磨之路(中),繼續描述打包、反編譯及加固。

一、打包

每個Android應用在完成後都需要打成APK包,對於單個打包的方式在此就不贅述了,基本IDE都帶,只是在對外發布的應用需要配置屬於該應用的唯一簽名,下文主要講述需要上傳多個市場的情況下怎麼批量打包。

1、Maven打包

Maven是一個項目管理工具,它包含了一個項目對象模型(Project Object Model),一組標准集合,一個項目生命周期(ProjectLifecycle),一個依賴管理系統(Dependency Management System),和用來運行定義在生命周期階段(phase)中插件(plugin)目標(goal)的邏輯。
Maven也是自動構建工具,配合使用android-maven-plugin插件,以及maven-resources-plugin插件可以很方便的生成渠道包,下面簡要介紹下打包過程。
首先,在AndroidManifest.xml的節點中添加如下元素,用來定義渠道的來源:


定義好渠道來源後,接下來就可以在程序啟動時讀取渠道號了:

private String getChannel(Context context) {
    try {
        PackageManager pm = context.getPackageManager();
        ApplicationInfo appInfo = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
        return appInfo.metaData.getString("channel");
    } catch (PackageManager.NameNotFoundException ignored) {
    }
    return "";

}

要替換AndroidManifest.xml文件定義的渠道號,還需要在pom.xml文件中配置Resources插件:



    
    
        
        ${project.basedir}
        
        true
        
        ${project.build.directory}/filtered-manifest
        
        
            AndroidManifest.xml
        
    

准備工作已經完成,現在需要的就是實際的渠道號了。下面的腳本會遍歷渠道列表,逐個替換並打包:

#!/bin/bash

package(){
    while read line
    do
        mvn clean
        mvn  -Dchannel=$line package
    done < $1
}

package $1

從以上描述中可以看出該方式每打一個包都會重新構建,執行效率太低,對於少量渠道還可以接受,渠道包過多就沒法滿足需求了。

2、Apktool打包

Apktool是一個逆向工程工具,可以用它解碼(decode)並修改apk中的資源。接下來詳細介紹如何使用apktool生成渠道包。
前期工作和用Maven打包一樣,也需要在AndroidManifest.xml文件中定義元素,並在應用啟動的時候讀取清單文件中的渠道號。具體請參考上面的代碼。和Maven不一樣的是,每次打包時不再需要重新構建項目。打包時,只需生成一個apk,然後在該apk的基礎上生成其他渠道包即可。
首先,使用apktool decode應用程序,在終端中輸入如下命令:

apktool d your_original_apk build

上面的命令會在build目錄中decode應用文件,decode完成後的目錄描述如下:

目錄 描述 assets目錄 存放需要打包到apk中的靜態文件 lib目錄 程序依賴的native庫 res目錄 存放應用程序的資源 smail目錄 存放Dalvik VM內部執行的smail代碼 AndroidManifest.xml 應用程序的配置文件 apktool.yml apktool相關配置文件

接下來,替換AndroidManifest.xml文件中定義的渠道號,下面是一段python腳本:

import re

def replace_channel(channel, manifest):
    pattern = r'()'
    replacement = r"\g<1>{channel}\g<3>".format(channel=channel)
    return re.sub(pattern, replacement, manifest)

更多有關Python的使用可參考Python教程。
然後,使用apktool構建未簽名的apk:

apktool b build your_unsigned_apk

最後,使用jarsigner重新簽名apk:

jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore your_keystore_path -storepass your_storepass -signedjar your_signed_apk, your_unsigned_apk, your_alias

上面就是使用apktool打包的方法,通過使用腳本可以批量地生成渠道包。不像Maven,每打一個包都需要執行一次構建過程,該方法只需構建一次,大大節省了時間,但缺點是每生成一個包需要重新簽名一次。

3、批量快速打包

如果能直接修改APK的渠道號,而不需要再重新簽名能節省不少打包的時間。上文APK瘦身中講述過APK解壓後的目錄結構,其中有個META-INF目錄,是存放簽名相關信息用來校驗APK的完整性的,如果在META-INF目錄內添加空文件,可以不用重新簽名應用。因此,通過為不同渠道的應用添加不同的空文件,可以唯一標識一個渠道。
下面的python代碼用來給apk添加空的渠道文件,渠道名的前綴為channel_:

import zipfile
zipped = zipfile.ZipFile(your_apk, 'a', zipfile.ZIP_DEFLATED)
empty_channel_file = "META-INF/channel_{channel}".format(channel=your_channel)
zipped.write(your_empty_file, empty_channel_file)

假設渠道名為test,則添加完空渠道文件後META-INFO目錄多了一個名為channel_test的空文件:
接下來就可以在代碼中讀取空渠道文件名了:

public static String getChannel(Context context) {
    ApplicationInfo appinfo = context.getApplicationInfo();
    String sourceDir = appinfo.sourceDir;
    String ret = "";
    ZipFile zipfile = null;
    try {
        zipfile = new ZipFile(sourceDir);
        Enumeration entries = zipfile.entries();
        while (entries.hasMoreElements()) {
            ZipEntry entry = ((ZipEntry) entries.nextElement());
            String entryName = entry.getName();
            if (entryName.startsWith("channel")) {
                ret = entryName;
                break;
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (zipfile != null) {
            try {
                zipfile.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    String[] split = ret.split("_");
    if (split != null && split.length >= 2) {
        return ret.substring(split[0].length() + 1);

    } else {
        return "";
    }
}

這樣,每打一個渠道包只需復制一個apk,在META-INF中添加一個使用渠道號命名的空文件即可。
更多關於打包詳情可參考AndroidMultiChannelBuildTool.

4、Gradle定制化打包

關於Gradle多渠道打包可以參考我的另一篇博文Android Studio常用Gradle操作,下面主要講解如何根據各個渠道不同的需求來定制化打包,如控制是否自動更新,使用不同的包名、應用名等。

使用不同的包名
如應用test有兩個不同的包名,分別是com.example.test1和com.example.test2,需要對應上傳到市場t1和t2,那麼在productFlavors中進行如下描述:
productFlavors {
    t1 {
        applicationId "com.example.test1"
    }
    t2 {
        applicationId "com.example.test1"
    }
}

上面的代碼添加了兩個渠道,兩個渠道的包名不同,運行gradle assemble命令即可生成兩個不同渠道的適配包。

控制是否自動更新
有些客戶端在啟動時會默認檢查客戶端是否有更新,如果有更新就會提示用戶下載。但是有些渠道和應用市場不允許這種默認行為,所以在適配這些渠道時需要禁止自動更新功能。一般的解決思路是提供一個配置字段,應用啟動的時候檢查該字段的值以決定是否開啟自動更新功能。
Gradle會在generateSources階段為flavor生成一個BuildConfig.java文件。BuildConfig類默認提供了一些常量字段,比如應用的版本名(VERSION_NAME),應用的包名(PACKAGE_NAME)等。更強大的是,開發者還可以添加自定義的一些字段。下面的示例假設t3市場默認禁止自動更新功能:
android {
    defaultConfig {
        buildConfigField "boolean", "AUTO_UPDATES", "true"
    }

    productFlavors {
        t3 {
            buildConfigField "boolean", "AUTO_UPDATES", "false"
        }
    }
}

上面的代碼會在BuildConfig類中生成AUTO_UPDATES布爾常量,默認值為true,在使用t3渠道時,該值會被設置成false。接下來就可以在代碼中使用AUTO_UPDATES常量來判斷是否開啟自動更新功能了。最後,運行gradle assembleT3命令即可生成默認不開啟自動升級功能的渠道包。

使用不同的資源
最常見的一類適配是修改應用的資源,如不同的應用名稱、不同的logo、不同的啟動頁等。
Gradle在構建應用時,會優先使用flavor所屬dataSet中的同名資源。所以,解決思路就是在flavor的dataSet中添加同名的字符串資源,以覆蓋默認的資源。下面以適配t4渠道的應用名為Example2為例進行介紹。
首先,在build.gradle配置文件中添加如下flavor:
android {
    productFlavors {
        t4 {
        }
    }
}

上面的配置會默認src/t4目錄為t4 flavor的dataSet。
接下來,在src目錄內創建t4目錄,並添加如下應用名字符串資源(src/t4/res/values/appname.xml):


    Example2

默認的應用名字符串資源如下(src/main/res/values/strings.xml):


    Example1

最後,運行gradle assembleT4命令即可生成應用名為Example2的應用了。

使用第三方SDK
某些渠道會要求客戶端嵌入第三方SDK來滿足特定的適配需求,假設渠道t5需要引用com.example.test3:test:1.0.0該庫,那麼可以像如下這樣描述:
android {
    productFlavors {
        t5 {
        }
    }
}
...
dependencies {
    provided 'com.example.test3:test:1.0.0'
    t5Compile 'com.example.test3:test:1.0.0'
}

上面添加了名為t5的flavor,並且指定編譯和運行時都依賴com.example.test3:test:1.0.0。而其他渠道只是在構建的時候依賴該SDK,打包的時候並不會添加它。
接下來,需要在代碼中使用反射技術判斷應用程序是否添加了該SDK,從而決定是否要顯示該SDK提供的功能。部分代碼如下:

class MyActivity extends Activity {
    private boolean useSdk;

    @override
    public void onCreate(Bundle savedInstanceState) {
        try {
            Class.forName("com.example.test3.Test");
            useSdk = true;
        } catch (ClassNotFoundException ignored) {

        }
    }
}

最後,運行gradle assembleT5命令即可生成包含該SDK功能的渠道包了。

二、反編譯

1、原理

反編譯,又稱為逆向編譯技術,是指將可執行文件變成高級語言源程序的過程。反編譯技術依賴於編譯技術,是編譯過程的逆過程。
編譯程序把一個源程序翻譯成目標程序的工作過程分為五個階段:詞法分析;語法分析;語義檢查和中間代碼生成;代碼優化;目標代碼生成。詞法分析的任務是對由字符組成的單詞進行處理,從左至右逐個字符地對源程序進行掃描,產生一個個的單詞符號,把作為字符串的源程序改造成為單詞符號串的中間程序。語法分析以單詞符號作為輸入,分析單詞符號串是否形成符合語法規則的語法單位,如表達式、賦值、循環等,最後看是否構成一個符合要求的程序。語義分析是審查源程序有無語義錯誤,為代碼生成階段收集類型信息。中間代碼是源程序的一種內部表示,或稱中間語言。中間代碼的作用是可使編譯程序的結構在邏輯上更為簡單明確,特別是可使目標代碼的優化比較容易實現。代碼優化是指對程序進行多種等價變換,使得從變換後的程序出發,能生成更有效的目標代碼。目標代碼生成是編譯的最後一個階段。
反編譯器分為前端和後端,前端是一個機器依賴的模塊,包含句法分析二進制程序、分析其指令的語義、並且生成該程序的低級中間表示法和每一子程序的控制流向圖,通用的反編譯機器是一個與語言和機器無關的模塊,分析低級中間代碼,將它轉換成對任何高級語言都可接受的高級表示法,並且分析控制流向圖的結構、把它們轉換成用高級控制結構表現的圖;而後端是一個目標語言依賴的模塊,生成目標語言代碼。

2、語言介紹

C++、C語言一般不能反編譯為源代碼,只能反編譯為asm(匯編)語言,因為C較為底層,編譯之後不保留任何元信息,而計算機運行的二進制實際上就代表了匯編指令,所以反編譯為匯編是較為簡單的。
C#、Java這類高級語言,尤其是需要運行環境的語言,如果沒有混淆,非常容易反編譯。原因很簡單,這類語言只會編譯為中間語言(C#為MSIL,Java為Bytecode),而中間語言與原語言本身較為相似,加上保留的元信息(記錄類名、成員函數等信息)就可以反向生成源代碼,注意是由反編譯器生成,不會與源代碼完全相同,但可以編譯通過。這些特性本來是為反射技術准備的,卻被反編譯器利用,現在的C#反編譯器ILSpy甚至可以反向工程。

3、工具
dex2jar 這個工具用於將dex文件轉換成jar文件
下載地址:http://sourceforge.net/projects/dex2jar/files/ jd-gui 這個工具用於將jar文件轉換成java代碼
下載地址:http://jd.benow.ca/ apktool 這個工具用於最大幅度地還原APK文件中的9-patch圖片、布局、字符串等等一系列的資源
下載地址:http://ibotpeaches.github.io/Apktool/install/
4、反編譯過程

4.1、解壓APK,獲得其中的classes.dex文件;
4.2、拷貝classes.dex文件到dex2jar工具的解壓目錄下,使用如下命令:d2j-dex2jar classes.dex獲得classes-dex2jar.jar文件;
4.3、使用工具jd-gui打開classes-dex2jar.jar文件,如果代碼未被混淆,那麼打開後就可以對除資源外的源碼進行分析了;
4.4、將APK拷貝到apktool的解壓目錄下,使用命令apktool -d ***.apk,其中d是decode的意思,表示我們要對***.apk這個文件進行解碼。這樣可得到一個以APK名稱命名的目錄,該目錄下就是解碼後的結果了,其中的資源都是可以查看的。apktool命令除了這個基本用法之外,我們還可以再加上一些附加參數來控制decode的更多行為:

-f 如果目標文件夾已存在,則強制刪除現有文件夾(默認如果目標文件夾已存在,則解碼失敗)。
-o 指定解碼目標文件夾的名稱(默認使用APK文件的名字來命名目標文件夾)。
-s 不反編譯dex文件,也就是說classes.dex文件會被保留(默認會將dex文件解碼成smali文件)。
-r 不反編譯資源文件,也就是說resources.arsc文件會被保留(默認會將resources.arsc解碼成具體的資源文件)。

4.5、假如我們修改了解碼後的部分代碼或資源中的內容需要重新打包,那麼則使用命令apktool b *** -o New_***.apk進行打包;
4.6、打包後還不能安裝,需要重新進行簽名,簽名過程上文已描述過,在此就不贅述該過程了;
4.7、Android還極度建議我們對簽名後的APK文件進行一次對齊操作,因為這樣可以使得我們的程序在Android系統中運行得更快,對齊操作使用的是zipalign工具,該工具存放於/build-tools/目錄下,對齊使用命令如下:zipalign 4 New_***.apk New_***_aligned.apk,其中4是固定值。

注:以上所寫***都表示該APK的名稱,還有以上所描述過程僅用作技術交流,僅限於學習。

三、加固

Android中的Apk反編譯可能是每個開發都會經歷的事,但是在反編譯的過程中,對於源程序的開發者來說那是不公平的,那麼Apk加固也是應運而生,現在網上有很多Apk加固的第三方平台,如以下所示:
愛加密加固
360加固
梆梆加固
其實加固有些人認為很高深的技術,其實不然,說的簡單點就是對源Apk進行加密,然後在套上一層殼即可,當然還有很多細節需要處理,其簡單介紹如下:
1、加殼程序
任務:對源程序Apk進行加密,合並脫殼程序的Dex文件 ,然後輸入一個加殼之後的Dex文件
語言:任何語言都可以,不限於Java語言
技術點:對Dex文件格式的解析
2、脫殼程序
任務:獲取源程序Apk,進行解密,然後動態加載進來,運行程序
語言:Android項目(Java)
技術點:如何從Apk中獲取Dex文件,動態加載Apk,使用反射運行Application
目前來說,不管是混淆、加密還是加固都不完全是安全的,不管何時,逆向和安全都永遠不會停止戰爭。但對於一般的應用來說,混淆和加固基本就可以保證你應用的安全了,因為不管是出於什麼原因都是需要考慮時間和人力成本的。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved