android APK Expansion Files

UE5安卓开发环境配置:XGE编译加速与Vulkan预览全攻略 在跨平台游戏开发中,安卓环境配置是连接PC创作与移动设备部署的关键桥梁,涉及SDK、NDK、JDK等核心工具链的集成。其原理在于通过原生编译工具链将C++代码转化为安卓设备可执行的本地库,并利用现代图形API实现高效渲染。这一过程的技术价值在于构建稳定、高效的移动端开发工作流,直接影响迭代速度与最终性能表现。应用场景广泛覆盖从独立游戏到大型商业项目的安卓平台移植与优化。本文聚焦于通过配置XGE分布式编译来大幅缩短代码构建时间,并启用Vulkan渲染后端实现编辑器内的实时安卓画面预览,从而解决编译效率低下与图 阅读详情

说在前面本篇文章是在 APK扩展文件及使用基础上修改和补充

一、APK扩展文件基本知识

Android Market (Google Play Store)中每个APK文件的最大限制是100MB。如果您的程序中包含大量的数据文件,以前您只能把这些数据文件放到自己的服务器上,当用户启动程序的时候让用户去下载。现在这些数据文件可以直接上传到Android Market了。在新的Market控制台上传App的时候,可以添加扩展文件了。

下面就来看看什么时候该使用扩展文件,该如何使用?

每个APK可以有2个扩展文件,每个文件最大限制是2GB。为了减少用户的带宽消耗,最好使用压缩格式文件吧。 这两扩展文件具有不同的用途: 第一个被称为 main (主)扩展文件,该扩展文件保护您程序中需要用到的附加数据; 第二个被称为 patch 扩展(修补)文件,该文件是可选的,并且应该只包含一些不同版本的补丁数据。

当然您可以按照您需要的方式来使用这两个扩展文件,不过Android官方还是推荐把这两个文件的功能分开。main扩展文件包含核心数据,并且尽量不随程序版本的升级去修改;而patch扩展文件可以随程序版本的升级做修改。为了帮助大家理解具体的含义,我们使用一个地图App来解释下: 比如 Google 地图程序需要包含一个离线地图数据包,这样可以方便用户离线查看地图,在程序发布的时候,可以把现有的离线数据包作为main扩展文件上传到Market。 然后过了半年Google地图更新了,新添加了一些刚刚修好的高速公路、新建立的商场 等信息,可以把这些新增的信息作为patch扩展文件使用。 这样Google 地图 1.0版本对应一个main扩展文件;而Google地图1.1版本对应一个main扩展文件和一个1.1版本的patch扩展文件;Google地图1.2版本对应一个main扩展文件和一个1.2版本的patch扩展文件。 这里面的main扩展文件是同一个文件而patch扩文件是随版本变化的。 这样的好处就是当程序升级的时候, 用户不用重新下载main扩展文件了,只需要下载少量的新增文件即可,节省用户流量。

二、扩展文件的命名格式

扩展文件可以使用任何文件格式(ZIP, PDF, MP4, 等)。不管任何文件格式Android都认为他们是obb(opaque binary blobs)文件,并且会根据如下文件命名规则来重命名扩展文件:

[main|patch]...obb

main or patch      指定文件是main扩展文件还是patch扩展文件,每个APK只能有一个main扩展文件和一个patch扩展文件。

和第一次上传该扩展文件的APK文件的android:versionCode一致。后续版本的APK可以重用前面上传的扩展文件。 您程序的Java包名

   例如程序的版本为5,程序的包名为org.goodev.expansion.downloader。则上传的main扩展文件会被重命名为:main.5.org.goodev.expansion.downloader.obb

打成obb包的方法:

1.可以用android Jobb 传送门

2.推荐使用比较简单的方式

传送门

三、扩展文件的保存位置

当Android Market下载程序的扩展文件的时候会保存到系统的共享存储区。为了确保程序正常运行,您不能删除、移动或者重命名扩展文件。在某些设备上Market无法自动下载该扩展文件,那么您应该在程序启动的时候去下载该文件并且保存到同样的位置。 扩展文件保存位置如下: /Android/obb//

代表共享文件的目录路径,通过函数getExternalStorageDirectory()获取; APK的Java包名。 对于每个App而言,该目录下最多只能包含2个扩展文件。一个是main扩展文件另外一个是patch扩展文件。当更新程序的时候,如果有新的扩展文件则新文件会覆盖旧的扩展文件。

如果您需要解压缩扩展文件来使用,请注意不要删除该.obb文件,并且也不要把文件解压缩到该目录。您应该把解压缩后的文件保存到getExternalFilesDir()返回的目录下面。如果有可能的话,最好使用程序能直接读取的文件格式而不用再次解压缩文件了。Android开发团队提供了一个项目( APK Expansion Zip Library)可以直接读取ZIP文件中的内容而不用解压缩该文件. 需要注意的是:保存在系统共享存储区的文件,用户和其他APP也可以访问。

四、下载扩展文件的流程

在大多数情况下,Market会在下载APK的同时去下载扩展文件。然而,在某些情况下Market无法下载扩展文件或者用户删除了以前下载的扩展文件,您的程序需要处理这种异常情况。当您的程序启动的时候,可以检测文件是否存在并且可以从Market上下载。

开发者检查清单:

您可以通过下面的清单来检查是否需要使用扩展文件

1.您的程序是否真的需要超过50MB的大小限制。在移动设备上空间是非常宝贵的,您应该尽可能减少App的大小。如果您仅仅是为了提供支持多种显示设备的图片资源的话,可以考虑使用发布多个APK的方式来减少每个APK的大小。
2.判断哪些数据需要打包为扩展文件发布。
3.在程序中添加访问共享存储区中扩展文件的代码
4.在程序的启动Activity中添加检测扩展文件是否存在,以及下载扩展文件的代码

五、扩展文件的规则和限制

1.每个扩展文件最大为2GB
2.用户必需要从Android Market获取您的程序才能自动从Market中下载扩展文件
3.当在您的程序中下载扩展文件的时候,Market每次都会为每个文件生成一个唯一的下载URL,该URL会在短期内失效。
4.当你上传一个新的APK的时候,可以选择使用以前上传的扩展文件
5.如果您使用多个APK文件来适配不同的设备,并且也希望使用多个扩展文件。为了获取一个唯一的versionCode和不同的Market filter, 您需要分别为每个设备上传不同的APK文件。
6. 不能通过更新扩展文件来发布一个新的版本。
7. 不要在obb/文件夹中保存其他数据
8.不要删除或者重命名.obb文件

六、APK扩展文件使用实例

要在App中使用扩展文件,需要两个附加的Android库项目:

1)Google Market Licensing package

2)Google Market APK Expansion Library package

可以通过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,那么也可以创建这个库项目。

注意:

这两个库可能很久都没有维护了,所以是存在一些bug的。有三个bug必须改掉

1.在com.google.android.vending.licensing.LicenseChecker类的checkAccess()中,有这样一段代码

boolean bindResult = mContext.bindService(new Intent(new String(Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))),
                            this, // ServiceConnection.
                            Context.BIND_AUTO_CREATE);

这段在代码在Android5.0上会抛出IllegalArgumentException,需要替换成如下代码。

Intent serviceIntent = new Intent(new String(Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")));
serviceIntent.setPackage("com.android.vending");

2.在com.google.android.vending.expansion.downloader.DownloaderClientMarshaller类的connect()方法中,有这样一段代码。

if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) {
 if ( Constants.LOGVV ) {
     Log.d(Constants.TAG, "Service Unbound");
 }
} else {
 mBound = true;
}

这里会导致绑定的服务在某些情况下无法启动,服务不启动,IDownloaderClient接口的onServiceConnected()方法就不会执行,mRemoteService为null,从而导致NullPointerException。虽然在使用mRemoteService前增加对其是否为null的判断可以避免crash,但是下载过程仍然无法监控,无法得到下载的结果。需要将这段代码替换成如下代码。也就是将BIND_DEBUG_UNBIND替换成BIND_AUTO_CREATE。

if ( !c.bindService(bindIntent, mConnection, Context.BIND_AUTO_CREATE) ) {
 if ( Constants.LOGVV ) {
     Log.d(Constants.TAG, "Service Unbound");
 }
 } else {
     mBound = true;
}

3.当执行到com.google.android.vending.expansion.downloader.impl.DownloadNotification类中的onDownloadStateChanged()方法时

mCurrentNotification.tickerText = mLabel + ": " + mCurrentText;
mCurrentNotification.icon = iconResource;
mCurrentNotification.setLatestEventInfo(mContext, mCurrentTitle, mCurrentText,mContentIntent);

在高版本中会报错。解决办法可以用Notification.Builder

private Notification.Builder mBuilder;

...
mBuilder.setContentTitle(mLabel + ": " + mCurrentText);
mBuilder.setSmallIcon(iconResource);
mBuilder.setContentIntent(mContentIntent);

在onDownloadProgress()方法中

mNotification.tickerText = mCurrentTitle;
mNotification.icon = android.R.drawable.stat_sys_download;
mNotification.setLatestEventInfo(mContext, mLabel, mCurrentText, mContentIntent);

修改成:

mBuilder.setContentTitle(mCurrentTitle);
mBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
mBuilder.setContentIntent(mContentIntent);

在DownloadNotification()方法中修改:

mBuilder = new Notification.Builder(ctx);
mNotification = mBuilder.getNotification();
mCurrentNotification = mNotification;

传送门:这里是修改好的library密码是:qk4c

1.声明需要的权限

    <manifest...>
    <!-- Required to access Android Market Licensing -->
    <uses-permission android:name="com.android.vending.CHECK_LICENSE"/>  

    <!-- Required to download files from Android Market -->
    <uses-permission android:name="android.permission.INTERNET"/>

    <!-- Required to keep CPU alive while downloading files (NOT to keep screen awake) -->
    <uses-permission android:name="android.permission.WAKE_LOCK"/>

    <!-- Required to poll the state of the network connection and respond to changes -->
    <uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE"/>

    <!-- Required to check whether Wi-Fi is enabled -->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>


    <!-- Required to read and write the expansion files on shared storage -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     ...

    </manifest>

注意:默认情况下,下载库项目需要的API level为4 而APK扩展ZIP库项目需要API level为5.

准备工作完成后,下面来具体看看如何使用扩展文件。

2.实现下载服务(Implementing the downloader service)

为了实现在后台下载文件,下载库项目提供了一个Service实现,名称为DownloaderService。您应该继承自这个文件来实现您的下载服务。为了简化下载服务的开发,该DownloaderService还实现了如下功能:

注册一个BroadcastReceiver来监听设备的网络连接状态的改变。如果网络连接断开就暂停下载;如果网络连接恢复就继续下载。
安排一个 RTC_WAKEUP 通知,当下载服务被终结的时候可以通过该通知来启动下载服务
生成一个通知(Notification )来显示下载的进度以及下载错误等状态
允许您的程序手工的暂停和恢复下载
检测共享存储区挂载了并且可用,在下载文件之前检测 文件是否已经存在、存储空间是否足够。如果出现问题就通知用户。
您仅仅需要创建一个继承自DownloaderService的类,并且实现如下三个函数即可:

getPublicKey():您Market账号的 Base64 编码 RSA 公共密钥,可以通过如下网址获取: https://market.android.com/publish/Home#ProfileEditorPlace:

getSALT(): 许可策略用来生成混淆器(Obfuscator)的一组随机bytes。

getAlarmReceiverClassName(): 返回您程序中用来重启下载进程的BroadcastReceiver类名称。当某些情况下,下载服务被意外终止的时候通过该BroadcastReceiver类来重新下载。比如 进程管理的程序终止了下载服务。

下面是一个DownloaderService类的实现代码:、

public  class SampleDownloaderService extends DownloaderService {
// You must use the public key belonging to your publisher account
publicstaticfinalString BASE64_PUBLIC_KEY ="YourAndroidMarketLVLKey";

// You should also modify this salt
publicstaticfinalbyte[] SALT =newbyte[] {1, 42, -12, -1, 54, 98,
        -100, -12, 43, 2, -8, -4, 9, 5, -106, -107, -33, 45, -1, 84
};

@Override
publicString getPublicKey() {
    returnBASE64_PUBLIC_KEY;
}

@Override
publicbyte[] getSALT() {
    returnSALT;
}

@Override
public String getAlarmReceiverClassName() {
    returnSampleAlarmReceiver.class.getName();
}
}

YourAndroidMarketLVLKey:这里要填写google play后台的publickey

然后在manifest文件中声明该Service即可。

<application ...>
    <service android:name=".SampleDownloaderService" />
    ...
</application> 

3.实现 alarm receiver

为了检测下载进程和重启下载服务,DownloaderService会安排一个RTC_WAKEUP Alarm来发送一个Intent到程序的 BroadcastReceiver。你必需定义这个 BroadcastReceiver 来调用 Downloader Library提供的函数,通过该函数来检测下载状态和在必要的情况下重启下载服务。

实现这个类也是非常简单的,一般来说只要覆写onReceive()函数并且调用DownloaderClientMarshaller.startDownloadServiceIfRequired()函数即可。如下所示:

public  class SampleAlarmReceiver extendsBroadcastReceiver {
@Override
publicvoidonReceive(Context context, Intent intent) {
    try{
        DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent,
                SampleDownloaderService.class);
    }catch(NameNotFoundException e) {
        e.printStackTrace();
    }
    }
}

注意这个类的名字就是前面getAlarmReceiverClassName()函数返回的名称。

然后在manifest文件中声明该receiver即可。

<application ...>
    <receiver android:name=".SampleAlarmReceiver" />
    ...
</application>  

4.开始下载扩展文件

程序的主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))
        return false;
}
return true;
}

这里的XAPKFile对象保存了已知扩展文件的版本号和大小以及是否为main扩展文件。如果该函数返回false则启动下载服务。

在使用XAPKFile时要注意:

private static final XAPKFile[] xAPKS = {
        new XAPKFile(
                true, // true signifies a main file
                8, // the version of the APK that the file was uploaded
                // against
                11933257 // the length of the file in bytes
        )
};

参数解释:

true:main扩展文件

8:是当前应用的版本号

11933257:可以用cmd命令dir 直接查看

2)通过 DownloaderClientMarshaller.startDownloadServiceIfRequired(Context c, PendingIntent notificationClient, ClassserviceClass)该函数来开始下载。 该函数的参数如下: context: Your application’s Context. notificationClient: 用来启动主Activity的PendingIntent。用在DownloaderService 创建的用来显示下载进度的通知中。当用户选择该通知,系统调用该PendingIntent来打开显示下载进度的Activity(一般而言就是启动下载的Activity)。 serviceClass: 程序中继承自DownloaderService的类。在必要的情况下会启动该服务来开始下载。

这个函数返回一个整数来表示是否有必要下载文件。有如下几个值:

NO_DOWNLOAD_REQUIRED: 表示文件已经存在或者当前正在下载。
LVL_CHECK_REQUIRED:表示需要授权验证来获取下载扩展文件的URL。
DOWNLOAD_REQUIRED: 表示扩展文件的URL已经获取到了,但是还没开始下载。
LVL_CHECK_REQUIRED 和 DOWNLOAD_REQUIRED 在本质上是一样的,一般而言您无需关注这个状态。在您的主Activity中调用 startDownloadServiceIfRequired(),你只需要看看返回值是否为NO_DOWNLOAD_REQUIRED即可。如果返回值不是NO_DOWNLOAD_REQUIRED, Downloader Library 开始启动下载,您应该更新程序界面来显示下载进度;如果返回值是 NO_DOWNLOAD_REQUIRED,表明该文件已经下载好了,您的程序可以正常启动了。

例如:

@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 。

  1. 处理下载进度

    要接收下载进度信息,需要实现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包含了下载进度的各种信息,例如 预计完成时间、当前下载速度、完成的百分比等。可以根据该信息来更新下载界面。

    另外还有一些有用的函数: requestPauseDownload() 暂停下载 requestContinueDownload() 恢复下载 setDownloadFlags(int flags) 设置下载的网络标示。当前只支持一个标示:FLAGS_DOWNLOAD_OVER_CELLULAR。 通过移动网络下载扩展文件。默认情况下该标示没有启用,所以默认情况下只通过WIFI下载。

  2. 读取扩展文件

    首先要获取扩展文件的路径,可以通过如下代码完成该操作:

    // 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 = new Vector();
    if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {

    // Build the full path to the app's expansion files
    File root = Environment.getExternalStorageDirectory();
    File expPath =new File(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 = new File(strMainPath);
            if( main.isFile() ) {
                    ret.add(strMainPath);
            }
        }
    
        if( patchVersion >0) {
            String strPatchPath = expPath + File.separator +"patch."+
                    mainVersion +"."+ packageName +".obb";
            File main = new File(strPatchPath);
            if( main.isFile() ) {
                    ret.add(strPatchPath);
            }
        }
    }
    

    }

    String[] retArray = new String[ret.size()];
    ret.toArray(retArray);
    return retArray;
    }

您可以在开始下载的时候,把扩展文件的版本号保存到 SharedPreferences 中,然后在这里使用。

  1. 使用 APK Expansion Zip Library

    APK Expansion Zip Library项目包含了对ZIP文件的处理,您可以通过该项目提供的函数来直接读取ZIP文件内容而不用解压缩扩展文件,它把ZIP扩展文件当一个虚拟文件系统来使用。

    APK Expansion Zip Library项目包含如下类和函数: APKExpansionSupport 提供一些函数来访问扩展文件名称和ZIP文件。

    getAPKExpansionFiles() 返回扩展文件的文件路径

    getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion) 返回一个包含main扩展文件和patch扩展文件的ZipResourceFile。如果您同时提供了 mainVersion 和 patchVersion ,则该函数返回main和patch扩展文件的所有内容,如果patch中的内容和main中的有重复,则使用patch的内容覆盖main中的内容。

    ZipResourceFile 用来处理ZIP文件的类 getInputStream(String assetPath) 读取ZIP文件中的具体文件,assetPath应该是相对于ZIP文件的路径信息

    getAssetFileDescriptor(String assetPath) 获取ZIP文件中具体文件的 AssetFileDescriptor 信息。

    APEZProvider 大多数的程序都不会用到这个类。具体情况请参考其文档。

    使用APK 扩展ZIP库从ZIP文件中读取文件参考代码如下:

    // Get a ZipResourceFile representing a merger of both the main and patch files
    ZipResourceFile expansionFile = APKExpansionSupport.getAPKExpansionZipFile(appContext,
    mainVersion, patchVersion);

    // Get an input stream for a known file inside the expansion file ZIPs
    InputStream fileStream = expansionFile.getInputStream(pathToFileInsideZip);

    上面的代码可以读取main扩展文件或patch扩展文件(通过读取两个文件的合并文件来实现)中的任何文件

    如果要读取指定的扩展文件,其方法如下:

    // Get a ZipResourceFile representing a specific expansion file
    ZipResourceFile expansionFile = new ZipResourceFile(filePathToMyZip);

    // Get an input stream for a known file inside the expansion file ZIPs
    InputStream fileStream = expansionFile.getInputStream(pathToFileInsideZip);

  2. 测试扩展文件

    在发布之前要测试两个东东,下载文件和读取文件。

    测试读取文件 在发布您的程序之前应该先测试下您的程序能否读取扩展文件,测试很简单,只要把扩展文件放到共享存储区的特殊位置,然后启动程序即可。
    1)创建文件目录: 如果程序的包名为org.goodev,就创建如下的目录:Android/obb/org.goodev/
    2)把扩展文件添加到该目录 如果程序的包名为org.goodev,则主扩展文件名如下:main.03.org.goodev.obb。 版本号可以为大于零的任意值。
    3)现在可以启动程序来测试读取扩展文件的功能了。

    测试下载文件 由于在某些情况下需要在程序第一次使用的时候手工下载扩展文件。所以需要测试来确保您的程序可以成功的获取下载URL、下载文件并且保存到设备中。

    您可以把程序上传到Market,同时上传扩展文件,然后不要发布程序。这样扩展文件已经可以从Market下载了。 当你测试完成后再发布您的程序。

9.更新程序

使用扩展文件的一大好处就是每次更新App 用户不用重新下载几十上百兆的数据文件了。Android Market让你可以为每个APK提供两个扩展文件,这样可以避免每次更新App都重新下载主扩展文件数据,减少下载时间。

为了方便大家研究如下使用扩展文件,可以到这里下载示例项目代码: http://code.google.com/p/goodev-demo-code/downloads/list 文件名:Market_Downloader_Sample.zip 里面包含了所需要的各种库项目。 在Eclipse中导入即可使用。

参考文献:

http://www.cnblogs.com/sevenyuan/p/3991118.html

http://blog.youkuaiyun.com/ccpat/article/details/51519821

https://developer.android.com/google/play/expansion-files.html

http://waychel.com/android-jobb/

http://zhixinliu.com/2015/03/13/2015-03-13-Androir-expansion-file/

http://www.magicsite.cn/blog/mobile/Android/Android66817.html

bug总结:
http://www.ithao123.cn/content-7871659.html

http://blog.youkuaiyun.com/tonyfield2015/article/details/11739035

http://www.cnblogs.com/stratrail/p/5195238.html

http://blog.youkuaiyun.com/ccpat/article/details/51519821

http://stackoverflow.com/questions/14495483/read-content-from-apk-expansion-file-from-obb-file#answer-15150977

http://stackoverflow.com/questions/14495483/read-content-from-apk-expansion-file-from-obb-file

http://sapandiwakar.in/how-to-set-up-android-app-to-support-expansion-files/

Unity开发Android动态壁纸:从原理到部署的完整指南 动态壁纸作为Android系统的一项特色功能,允许开发者在设备桌面上创建可交互的视觉体验。其核心原理在于通过WallpaperService在系统Surface上托管图形渲染进程,这为游戏引擎的集成提供了技术基础。Unity引擎凭借其强大的实时3D渲染能力和跨平台特性,成为实现高性能、高表现力动态壁纸的理想选择。通过将Unity运行时嵌入到Android动态壁纸服务中,开发者能够将复杂的粒子系统、3D场景和交互逻辑引入桌面环境,极大地拓展了动态壁纸的应用场景和创意空间。本文聚焦于Unity与Android动 阅读详情

相关推荐

UE4项目性能优化实战:从渲染管线到资源管理的全流程指南

在游戏开发中,性能优化是确保流畅用户体验和高效资源利用的核心工程实践。其原理在于平衡视觉表现与硬件负载,通过剖析CPU、GPU及内存瓶颈,系统性降低开销。这项技术的价值在于能显著提升帧率稳定性、控制包体大小并优化加载速度,直接影响产品的可玩性与商业表现。应用场景广泛覆盖移动端、PC及主机平台的游戏开发全周期。本文聚焦UE4引擎,深入探讨如何利用Stat命令、GPU Visualizer等工具进行深度性能剖析,并围绕渲染管线、资源管理及高效打包等关键环节,提供一套可复现的优化工作流与实战技巧,帮助开发者构建高

weixin_30443075的博客 1147

Android基础知识之apk签名权限

原文链接:http://developer.android.com/guide/google/play/licensing/adding-licensing.html 当你建立一个开发者账号和开发环境之后(见设置签名),你就可以在License Verification Library(LVL)给你的app添加签名了。 在LVL添加签名认证需要遵循下面这些步骤: 1.在你的应用的manife

Twofish 3149

Shell遍历目录文件的可靠写法与工程实践

在命令行自动化中,'遍历目录下文件'是基础却关键的技术动作,其本质是利用shell的路径名展开(pathname expansion)机制实现安全、可控的批量操作。原理上,`for file in *.txt`优于`ls | while read`,因前者由shell原生解析glob,规避子shell变量隔离、文件名空格/编码解析错误等风险;技术价值在于构建高鲁棒性脚本——通过`nullglob`选项、引号包裹、存在性校验和POSIX兼容设计,保障脚本在Git Bash、Zsh、ADB shell及麒麟系统

weixin_30444105的博客 337

APK Expansion Files / Obb 接入介绍

APK Expansion Files / Obb 接入介绍 APK Expansion Files 是谷歌官方提供的APK分包下载策略, 依赖工程已经包含在SDK中. 官方资料 文档 使用方法 1. 导入依赖工程 依赖工程放在SDK文件夹中/SDK/extras/google/文件夹下, 需要依赖market_apk_expansi...

weixin_33670786的博客 1838

Gemma 4端侧AI实战:手机离线多模态推理全解析

端侧AI是指在终端设备本地完成模型推理的人工智能技术,其核心原理是通过模型轻量化、硬件协同优化与异步流水线设计,在有限算力、内存和功耗约束下实现低延迟、高隐私的智能服务。技术价值在于摆脱云端依赖,保障数据不出设备、响应毫秒级、适配无网环境。典型应用场景包括移动端文档结构化提取、语音实时转写、图像局部理解与提示生成等高频生产力任务。本文聚焦Gemma 4-E4B这一代表性模型,深入解析其MatFormer架构、多模态原生融合机制及NPU/GPU协同部署实践,覆盖离线推理、手机本地部署、端侧多模态等关键热词,为

alexhill2009的博客 380

ubantu端使用命令直接在Windows端打开文件或目录

本文介绍了一种基于网络通信的跨平台文件访问方法,通过Ubuntu向Windows发送指令来打开共享文件。作者实现了面向过程的C程序,主要功能包括:1)获取IP地址和文件路径;2)转换路径格式为Windows可识别格式;3)通过curl命令向Windows端发送打开文件/目录的请求。在Ubuntu端编译程序并配置环境变量后,可直接通过fw命令触发操作。Windows端需运行Python脚本监听8080端口来接收并执行指令。该方法为Linux/Android底层开发人员提供了便捷的跨平台文件访问方案。

qq_44436156的博客 407

【Android开发】如何在Android中添加扩展文件?

每个安卓开发者 都知道,不可能在商店里推出大小超过100MB的安卓应用,这是每个开发者在开发具有大量资产的应用时应该保留的东西。随着资产的增加,我们寻找更可靠的技术来优化应用程序,如使用WebP 图像而不是 PNG ,或添加 SVG(矢量可画) ,或消除不需要的资源。有些应用程序需要动态图形或媒体文件或巨大的资产。安卓团队已经提供了一个扩展支持的选项。让我们看看如何为一个应用程序实现扩展。扩展文件是完全存档的文件格式,即不透明二进制Blob(.obb),一旦发布的APK在Google Play Consol

大模型学习路线 1126

com.android.vending.apk

com.android.vending.apk

APK Expansion Files使用文档

应用场景 当应用程序超过50M时,可以采用谷歌官方提供的APK Expansion Files.在Market控制台上传App时,可将多余的数据添加为扩展文件. 请注意:每个APK最多只能有两个扩展文件(Expansion File).这两个扩展文件通常被命名为main和patch. main是扩展文件包含核心数据,并且尽量不随程序版本的升级去修改;而patch扩展文件可以随程序版本的升级做

谷哥的小弟 7480

OpenLayers源码解析1 Map.js

Map.js 父类:./PluggableMap/PluggableMap 引用:5个模块 ./renderer/Composite.js ./PluggableMap.js ./obj.js ./control.js(控件) ./interaction.js(交互) 主要功能:核心组件,渲染地图 必要参数: view layers target controls和interactions要是未指定,使用默认值 方法:createRenderer()? /** * @module ol/Map */ i

qq_45005996的博客 1898

【AUTOSAR-COM】Transmission Mode 和 Transfer Property

1.COM模块的基本概念 首先在描述Transmission Mode 和 Transfer Property之前,有必要对COM模块的基本处理有一个直观的理解,我所描述的和对概念理解的也不一定完全正确。 Signal:指的是通信协议中的,一条报文中所保存的若干信号。 I-PDU:至少在COM模块中,可以理解为一条报文。 上图是在CANdb软件中编辑一个报文的Layout的界面,可以很形象的将I-PDU对等为这里的一条Message,而Signal就是需要在Message中需要摆放的Signal

tim_hoven的博客 4635

Android集成google license(License Verification Library)

当我们的应用上架google应用市场时,可能希望应用在被授权后才能使用。因此Google play为我们提供了许可服务,借助该服务,应用可以在运行时查询google play服务器,获取当前用户的许可状态,APP可以根据具体的情况禁止或允许用户进一步使用。以下是官方文档的链接:应用许可。 本博客的重点在于如何集成官方提供的License Verification Library,来实现权限验证。 1. 设置发布账号,并生成应用的许可公钥; 2. 下载LVL; git clone https://g

z372574152的博客 2455

分析方法—temporal receptive field(TRF)

总结来说,时间感受野其实就是一个 filter!课程中的描述—.(使用时间感受野与刺激进行卷积来描述)时间感受野一般是听觉计算中使用的,在视觉中,有另一种分析叫做空间感受野(spatial receptive fields)。回归权重(Regression weights)是指在回归分析中用于估计自变量与因变量之间关系的权重或系数。回归分析是一种统计方法,用于建立自变量与因变量之间的关联模型,并通过拟合数据来估计模型中的参数。在简单线性回归中,只有一个自变量与一个因变量之间的关系被建模。

m0_51474171的博客 1603

APK Expansion Files调查笔记

最近在调查APK Expansion Files的使用方法,在此过程中遇到不少问题,在此做一个总结 重要参考资料:   http://developer.android.com/google/play/expansion-files.html   http://blog.chengyunfeng.com/?p=342   http://yunfeng.sinaapp.com/?p=34

谷哥的小弟 4491

comsol如何实现五厚度的单元模拟?

🏆本文收录于 《全栈 Bug 调优(实战版)》 专栏。专栏聚焦真实项目中的各类疑难 Bug,从成因剖析 → 排查路径 → 解决方案 → 预防优化全链路拆解,形成一套可复用、可沉淀的实战知识体系。无论你是初入职场的开发者,还是负责复杂项目的资深工程师,都可以在这里构建一套属于自己的「问题诊断与性能调优」方法论,助你稳步进阶、放大技术价值 。

**My Coding Family** 471

大模型——AI对话四象限

{ "摘要": "文章揭示了90%的人不会有效与AI沟通的问题,指出简单粗暴的提问只能得到标准答案。作者提出'AI对话四象限'模型:1)双方都懂时提效;2)AI懂但需学会分层提问;3)共同探索未知领域;4)向AI传授独家知识。强调沟通技巧的重要性,认为会提问的人不会被AI替代。最后呼吁读者尝试四象限思维,与AI碰撞火花。" }

312

java解压obb,使用 OBB 文件(Android)

OBB 和 Wwise IO 的概述对于 Android 平台,当检查 Unity 选项 Player Settings > Publishing Settings > Split Application Binary 时,APK Expansion Files 用于封装工程素材。这样就有效地创建了一个有 .obb 扩展的 zip 文件,它包含 StreamingAssets 文件夹,...

weixin_42691388的博客 1435

流体仿真软件:COMSOL Multiphysics二次开发_(3).流体仿真基础理论

流体仿真基础理论包括流体的基本性质、流体运动方程、边界条件、初始条件、湍流模型和网格生成等。在使用COMSOL Multiphysics进行流体仿真时,正确设置这些参数和条件是获得准确仿真结果的关键。通过实际应用案例,可以更好地理解如何在COMSOL中进行流体仿真,并提取和分析仿真结果。希望本文对流体仿真初学者和从业人员有所帮助。

kkchenjj的博客 1341

APK Expansion Files

APK Expansion Files Quickview Recommended for most apps that exceed the 50MB APK limitYou can provide up to 4GB of additional data for each APKGoogle Play hosts and serves the expansion fi

Tonyfield的专栏 3059

AI 大模型接口太零散?自建私有模型管理平台统一整合,管理使用超方便!(支持飞牛nas、群晖nas等服务器快速部署)

面对多种 AI 大模型接口难统一、管理繁琐的问题🤯,本文分享一款超实用开源网关工具⚡,可聚合各大模型、自动转接口、支持可视化管理,Docker 一键部署,大幅降低开发与维护成本🚀

XiaoqiangClub的博客 266

OpenClaw进阶实战(五):多模态能力扩展——让OpenClaw看懂图片、听懂语音

本文介绍了如何在OpenClaw中实现多模态能力,包括图像文字识别(OCR)、图像描述和语音识别(ASR)三种技能。通过PaddleOCR、BLIP和Whisper等开源工具,可以本地化部署这些功能,保护数据隐私。文章详细展示了三个独立技能模块的实现代码,并提出了一个"多模态智能管家"方案,能自动识别消息类型(文本/图片/语音)并调用相应处理技能。该方案适用于电商客服、会议纪要等真实业务场景,使AI助手具备"眼睛"和"耳朵"的多模态交互能力。

PeterPan的博客 161

Linux Shell类型本质:可执行程序、登录会话与解析引擎的三重解耦

Shell不是简单的命令行界面,而是操作系统中承担程序加载、会话管理与语法解析三重职责的核心组件。其底层是可执行二进制文件(如/bin/bash),启动时依据登录/非登录模式加载不同配置(.profile/.zshrc),再通过内置解析引擎执行POSIX或扩展语法。理解这一分层结构,是解决zsh: command not found、bash: syntax error等高频问题的关键前提——错误往往源于环境链断裂而非命令缺失。本文聚焦Shell的程序本体、会话载体与解析引擎三重身份,结合dash/bas/

weixin_30268921的博客 375

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

Pro Android学习笔记(一七零):发布应用(3):打开商店、许可和ProGuard

文章转载只能用于非商业性质,且不能带有虚拟货币、积分、注册等附加条件。转载须注明出处http://blog.csdn.net/flowingflying/以及作者@恺风Wei。 打开应用商店 Android提供URI(market://),在代码中打开Android Market应用商店。 Intent intent = new Intent(Intent.ACTION_VIEW

愷风(Wei)的专栏 4505

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

ChatGPT 1.0.0安卓逆向分析,仅限中国分享

本文将对ChatGpt Android版本1.0.0 APK进行静态解包分析和抓包分析,从ChatGpt Android APK功能的设计、校验过程和代码内包含的信息来预测OpenAI的大致走向。

u011312432的专栏 8500

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

Android Expansion 总结

使用 Expansion以解决目前 APK 尺寸 50M 限制。应用中关键点: 1.  程序使用 download_library,zib_file 库支持,这两个都能在 SDK 的 extras/google 路径下找到,相应例子也能找到,对使用上Android.com上也有很好的说明。 2.  最多可有两个 Expansion文件,一个Main,一个Patch。文件名格式为 ...obb,

Tonyfield的专栏 3178

Android工程:Google - play pass:应用要求

Google - play pass:应用要求

ImCode 691
上一篇: Scrapy入门教程之写入数据库
下一篇: android 指纹识别 之 BiometricPrompt的简单使用教程
androidWorkor
博客等级 码龄14年 17粉丝 19原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值