obb制作官方文档参考:
Google Play应用商店在上传限制100MB大小,超过该大小的应用必须将超过部分以扩展文件的形式进行上传处理,总共可上传2个扩展文件,每个最大文件可为2GB,同时obb文件格式【扩展文件可以使用任何文件格式(ZIP, PDF, MP4, 等)。不管任何文件格式Android都认为他们是obb(opaque binary blobs)文件】可自选。
对每个App而言,该目录下最多只能包含2个扩展文件。一个是main扩展文件另外一个是patch扩展文件,所以一般只需要处理main扩展文件。
[main|patch].<expansion-version>.<package-name>.obb
文件名包含四部分,其中:
例如,假设您的APK版本是(versionCode)25,你的包名为com.example.app。如果上传的主扩展文件,该文件被重命名为:
main.25.com.example.app.obb
注意:
制作成OBB(Opaque Binary Blob)格式文件的一种工具,在Android SDK中, %ANDROID——HOME/tools%中,在tools/bin下有jobb。
命令:
jobb -d [目录名称的完整路径] -o [输出目标文件的完整路径] -pn [软件包名] -pv [包版本]
注:如果没有将android sdk配置环境就需要打开命令窗口后将jobb.bat拖入cmd窗口内才能执行
EG:
源文件目录 D:contents\main\assets\ 目标文件夹: D:\obb\output.obb 软件包名 com.example.app 包版本 25
jobb -d D:\contents\ main\assets\ -o D:\obb\output.obb -pn com.example.app -pv 25
注意:
选项 |
|
-d | 设置用于创建OBB文件的输入目录,或在提取(-dump)现有文件时设置输出目录。创建OBB文件时,指定目录及其所有子目录的内容都包含在OBB文件系统中。 |
-o | 指定OBB文件的文件名。创建OBB并提取(转储)其内容时,此参数是必需的。 |
-pn | 指定安装OBB文件的应用程序的软件包名称,该名称对应package于应用程序清单中指定的值。创建OBB文件时需要此参数。 |
-pv | 设置可以挂载OBB文件的应用程序的最低版本,该版本对应android:versionCode于应用程序清单中的值。创建OBB文件时需要此参数。 |
-k | 指定用于加密新OBB文件或解密现有加密OBB文件的密码。 |
-ov | 创建OBB文件,该文件是现有OBB文件结构的叠加层。此选项允许将新包装内容装入与先前包装相同的位置,并用于创建以前生成的OBB文件的修补程序版本。覆盖OBB文件中的文件替换具有相同路径的文件。 |
-dump | 提取指定OBB文件的内容。使用此选项时,还必须使用-d 参数指定内容的输出目录。 |
|
|
-v | 设置该工具的详细输出。 |
-about | 显示该jobb工具的版本和帮助信息。 |
将需要压缩的文件放入assets 或者根据目录中在使用winRAR进行压缩。
选中assets文件夹右击—添加到压缩文件,打开下面的窗口,根据命名规则进行命名。
当Android Market下载程序的扩展文件的时候会保存到系统的共享存储区。为了确保程序正常运行,您不能删除、移动或者重命名扩展文件。在某些设备上Market无法自动下载该扩展文件,那么您应该在程序启动的时候去下载该文件并且保存到同样的位置。
扩展文件保存位置如下:
<shared-storage>/Android/obb/<package-name>/
<shared-storage> 代表共享文件的目录路径,通过函数getExternalStorageDirectory()获取;
<package-name> APK的Java包名。
其中shared-storage是设备的primary external storage。
对于每个App而言,该目录下最多只能包含2个扩展文件。一个是main扩展文件另外一个是patch扩展文件。当更新程序的时候,如果有新的扩展文件则新文件会覆盖旧的扩展文件。
如果您需要解压缩扩展文件来使用,请注意不要删除该.obb文件,并且也不要把文件解压缩到该目录。您应该把解压缩后的文件保存到getExternalFilesDir()返回的目录下面。如果有可能的话,最好使用程序能直接读取的文件格式而不用再次解压缩文件了。Android开发团队提供了一个项目( APK Expansion Zip Library)可以直接读取ZIP文件中的内容而不用解压缩该文件.
需要注意的是:保存在系统共享存储区的文件,用户和其他APP也可以访问。
要在App中使用扩展文件,需要两个附加的Android库项目:
可以通过Android SDK Manager来下载,也可以直接通过如下链接下载:
https://dl-ssl.google.com/android/repository/market_licensing-r02.zip
https://dl-ssl.google.com/android/repository/market_apk_expansion-r01.zip
下载完成后使用market_licensing-r02.zip文件中的目录google_market_licensing\library来创建一个库项目;
然后使用market_apk_expansion-r01.zip中的google_market_apk_expansion\downloader_library来创建另外一个库项目。
同时为了简化对ZIP格式扩展文件的处理,在market_apk_expansion-r01.zip文件中还包含了一个对ZIP文件处理的库项目:google_market_apk_expansion\zip_file。 如果您使用的扩展文件格式是ZIP,那么也可以创建这个库项目。
<manifest...>
<uses-permissionandroid:name="com.android.vending.CHECK_LICENSE"/>
<uses-permissionandroid:name="android.permission.INTERNET"/>
<uses-permissionandroid:name="android.permission.WAKE_LOCK"/>
<uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permissionandroid:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
...
manifest>
注意:默认情况下,下载库项目需要的API level为4 而APK扩展ZIP库项目需要API level为5.
准备工作完成后,下面来具体看看如何使用扩展文件。
为了实现在后台下载文件,下载库项目提供了一个Service实现,名称为DownloaderService。您应该继承自这个文件来实现您的下载服务。为了简化下载服务的开发,该DownloaderService还实现了如下功能:
检测共享存储区挂载了并且可用,在下载文件之前检测 文件是否已经存在、存储空间是否足够。如果出现问题就通知用户。
您仅仅需要创建一个继承自DownloaderService的类,并且实现如下三个函数即可:
getPublicKey():您Market账号的 Base64 编码 RSA 公共密钥,可以通过如下网址获取:
https://market.android.com/publish/Home#ProfileEditorPlace:
getSALT(): 许可策略用来生成混淆器(Obfuscator)的一组随机bytes。
getAlarmReceiverClassName(): 返回您程序中用来重启下载进程的BroadcastReceiver类名称。当某些情况下,下载服务被意外终止的时候通过该BroadcastReceiver类来重新下载。比如 进程管理的程序终止了下载服务。
public class SampleDownloaderService extends DownloaderService {
// You must use the public key belonging to your publisher account
public static final String BASE64_PUBLIC_KEY ="YourAndroidMarketLVLKey";
// You should also modify this salt
public static final byte[] SALT =new byte[] {1,42, -12, -1,54,98,
-100, -12,43,2, -8, -4,9,5, -106, -107, -33,45, -1,84
};
@Override
public String getPublicKey() {
return BASE64_PUBLIC_KEY;
}
@Override
public byte[] getSALT() {
return SALT;
}
@Override
public String getAlarmReceiverClassName() {
return SampleAlarmReceiver.class.getName();
}
}
然后 在Manifest文件中声明该Service即可。非常简单吧!
...>
".SampleDownloaderService" />
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
try {
PrintLog.d("AlarmReceiver startDownloadServiceIfRequired");
DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent,
YRDownloaderService.class);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
}
}
然后 在Manifest文件中声明该Receiver即可。非常简单吧!
...>
".AlarmReceiver" />
程序的主Activity(通过Launcher图标启动的Activity)应该负责检查扩展文件是否存在、如果不存在就启动下载服务。
使用Downloader Library来下载需要遵守如下步骤:
1)检查文件是否已经下载了
Downloader Library中的Helper类中包含了一些函数来简化这个步骤:
getExtendedAPKFileName(Context, c, boolean mainFile, int versionCode)
doesFileExist(Context c, String fileName, long fileSize)
例如在示例项目中,在Activity的onCreate()函数中通过如下函数来检查文件是否存在:
boolean expansionFilesDelivered() {
for(XAPKFile xf : xAPKS) {
String fileName = Helpers.getExpansionAPKFileName(this, xf.mIsBase, xf.mFileVersion);
if(!Helpers.doesFileExist(this, fileName, xf.mFileSize,false))
returnfalse;
}
returntrue;
}
这里的XAPKFile对象保存了已知扩展文件的版本号和大小以及是否为main扩展文件。如果该函数返回false则启动下载服务。
2)通过 DownloaderClientMarshaller.startDownloadServiceIfRequired(Context c, PendingIntent notificationClient, ClassserviceClass)该函数来开始下载。
该函数的参数如下:
这个函数返回一个整数来表示是否有必要下载文件。有如下几个值:
例如:
@Override
public void onCreate(Bundle savedInstanceState) {
// Check if expansion files are available before going any further
if(!expansionFilesDelivered()) {
// Build an Intent to start this activity from the Notification
Intent notifierIntent =newIntent(this, MainActivity.getClass());
notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TOP);
...
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,
notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Start the download service (if required)
intstartResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(this,
pendingIntent, SampleDownloaderService.class);
// If download has started, initialize this activity to show download progress
if(startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
// This is where you do set up to display the download progress (next step)
...
return;
}// If the download wasn't necessary, fall through to start the app
}
startApp();// Expansion files are available, start the app
}
3) 当 startDownloadServiceIfRequired() 函数的返回值不是NO_DOWNLOAD_REQUIRED的时候,
调用DownloaderClientMarshaller.CreateStub(IDownloaderClient client, ClassdownloaderService)函数来创建一个IStub实例。这个IStub实例提供了Activity和下载服务之前的绑定功能,这样您的Activity就可以收到下载事件了。
CreateStub()函数需要一个实现了IDownloaderClient接口的类和DownloaderService的实现类作为参数。一般而言只要让Activity实现IDownloaderClient接口即可。
Android开发团队推荐在Activity的onCreate()函数中创建IStub对象(在startDownloadServiceIfRequired()函数之后创建)。
例如:
// Start the download service (if required)
int startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(this,
pendingIntent, SampleDownloaderService.class);
// If download has started, initialize activity to show progress
if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
// Instantiate a member instance of IStub
mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this,
SampleDownloaderService.class);
// Inflate layout that shows download progress
setContentView(R.layout.downloader_ui);
return;
}
当onCreate()函数返回以后,Activity会执行onResume()函数,在该函数中调用IStub的 connect() 函数。同样在onStop()函数中调用IStub的 disconnect()函数。
例如:
@Override
protectedvoidonResume() {
if(null!= mDownloaderClientStub) {
mDownloaderClientStub.connect(this);
}
super.onResume();
}
@Override
protectedvoidonStop() {
if(null!= mDownloaderClientStub) {
mDownloaderClientStub.disconnect(this);
}
super.onStop();
}
调用connect()用来绑定Activity和DownloaderService 。
要接收下载进度信息,需要实现IDownloaderClient 接口。该接口有如下函数:
onServiceConnected(Messenger m)
在初始化完IStub后,会回调该函数。该函数的参数是用来访问您的DownloaderService的,通过 DownloaderServiceMarshaller.CreateProxy()函数来创建这个IDownloaderService对象,然后可以用这个对象来控制下载服务,比如 暂停、继续下载等。
推荐的实现方式:
private IDownloaderService mRemoteService;
...
@Override
public void onServiceConnected(Messenger m) {
mRemoteService = DownloaderServiceMarshaller.CreateProxy(m);
mRemoteService.onClientUpdated(mDownloaderClientStub.getMessenger());
}
onDownloadStateChanged(int newState)
当下载状态发生变化的时候调用该函数,例如 开始下载或者下载完成。
参数newState的值是IDownloaderClient接口中定义的一些常量之一(以 STATE_ 开头的);
可以通过函数 Helpers.getDownloaderStringResourceIDFromState()来获取一个状态的文本描述,这样用户更容易理解。例如 STATE_PAUSED_ROAMING 对应的文本描述是: “Download paused because you are roaming/当前在漫游状态,下载停止”
onDownloadProgress(DownloadProgressInfo progress)
该函数的参数DownloadProgressInfo包含了下载进度的各种信息,例如 预计完成时间、当前下载速度、完成的百分比等。可以根据该信息来更新下载界面。
另外还有一些有用的函数:
将APK扩展文件下载下来之后,紧接着我们就要考虑如何使用它了。但是Android 6.0的一些实现(API级别23)以及以后的一些实现仍然需要权限,因此需要在应用程序清单中声明存储权限,并在运行时请求外部存储权限,如下所示:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
或者
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
在使用obb扩展文件前需要将Zip Library的库移入自己的项目中进行依赖使用此库可以轻松地将ZIP扩展文件中的资源作为虚拟文件系统读取。
Tip:(Zip Library位于其中/extras/google/google_market_apk_expansion/zip_file/)
使用APKExpansionSupport类来获取obb中的资源
提供一些访问扩展文件名和ZIP文件的方法:
首先要获取扩展文件的路径,可以通过如下代码完成该操作:
// The shared path to all app expansion files
private final static String EXP_PATH ="/Android/obb/";
static String[] getAPKExpansionFiles(Context ctx, intmainVersion,intpatchVersion) {
String packageName = ctx.getPackageName();
Vector ret =newVector();
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
// Build the full path to the app's expansion files
File root = Environment.getExternalStorageDirectory();
File expPath =newFile(root.toString() + EXP_PATH + packageName);
// Check that expansion file path exists
if(expPath.exists()) {
if( mainVersion >0) {
String strMainPath = expPath + File.separator +"main."+
mainVersion +"."+ packageName +".obb";
File main =newFile(strMainPath);
if( main.isFile() ) {
ret.add(strMainPath);
}
}
if( patchVersion >0) {
String strPatchPath = expPath + File.separator +"patch."+
mainVersion +"."+ packageName +".obb";
File main =newFile(strPatchPath);
if( main.isFile() ) {
ret.add(strPatchPath);
}
}
}
}
String[] retArray =newString[ret.size()];
ret.toArray(retArray);
returnretArray;
}
ProvinceUtil provinceUtil = new ProvinceUtil();
ZipResourceFile expansionFile = null;
try {
expansionFile = APKExpansionSupport.getAPKExpansionZipFile(getApplicationContext(), BuildConfig.VERSION_CODE, 0);
if (expansionFile != null) {
AssetFileDescriptor fd =
expansionFile.getAssetFileDescriptor("assets/my_province_data.txt");
PrintLog.e("-------" + fd.getLength());
InputStream inputStream = expansionFile.getInputStream("assets/my_province_data.txt");
if (inputStream != null) {
provinceUtil.initProvinceDatas(inputStream);
String[] provinces = provinceUtil.getmProvinceDatas();
if (provinces != null) {
//参考ArrayAdapter的构造函数
listView.setAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1,
provinces));
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}