所谓 Storage 是在 Android 里除了正常文件系统之外,它还实现了两种特殊的“文件存储”方式,一种是 OBB 另一种是 Asset, OBB 是独立于 APK 之外的资源包,而 Asset 是 APK 内置的文件。OBB 使用较少,所以 OBB 就跳过不提,只说明一下 Asset 用法。
我们解开一些 APK 可能会发现它里面有一个 assets 目录(解不开?改扩展名为.zip)。典型的如:
│ AndroidManifest.xml
│ classes.dex
│ resources.arsc
│
├─assets
│ ...
├─lib
│ ...
├─META-INF
└─res
...
这个目录是在 APK 内部,显然它和 APK 是一起发布的。关于它的使用场景,用一个词典应用来说,基础的词库可以放在 assets 这个目录(内置词库),而如果是要付费的一些专业词库则可以通过网络下载到手机,用正常文件系统来访问。在我们用 Golang 开发应用的情况下,它也可以用来放应用的界面资源,如图标、字串、界面布局等等。可参考示例文件。
在应用中如何访问这个目录?
相关的对象有:
它是通过调用 func (a *Activity) AssetManager() *AssetManager
得到的。
并且它只有两个函数
// Open the named directory within the asset hierarchy. The directory can then
// be inspected with the AAssetDir functions. To open the top-level directory,
// pass in "" as the dirName.
//
// The object returned here should be freed by calling AAssetDir_close().
func (mgr *AssetManager) OpenDir(dirName string) *AssetDir
// Open an asset.
//
// The object returned here should be freed by calling AAsset_close().
func (mgr *AssetManager) Open(filename string, mode int) *Asset
OpenDir 是打开 assets 下的一个目录, 要注意的是根目录是用 “” 表示, 同时这也是 assets 里文件路径的表示方式,比如 asserts/a.txt 和 assets/a/b/c.txt 在调用 Open 时,filename 分别是 “a.txt” 和 “a/b/c.txt” 。
OpenDir 会返回 AssetDir 对象,下面会详细说明。
而 Open 是返回 Asset 对象,关于 Asset 就简单了,因为它是(实现了) io.ReadSeeker 和 io.Closer,也就是几乎可当它是一个 os.File 来用。注意要调用 Close 释放资源。
// Iterate over the files in an asset directory. A "" string is returned
// when all the file names have been returned.
//
// The string returned here is owned by the AssetDir implementation and is not
// guaranteed to remain valid if any other calls are made on this AAssetDir
// instance.
func (assetDir *AssetDir) GetNextFileName() string
// Reset the iteration state of AAssetDir_getNextFileName() to the beginning.
func (assetDir *AssetDir) Rewind()
// Close an opened AAssetDir, freeing any related resources.
//
//void AAssetDir_close(AAssetDir* assetDir);
func (assetDir *AssetDir) Close()
GetNextFileName 返回下一个文件名,如果返回"", 说明所有文件列举完了。请注意这里只会返回此目录下的文件,不会返回目录,不知是出于什么考虑?但对于内置文件来说,也算是合理的,理论上 APK 一发布,内置文件就是固定了的。所以从另一面来说,它是给了我们一点灵活性。
Rewind 是重置 GetNextFileName 相关状态和指针,使 GetNextFileName 又从第一个文件开始列举。
列举目录下所有文件的代码如下:
func AssetInfo(act *app.Activity, dir string) {
assetMgr := act.AssetManager()
assetDir := assetMgr.OpenDir(dir)
defer assetDir.Close()
assetDir.Rewind()
for {
// 得到下一个文件名,但不能列出子目录
fname := assetDir.GetNextFileName()
if fname == "" {
return
}
...
}
}
请参考示例,这个例子中 assets 目录树如下:
│ AndroidManifest.xml
│
├─assets
│ │ label_icon.png
│ │ readme_root.txt
│ │
│ └─a
│ readme_a.txt
因为无法列出子目录,因此对于本例的目录树,对于 assets 需调用 AssetInfo(acr, "")
,对于 assets/a 需调用 AssetInfo(act, "a")
。
示例中有个 ReadImage 函数,既说明了 Asset 文件的用法,同时这个例子也演示了 imgui 中图像的显示。
主要是三步:
func ReadImage(act *app.Activity, fname string) image.Image {
// load image
m := act.AssetManager()
fa := m.Open(fname, storage.ASSET_MODE_BUFFER)
buf := make([]byte, fa.Length())
fa.Read(buf)
img, _, err := image.Decode(bytes.NewBuffer(buf))
if err != nil {
log.Println("ReadImage:", err)
}
fa.Close()
// 转换成 RGBA 格式
dst := image.NewRGBA(img.Bounds())
imagedraw.Draw(dst, dst.Bounds(), img, image.Point{}, imagedraw.Src)
return dst
}
需要注意的是上面例子中有个 “转换成 RGBA 格式” 的操作,这个效率非常低,不建议这么做,要尽量避免数据转换。如果是 openGL 支持的格式如 RGB565 等,根据不同的图像数据格式找到对应参数直接生成 texture ,如果是不支持的数据格式则可用 GLSL 来绘制。
上面已经说过 assets 目录下可以用来放应用的界面资源,如图标、字串、界面布局等等。然后是应用需要的数据,这里需要再次提醒的是因为不能列举目录,所以在目录设计的时候需要考虑这个因素,如果做成数据驱动的方式则需要把目录定下来,而以文件的变化作为区别。比如说惯用的版本区分 lite 和 pro 版,它们最好保持一致的目录结构,以文件的不同来区分版本 。