首先,對於傳統的Android開發領域來講,分包指的是MultiDex,即將單個dex拆分紅多個以突破函數數目瓶頸的技術。而這裏的分包(Apk Expansion Files)指的是將Apk文件和大容量的資源文件分開打包,大容量的資源文件包括高清大圖,音頻文件,視頻文件等,這些文件最終都會壓縮到統一的.obb文件裏。注意,抽出到obb的內容不包括運行時代碼。因此開發者須要保證在缺乏.obb文件的狀況下,程序依然能正常運行(不會Crash)。java
在分包以前,開發者須要明確項目中的大容量資源文件到底是什麼,大多數狀況下,他們指的是assets目錄下的資源以及raw下的文件,若是drawable和mipmap目錄下有超過1M的文件,也能夠考慮將其進行分包處理,這種狀況下須要開發者將該資源的引用方式從直接使用資源id:R.drawable.xxx改成從文件中解析。android
全部的資源文件將被壓縮爲obb文件,最終上傳到GooglePlay供用戶下載。數組
什麼是obb文件,obb全稱是Opaque Binary Blob,翻譯過來是不透明的二進制對象,再進一步解析就是具備訪問權限的二進制文件。看到這個定義很容易聯想到另一種文件格式——zip壓縮包文件。因此,從本質上來講,obb文件和zip文件是同樣的,它們只是在不一樣領域上不一樣解釋罷了。而在Android分包領域,obb還有本身的一些規則。瀏覽器
obb的命名規則以下:bash
[main/patch].[versionCode].[packageName].obb
複製代碼
main.16.com.example.obbtest.obb
複製代碼
方法一:併發
官方工具法,Google官方提供了Jobb工具用來生成obb文件,工具能夠在 Android\sdk\tools\bin文件夾下找到。這是一個命令行工具,具體用法和參數以下:app
$ jobb -d [全部資源的路徑] -o [生成的obb名稱(請遵循上述命名規則)] -k [打包密碼] -pn [包名] -pv [versionCode(跟obb名稱的versionCode一致)]
複製代碼
也可使用該工具對obb文件進行解壓:ide
$ jobb -d [輸出路徑] -o [obb文件名] -k [打包所用的密碼]
複製代碼
方法二:函數
壓縮工具法,直接使用Windows或者Mac上的打包工具,將文件壓縮成zip包後,更改文件名便可。 工具
須要注意的是,壓縮文件格式須要選擇zip,並將壓縮方式改成存儲。如需進行加密,可以使用壓縮工具自帶的設置密碼方法,獲得的效果和官方方法設置 -k 參數是同樣的。 壓縮完後別忘了將文件名改成符合命名規範的obb文件名,如:main.16.com.example.obbtest.obb
複製代碼
方法三:
gradle打包法,即經過在build.gradle中添加壓縮腳本的方式,將須要打入obb的資源集體打包的方法。該方法會在後文中進行詳細介紹。
本地測試 本地測試的原理是模仿Google Play下載,將obb文件複製到相應的目錄。經過Google Play下載的obb文件存放的路徑爲:
/Android/obb/App包名/
複製代碼
因此,經過在/Android/obb/下建立[app包名 如com.example.obbtest]文件夾,並將obb文件複製到該目錄下便可模擬Google Play安裝App。
線上測試
登陸Google Play Console開發者帳號,打開應用列表,選擇須要測試的App:
左邊控制欄選擇 Release managerment ,而後選擇 App Release,最後選擇Internal test 的MANAGE INTERNAL TEST發佈內部測試版本。
在內部測試裏建立新的發佈版本:將GooglePlay版本的Apk上傳,上傳完畢後,點擊Apk右側添加更多按鈕,將obb文件提交上去,注意obb文件的命名版本號必須與上傳的apk的版本號一致,不然會收到提交版本失敗的錯誤。推薦你們使用不可能用在線上版本的versionCode進行測試,好比手機號碼、女友生日等,以避免後續提交正式版本時版本號被佔用(不知道爲何GooglePlay的內部測試和正式發佈的版本號居然不能重複)。
填寫剩下內容併發。,回到內部測試管理界面,選擇管理測試者,將須要測試的Google帳號提交上去,並將「Opt-in URL」的地址複製下來。
在測試機上登陸測試帳號,在瀏覽器裏打開剛剛的「Opt-in URL」地址,便可加入內測,並能夠經過Google Play App下載測試版本的App。
下載完成後,能夠在/Android/obb/App包名/下看到一份嶄新的obb文件。
解壓
第一次安裝完app後,須要將obb文件進行解壓並將解壓後的文件存儲到咱們定義的文件夾裏(能夠是data/data/包名/files/也能夠是內置存儲下自定義的項目文件夾)。要想解壓obb文件,第一步是獲取obb文件的本地路徑,具體代碼以下:
public static String getObbFilePath(Context context) {
try {
return Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/Android/obb/"
+ context.getPackageName()
+ File.separator
+ "main."
+ context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode
+ "."
+ context.getPackageName()
+ ".obb";
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return null;
}
}
複製代碼
拿到obb文件路徑後,能夠開始進行解壓了:
public static void unZipObb(Context context) {
String obbFilePath = getObbFilePath(context);
if (obbFilePath == null) {
return;
} else {
File obbFile = new File(obbFilePath);
if (!obbFile.exists()) {
//下載obb文件
} else {
File outputFolder = new File("yourOutputFilePath");
if (!outputFolder.exists()) {
//目錄未建立 沒有解壓過
outputFolder.mkdirs();
unZip(obbFile, outputFolder.getAbsolutePath());
} else {
//目錄已建立 判斷是否解壓過
if (outputFolder.listFiles() == null) {
//解壓過的文件被刪除
unZip(obbFile, outputFolder.getAbsolutePath());
}else {
//此處可添加文件對比邏輯
}
}
}
}
}
複製代碼
谷歌官方有提供解壓obb文件的庫供開發者使用,叫作APK Expansion Zip Library,感興趣的小夥伴能夠在一下路徑下查看。
<sdk>/extras/google/google_market_apk_expansion/zip_file/
複製代碼
筆者不推薦使用該庫,緣由是這個庫已經編寫了有一些年頭了,當時編譯的sdk版本比較低,有一些兼容性的bug須要開發者修改代碼後才能使用。因此這裏使用的upzip方法是用最普通的ZipInputStream和FileOutputStream解壓zip包的方式來實現的:
//這裏沒有添加解壓密碼邏輯,小夥伴們能夠本身修改添加如下
public static void unzip(File zipFile, String outPathString) throws IOException {
FileUtils.createDirectoryIfNeeded(outPathString);
ZipInputStream inZip = new ZipInputStream(new FileInputStream(zipFile));
ZipEntry zipEntry;
String szName;
while ((zipEntry = inZip.getNextEntry()) != null) {
szName = zipEntry.getName();
if (zipEntry.isDirectory()) {
szName = szName.substring(0, szName.length() - 1);
File folder = new File(outPathString + File.separator + szName);
folder.mkdirs();
} else {
File file = new File(outPathString + File.separator + szName);
FileUtils.createDirectoryIfNeeded(file.getParent());
file.createNewFile();
FileOutputStream out = new FileOutputStream(file);
int len;
byte[] buffer = new byte[1024];
while ((len = inZip.read(buffer)) != -1) {
out.write(buffer, 0, len);
out.flush();
}
out.close();
}
}
inZip.close();
}
public static String createDirectoryIfNeeded(String folderPath) {
File folder = new File(folderPath);
if (!folder.exists() || !folder.isDirectory()) {
folder.mkdirs();
}
return folderPath;
}
複製代碼
解壓完成後,就能夠經過輸出文件的路徑來訪問到咱們須要訪問的大容量資源了,文件的讀取在這裏就不展開了。
下載obb
從Google Play下載和安裝App有必定機率會下載到不包含obb文件的apk,或者obb文件被人爲刪除了。這種狀況下,須要開發者到谷歌提供的下載地址處下載相應的obb文件。但是要怎麼獲取到下載地址呢,這裏使用了官方的Downloader Library。
這個庫能夠經過Android Sdk Manager下載到,打開manager後勾上Google Play Licensing Library package和Google Play APK Expansion Library package點下載便可。但是在我興高采烈準備大幹一場的時候,發現它居然編譯不過[捂臉]。這個庫和上面說的APK Expansion Zip Library同樣,因爲年代久遠又年久失修,基本不能使用了。折騰了一些時間後,魔改了一個版本,才終於可使用。 這裏提供一個編譯好的jar包google_apk_expand_helper。具體代碼以下:
//隨機byte數組,隨便填就好
private static final byte[] salt = new byte[]{18, 22, -31, -11, -54, 18, -101, -32, 43, 2, -8, -4, 9, 5, -106, -17, 33, 44, 3, 1};
private static final String TAG = "Obb";
public static void getObbUrl(Context context, String publicKey) {
final APKExpansionPolicy aep = new APKExpansionPolicy(
context,
new AESObfuscator(salt,
context.getPackageName(),
Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID)
));
aep.resetPolicy();
final LicenseChecker checker = new LicenseChecker(context, aep, publicKey);
checker.checkAccess(new LicenseCheckerCallback() {
@Override
public void allow(int reason) {
Log.i(TAG, "allow:" + reason);
if (aep.getExpansionURLCount() > 0) {
//這裏就是獲取到的地址
String url = aep.getExpansionURL(0);
}
}
@Override
public void dontAllow(int reason) {
Log.i(TAG, "dontAllow:" + reason);
}
@Override
public void applicationError(int errorCode) {
Log.i(TAG, "applicationError:" + errorCode);
}
});
}
複製代碼
上述方法中須要提供參數publicKey,這個publicKey能夠在GooglePlayConsole中找到。
小結
掌握了上述的方法咱們就已經完成了Apk分包的主要流程了,如下內容將舉例說明若是經過配置gradle文件進行多渠道打包,如何在每次打包的時候自動將大容量資源文件壓縮成obb等。
例子
假設咱們如今須要發佈一個超過100M的安裝包到GooglePlay以及應用寶,對於GooglePlay來講,咱們須要生成小於100M的apk文件和obb文件,而對於應用寶來講,只須要生成一個完整的apk便可。
那麼問題來了,咱們不可能說在打包GooglePlay的時候將資源文件手動移除並修改資源引用的相關邏輯,而後再在打包應用寶的時候將他們放回來,這樣作會大大增長開發者的工做量而且增大出錯的可能性。那有沒有辦法在單個工程項目下既能打包GooglePlay的包又能夠打包應用寶的包呢?答案是有的,build.gradle中的sourceSets就能夠解決這樣的問題。
利用sourceSets隔離渠道資源和資源引用代碼
假設咱們有一個splash.mp4文件,在應用寶中渠道包中,它被放在了res/raw/目錄下。而在googlePlay渠道包中,它被放置在obb文件裏,咱們能夠這麼處理。
首先,在src目錄下建立兩個新的目錄googlePlay和tencent,並在他們的目錄下新建java,res和assest文件夾。
在app級別的build.gradle文件中添加GooglePlay和應用寶的渠道信息:
android {
flavorDimensions "default"
productFlavors {
GooglePlay { dimension "default" }
Tencent { dimension "default" }
/** 在AndroidManifest.xml中加入
<meta-data android:name="Channel"
android:value="${CHANNEL_NAME}" />
**/
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [CHANNEL_NAME: name]
}
}
}
複製代碼
緊接其後添加sourceSets配置,指定不一樣渠道的資源和代碼地址,其中main爲共有資源和代碼,其他的爲對應渠道包的資源和代碼:
sourceSets {
main {
java.srcDirs = ['src/main/java']
assets.srcDirs = ['src/main/assets']
res.srcDirs = ['src/main/res']
}
GooglePlay {
java.srcDirs = ['src/googlePlay/java']
res.srcDirs = ['src/googlePlay/res']
assets.srcDirs = ['src/googlePlay/assets']
}
Tencent {
java.srcDirs = ['src/tencent/java']
res.srcDirs = ['src/tencent/res']
assets.srcDirs = ['src/tencent/assets']
}
}
複製代碼
將splash.mp4放到tencent/res/raw/文件夾下,併爲不一樣渠道的java文件夾新建包名文件夾以及ResourcesHelper.java,完成後的目錄結構以下:
有兩點須要注意的地方:
一是java包下必須建立包名文件夾,不然會沒法引用到項目下的類。該例子中就是com.example.obbtest包。
二是AndroidStudio中能夠經過左下角的Build Variants窗口選擇當前須要編譯的渠道包類型,當選擇GooglePlay時會發現tencent下的java文件失效了。因此,若是須要修改某渠道下的java文件,請先經過Build Variants切換到指定渠道。
最後,針對不一樣渠道的ResourcesHelper.java採用不一樣的資源獲取方式:
GooglePlay版本:
public class ResourcesHelper {
public static void playSplashVideoResource(VideoView videoView){
String filePath = ObbHelper.getCurrentObbFileFolder()+"raw/"+"splash.mp4";
videoView.setVideoPath(filePath);
}
}
複製代碼
tencent版本:
public class ResourcesHelper {
public static void playSplashVideoResource(VideoView videoView) {
int resource = R.raw.splash;
String uri = "android.resource://" + videoView.getContext().getApplicationContext().getPackageName() + "/" + resource;
videoView.setVideoURI(Uri.parse(uri));
}
}
複製代碼
經過sourceSets隔離渠道資源和資源引用代碼在這裏就完成了,針對更加複雜的場景,就須要小夥伴根據實際狀況進行擴展和修改了。下面咱們來看一下如何在構建時自動將資源打包成obb文件。
構建時生成obb文件
要在構建時生成obb文件就必須經過添加gradle腳原本實現。咱們先在項目目錄下新建一個腳本文件flavour.gradle。
而後,要想打包obb文件,就必須知道如今構建的是哪一個渠道的包,那要怎麼拿到如今的渠道呢,請看代碼:
def String getCurrentFlavor() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Pattern pattern;
if (tskReqStr.contains("assemble"))
pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
else
pattern = Pattern.compile("generate(\\w+)(Release|Debug)")
Matcher matcher = pattern.matcher(tskReqStr)
if (matcher.find())
return matcher.group(1).toLowerCase()
else {
println "NO MATCH FOUND"
return ""
}
}
複製代碼
咱們知道obb的本質就是zip文件,因此只要在flavour.gradle中添加壓縮文件的方法,就能夠達到生成obb的效果了。因爲筆者的Groovy語言不精通,因此這裏使用java代碼來解決,在flavour.gradle中添加:
import java.util.regex.Matcher
import java.util.regex.Pattern
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
ext {
zipObb = this.&zipObb
getCurrentFlavor = this.&getCurrentFlavor
}
//外部壓縮方法入口,參數是全部須要壓縮文件的目錄以及輸出路徑,一樣沒有添加壓縮密碼邏輯,小夥伴們須要的本身添加吧
def static zipObb(File[] fs, String zipFilePath) {
if (fs == null) {
throw new NullPointerException("fs == null");
}
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFilePath)));
for (File file : fs) {
if (file == null || !file.exists()) {
continue;
}
compress(file, zos, file.getName());
}
zos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(zos != null){
try {
zos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//內部遞歸壓縮方法
def static compress(File sourceFile, ZipOutputStream zos, String name) throws Exception {
byte[] buf = new byte[2048];
if (sourceFile.isFile()) {
// 向zip輸出流中添加一個zip實體,構造器中name爲zip實體的文件的名字
zos.putNextEntry(new ZipEntry(name));
// copy文件到zip輸出流中
int len;
FileInputStream inputStream = new FileInputStream(sourceFile);
while ((len = inputStream.read(buf)) != -1) {
zos.write(buf, 0, len);
}
// Complete the entry
zos.closeEntry();
inputStream.close();
} else {
File[] listFiles = sourceFile.listFiles();
if (listFiles == null || listFiles.length == 0) {
// 須要保留原來的文件結構時,須要對空文件夾進行處理
zos.putNextEntry(new ZipEntry(name + "/"));
// 沒有文件,不須要文件的copy
zos.closeEntry();
} else {
for (File file : listFiles) {
compress(file, zos, name + "/" + file.getName());
}
}
}
}
def String getCurrentFlavor() {
........
}
複製代碼
咱們已經在flavour.gradle中添加了獲取當前渠道和壓縮文件的方法了,如今回到app下的build.gradle文件中,經過判斷當前渠道是否GooglePaly,對須要壓縮的全部文件進行壓縮,並輸出到googlePlay渠道包apk的同級目錄下:
apply from: "../flavour.gradle"
//添加到文件最後
//自動打包擴展文件obb
task zipObb(type: JavaExec) {
//判斷是否GooglePlay渠道包,獲取渠道包的時候作了小寫處理
if (getCurrentFlavor().equals("googleplay")) {
//獲取debug仍是release模式輸出到不一樣地址
String outputFilePath
if(gradle.startParameter.taskNames.toString().contains("Debug")){
outputFilePath = "app/build/outputs/apk/GooglePlay/debug/main." + android.defaultConfig.versionCode + ".com.example.testobb.obb"
}else{
outputFilePath = "app/GooglePlay/release/main." + android.defaultConfig.versionCode + ".com.example.testobb.obb"
}
File file = new File('app/src/tencent/res/raw/splash.mp4')
//此處添加更多文件 也能夠經過配置文件的方式輸入須要打包obb的全部資源文件
File[] files = new File[]{file}
zipObb(files, outputFilePath)
}
}
複製代碼
至此,咱們的多渠道打包和自動化生成obb就實現了。
如發現任何錯誤或有不明白的地方能夠留言。