Android – 读取assets文件夹下的资源

参考:https://blog.csdn.net/qq_24382363/article/details/86480943

Android 中的资源文件

Android 资源文件大致可以分为两种:res/raw 和 assets

  • res/raw
    res/raw 目录下存放可编译的资源文件

    这种资源文件系统会在 R.Java 里面自动生成该资源文件的 ID,所以访问这种资源文件比较简单,通过 R.XXX.ID 即可。

  • assets
    assets目录下存放原生资源文件,可以存放一些图片,html,js, css等文件。

    因为系统在编译的时候不会编译 assets 下的资源文件,所以不能通过 R.XXX.ID 的方式访问它们。那我么能不能通过该资源的绝对路径去访问它们呢?因为apk安装之后会放在/data/app/**.apk目录下,以apk形式存在,asset/res/raw被绑定在apk里,并不会解压到/data/data/YourApp目录下去,所以无法直接获取到 assets 的绝对路径,因为它们根本就没有。

assets 文件夹资源的访问

Android 系统提供了 AssetManager 类来访问 assets 文件里的资源。
assets 文件里的文件都是保持原始的文件格式,需要使用 AssetManager 以字节流的形式读取文件。但是无法以正常的文件系统路径来访问

  • 先在 Activity 里面调用 getAssets() 来获取 AssetManager 引用。
  • 再用 AssetManager 的 open(String fileName, int accessMode) 方法,指定读取的文件以及访问模式,就能得到输入流 InputStream。
  • 然后用已经 open file 的 inputStream 读取文件,读取完成后记得 inputStream.close() 。
  • 调用 AssetManager.close() 关闭 AssetManager。

res/raw 和 assets 对比

res/raw和assets的相同点:

  • 两者目录下的文件在打包后会原封不动的保存在apk中,不会被变成二进制。

res/raw和assets的不同点:

  • res/raw 中的文件会被映射到 R.Java 文件中,访问的时候直接使用资源 ID 即 R.XXX.ID;
    assets 文件夹下的文件不会被映射到 R.Java 中,访问的时候需要 AssetManager 类。

  • res/raw 不可以有目录结构;
    而 assets 则可以有目录结构,也就是 assets 目录下可以再建立文件夹。

  • 读取文件资源方式不同:

    • 读取 res/raw 下的文件资源:
      InputStream is =getResources().openRawResource(R.id.filename);

    • 读取assets下的文件资源:
      AssetManager am = getAssets();
      InputStream is = am.open(“filename”);

    • 注意1:来自 Resources 和 Assets 中的文件只可以读取而不能进行写的操作

    • 注意2:Google 的 Android 系统处理 Assert 有个 bug,在 AssertManager 中不能处理单个超过1MB的文件,不然会报异常,raw 没这个限制,可以放个4MB的Mp3文件没问题。

    • 注意3:assets 文件夹是存放不进行编译加工的原生文件,即该文件夹里面的文件不会像 xml, java 文件被预编译,可以存放一些图片,html,js, css 等文件。

res/raw 和 assets 使用场景

  • 由于 res/raw 是Resources(res)的子目录,Android会自动的为这目录中的所有资源文件生成一个ID,这个ID会被存储在R类当中,作为一个文件的引用。这意味着这个资源文件可以很容易的被Android的类和方法访问到,甚至在Android XML文件中你也可以@raw/的形式引用到它。在Android中,使用ID是访问一个文件最快捷的方式。MP3和Ogg文件放在这个目录下是比较合适的。
  • assets 目录更像一个附录类型的目录,Android不会为这个目录中的文件生成ID并保存在R类当中,因此它与Android中的一些类和方法兼容度更低。同时,由于你需要一个字符串路径来获取这个目录下的文件描述符,访问的速度会更慢。但是把一些文件放在这个目录下会使一些操作更加方便,比方说拷贝一个数据库文件到系统内存中。要注意的是,你无法在Android XML文件中引用到assets目录下的文件,只能通过AssetManager来访问这些文件。数据库文件和游戏数据等放在这个目录下是比较合适的。

    如果你的应用需要预加载一些静态的、不会改变的数据,并且数据量不大,那么将一个预先填充的数据库文件放入assets文件夹,然后在应用首次运行时将其复制到应用的数据库文件夹,是一种可行的方案。
    这种方法的优点是可以省去在应用运行时填充数据库的时间,特别是当插入的数据量较大时。这可以使你的应用启动更快,提供更好的用户体验。
    然而,这种方法也有几个缺点:
    增加了应用的大小:预填充的数据库会增加应用的安装包大小,这可能对用户是一个负担,特别是在存储空间有限的设备上。
    数据更新问题:如果你的应用需要更新预加载的数据,那么必须更新整个应用,以包含新的数据库文件。为避免这个问题,一些应用会选择在运行时从服务器下载数据更新。
    适应性问题:这种方法并不适合需要读写动态数据的情况。如果你的应用需要用户能够修改数据,或者数据会随着时间改变,那么你应该使用Android提供的SQLite数据库或者其他数据库方案。

go语言的mobile绑定, 如何访问安卓apk中的assets

前言

用于在gin中, 代理整个单页应用spa, 因此需要访问到我们android中的assets。 但遗憾的是, 由于android特殊的限制, 我们无法使用正常的文件系统。

Java可以访问assets目录的静态资源是因为Android系统允许Java通过特定的API直接读取APK包内的资源文件。

在Android应用开发中,所有放在assets目录下的文件都会在应用编译时被打包进APK文件中,而这部分文件在应用运行时是可以被访问的。为了让开发者能够方便地访问到这部分文件,Android系统在Java层面提供了AssetManager类。开发者可以通过AssetManager类提供的API,如open()方法,来获取assets目录下文件的输入流,从而读取文件内容。

但是,这种方式只在Java层面可用,对于运行在Native层面的代码(如C++,Go等)则无法直接访问APK包内的资源。如果Native代码需要访问assets目录的内容,通常需要通过JNI来调用Java层面的代码进行桥接。

解决方案探讨

简单来说, 就是我们无法像正常的访问文件系统那样, 访问android的sdk中的assets目录。那么我们就需要解决这个问题。

大体上可行的解决方案:

  • 使用外部存储作为中间层, 将读取到的apk中的文件内容, 写入外部存储, 然后提供这个外部存储的字符串路径, 供gin使用。

    缺点1: 可以设置每次开启后, 先删除之前的复制品, 然后再重新写入。不过这个很影响启动时间。
    缺点2: 需要像用户请求存储权限, 否则无法正常使用, 对用户是一种负担。

  • 自己写, 使用JNI(Java Native Interface):Go语言可以通过CGO调用C接口,因此可以考虑实现一个利用JNI访问Asset文件的C库,然后编译为.so文件供Go调用。这种方式比较复杂,可能需要对JNI和CGO都有一定的了解。

  • 充当资源服务提供者的Content Provider:通过这种方式,Go SDK可以像访问文件系统一样访问Content Provider提供的资源。这种方式需要在Android端实现一个Content Provider,并在其openAssetFile()方法中处理资源的访问请求。

  • Input Stream:给Go SDK一个可以读取数据的InputStream。在Java代码中打开asset文件得到InputStream,然后您需要找到一种方式让Go SDK读取这个InputStream。这可能会涉及到JNI或JNA。

  • 利用go的mobile库的原生开发库(结合其绑定库使用), 其中提供了原生android中访问asset的api。按文档中的描述, 我们只要使用asset.Open函数, 就可以正常访问了。 下面只是举个例子, 不保证此例子的可用性, 当作伪代码看就好:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    package main
    import (
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
    "golang.org/x/mobile/asset"
    "io/ioutil"
    )
    func main() {
    router := gin.Default()
    router.GET("/assets/*filepath", func(c *gin.Context) {
    filepath := c.Param("filepath")
    if strings.HasSuffix(filepath, "/") {
    filepath = filepath + "index.html"
    }
    file, err := asset.Open(filepath)
    if err != nil {
    c.String(http.StatusNotFound, "File not found.")
    return
    }
    defer file.Close()
    content, err := ioutil.ReadAll(file)
    if err != nil {
    c.String(http.StatusInternalServerError, "Error reading file.")
    return
    }
    c.Data(http.StatusOK, getFileContentType(filepath), content)
    })
    router.Run()
    }
    func getFileContentType(filepath string) string {
    if strings.HasSuffix(filepath, ".html") {
    return "text/html"
    } else if strings.HasSuffix(filepath, ".css") {
    return "text/css"
    } else if strings.HasSuffix(filepath, ".js") {
    return "application/javascript"
    } else {
    return "text/plain"
    }
    }
  • 利用go的只读的虚拟文件系统embed, 直接将 assets在 打sdk的时候, 就打入虚拟静态文件系统中。此方案的唯一副作用就是, spa需要两份, 但是带来的好处是降低了多端适配成本。

    在Go 1.16及更高版本中,当你使用embed包将文件或目录嵌入到你的程序时,你需要在嵌入指令//go:embed后面指定文件或目录的相对路径(相对于包含嵌入指令的Go文件)。
    所以,assets文件在你的项目中的位置取决于你的项目的文件布局和你的embed指令。例如,如果assets目录与包含嵌入指令的Go文件在同一目录下,你可以像这样指定它:
    //go:embed assets/*

    如果assets目录处于包含嵌入指令的Go文件的子目录中,你需要包含子目录的名称,如下所示:
    //go:embed mySubdir/assets/*

    如果assets目录在包含嵌入指令的Go文件的上级目录中,你需要使用../前缀来指定这个路径(不过貌似不合规),如下所示:
    //go:embed ../assets/*

    请注意,embed指令还支持通配符和多个路径。所以如果你的assets文件被分散在项目的多个目录中,你可以在一个embed指令中包含他们所有的路径。例如:
    //go:embed assets/* moreAssets/* yetMoreAssets/*

最终方案

最终使用了 embed 的方案。

此时使用gin中的静态文件服务时, 需要选择StaticFS这个。

gin中的静态文件服务科普:

  • router.Static 指定某个目录为静态资源目录, 可直接访问这个目录下的资源, url要具体到资源名称。 – 资源可以是目录或文件。
  • router.StaticFS 比前面一个多了个功能, 当目录下不存在index.html文件时, 会列出该目录下的所有文件。– 基本上可以与上面的Static通用。
  • router.StaticFile 指定某个具体的文件作为静态资源访问。– 我个人一般不怎么使用。

最终示例:
假设这是我们的实际文件目录
├── spa
| ├── assets
| ├── icons
| ├── favicon.ico
| └── index.html
└── main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"github.com/gin-gonic/gin"
)

func main() {
// Set Gin to production mode when deploying to live environment
// gin.SetMode(gin.ReleaseMode)

// Create a router with default middleware: logger and recovery (crash-free) middleware
router := gin.Default()

// Serve frontend static files
router.Static("/", "./spa")


/* TIPS:
* 如果你的Vue 3项目是用vue-router 4的hash模式,那么通常是不需要在后端服务器(如Gin)上配置路由来处理前端页面的。Hash模式通过改变URL的hash(#后面的部分)来实现页面的跳转,而hash变化不会导致浏览器向服务器发起请求,浏览器只会加载一次页面(通常是index.html),之后所有的路由变化都是由vue-router在前端处理的。
* 因此,在hash模式下,无论用户刷新页面还是直接输入URL访问特定的hash路由,服务器端都只需要提供同一个index.html即可,无需额外的路由配置。只有在使用history模式的时候,由于URL不含hash,直接访问或刷新非根URL时会向服务器发起请求,这时候才需要后端设置NoRoute这样的路由处理来确保前端应用能被正确加载和显示。
* 所以,如果你正在使用hash模式,并且服务器已经配置好了对index.html的正常访问,那么你不需要额外配置NoRoute处理器。
*/
// Handle single page application
// router.NoRoute(func(c *gin.Context) {
// c.File("./dist/spa/index.html")
// })
/* TIPS:
* 如果你的Vue 3项目使用的是History模式的vue-router,那么在你的Gin后端中配置上面的代码是必要的。这段代码会处理所有不符合其他路由规则的请求(即“NoRoute”),通过返回index.html文件,来确保前端路由可以控制页面的展示。这对于单页面应用(SPA)非常重要,可以让前端的Vue路由器处理所有的路由。
* 因此,如果你希望能够直接通过URL访问或刷新任何前端路由而不是只有根URL(/),那么你还是需要这段代码的。没有它,当用户尝试访问非根路由时,他们可能会看到404错误,因为Gin不知道如何处理这些请求。这段代码确保所有的请求都会回退到你的index.html,由vue-router处理路由,提供正确的组件和视图。
* 总的来说,这段NoRoute配置是连接后端服务器路由和前端Vue路由的桥梁,确保SPA在用户直接访问任何路由时表现得和在客户端之间跳转时一样。所以,如果你正在开发一个SPA并且想要这种无缝的路由体验,那么这段代码是需要的。
*/

// Run the server
router.Run(":26666")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"embed"
"io/fs"
"net/http"

"github.com/gin-gonic/gin"
)

//go:embed spa/*
var embeddedFiles embed.FS

func main() {
r := gin.Default()
fsys, err := fs.Sub(embeddedFiles, "spa")
if err != nil {
panic(err)
}
//设置静态文件的路由
r.StaticFS("/", http.FS(fsys))
/* TIPS:
* 如果你的Vue 3项目是用vue-router 4的hash模式,那么通常是不需要在后端服务器(如Gin)上配置路由来处理前端页面的。Hash模式通过改变URL的hash(#后面的部分)来实现页面的跳转,而hash变化不会导致浏览器向服务器发起请求,浏览器只会加载一次页面(通常是index.html),之后所有的路由变化都是由vue-router在前端处理的。
* 因此,在hash模式下,无论用户刷新页面还是直接输入URL访问特定的hash路由,服务器端都只需要提供同一个index.html即可,无需额外的路由配置。只有在使用history模式的时候,由于URL不含hash,直接访问或刷新非根URL时会向服务器发起请求,这时候才需要后端设置NoRoute这样的路由处理来确保前端应用能被正确加载和显示。
* 所以,如果你正在使用hash模式,并且服务器已经配置好了对index.html的正常访问,那么你不需要额外配置NoRoute处理器。
*/
// Handle single page application
// r.NoRoute(func(c *gin.Context) {
// c.FileFromFS("index.html", http.FS(fsys))
// })
/* TIPS:
* 如果你的Vue 3项目使用的是History模式的vue-router,那么在你的Gin后端中配置上面的代码是必要的。这段代码会处理所有不符合其他路由规则的请求(即“NoRoute”),通过返回index.html文件,来确保前端路由可以控制页面的展示。这对于单页面应用(SPA)非常重要,可以让前端的Vue路由器处理所有的路由。
* 因此,如果你希望能够直接通过URL访问或刷新任何前端路由而不是只有根URL(/),那么你还是需要这段代码的。没有它,当用户尝试访问非根路由时,他们可能会看到404错误,因为Gin不知道如何处理这些请求。这段代码确保所有的请求都会回退到你的index.html,由vue-router处理路由,提供正确的组件和视图。
* 总的来说,这段NoRoute配置是连接后端服务器路由和前端Vue路由的桥梁,确保SPA在用户直接访问任何路由时表现得和在客户端之间跳转时一样。所以,如果你正在开发一个SPA并且想要这种无缝的路由体验,那么这段代码是需要的。
*/

r.Run(":8080")
}