文件架构

Android目录下有很多固定名字的文件夹, 类似Unity中的Resources, Editor这些文件夹, 用来标记相关特殊的功能。具体如下列表:

  • assets资产目录,这里面的文件会被打包到应用成的apk(安装包)里面,不会被本地化(不会被转成二进制文件)
  • bin 编译后的文件目录
  • gen 自动生成的文件目录 用于统一管理资源,通过id使用资源
  • project.properties 代表编译的版本,对应android.jar的版本eg: target= android-19
  • libs支持jar包,会被添加到android depend
  • res资源目录(会把某些文件存为二进制文件)
    • drawable存放应用的图片资源,android系统会根据你的手机分辨率加载不同的图片
    • layout布局文件(设置界面显示样式)
    • menu设置菜单文件
    • values包含一些字符串等等
  • AndroidManifest.xml本应用的配置信息
    • package:唯一标示一个应用的包名
    • versionCode:版本代码
    • versionName:版本名
    • minSdkVersion:支持的最小版本
    • targetSdkVersion:目标版本(一般高版本手机支持低版本应用)
    • application:icon应用图标,label应用名,activity声明活动

StreamingAssets

unity中的StreamingAssets目录通常用来放一些Bundle,或者诸如Fmod的Bank文件, 但此文件夹在Windows平台和IOS平台下可以通过IO接口直接访问, 这就意味着我们可以使用多线程或者线程池来高效率的读取资源。 但遗憾的是这接口不支持Android。 那具体Android如何存储StreamingAssets下的文件呢?

一般的话, 我们可以通过Assetbundle或者WWW的接口来获取StreamingAssets下的资源, 但需要转换下路径, 不能使用物理路径, 而是相应的需要是映射的虚拟路径。官方推荐使用Application.streamingAssetsPath来获得该文件夹的实际位置,其可以规避平台差异。

  • 对于ios平台,其等价于:Application.dataPath + “/Raw”;
  • 对于android平台,其等价于:”jar:file://” + Application.dataPath + “!/assets/”;
private void SetupPath()
{
    switch (Application.platform)
    {
        case RuntimePlatform.Android:
            BundleRootDefault = string.Format("{0}!assets/{1}/", Application.dataPath, BundleFolder);
            break;
        case RuntimePlatform.IPhonePlayer:
            BundleRootDefault = string.Format("{0}/Raw/{1}/", Application.dataPath, BundleFolder);
            break;
        default:
            BundleRootDefault = string.Format("{0}/StreamingAssets/{1}/", Application.dataPath, BundleFolder);
            break;
    }
}

但底层Unity生成的Android包安装之后, 究竟是如何存储的, 这里通过实验的方法一探究竟:

第一步, 我们先在StreamingAssets目录下, 放几个文件, 如下图所示:

第二步,生成Gradle工程(工程放在跟Asset目录同级的Temp文件夹下), 这里我们发现StreamingAssets目录下的文件其实对应assets文件夹, 并且我们发现gradle生成的apk的过程中,build文件夹下的目录:

第三步, 安装生成的apk, 这里需要一台root的手机或者模拟器, 这里选择网易的mumu模拟器, 直接在设置里勾一下重启就Root掉了, 真是方便。

StreamingAssets安装之后,默认解压到/data/app/包名 文件夹下, 我们通过abd shell命令进入手机(需要电脑配置好Android开发环境。

可以看到里面有个base.apk, 通过命令查看其占用磁盘大小, 基本可以定位这个就是我们放置StreamingAssets文件的

du -h base.apk

我们知道.apk本质就是zip压缩, 这里我们改其后缀, 然后使用unzip命令解压, 就可以看到一开始Unity放入StreamingAssets里的文件啦。

访问assets文件夹下的资源, 可以通过java或者JNI的接口:

public String readFromAssets(String name){
String resultStr = "";
try {
    InputStream inStream = this.getResources().getAssets().open(name);
    resultStr = convertStream2String(inStream);

    //convertStreamToString(inStream);
} catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}
return resultStr;

JNI的c++接口:

#include "mylog.h"
#include <jni.h>
#include <sys/types.h>
#include <stdlib.h>
#include <android/asset_manager_jni.h>
#include <android/asset_manager.h>

extern "C"{
JNIEXPORT  jstring JNICALL Java_org_yanzi_lib_MyLib_readFromAssets(JNIEnv* env,jclass clazz, jobject assetManager,jstring name);
JNIEXPORT  jstring JNICALL Java_org_yanzi_lib_MyLib_readFromAssets(JNIEnv* env, jclass clazz, jobject assetManager,jstring name){
	LOGI("readFromAssets enter..."); //jclass this,
	jstring resultStr;
	AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
	if(mgr==NULL)
	{
		LOGI(" %s","AAssetManager==NULL");
		return 0;
	}
 
	/*获取文件名并打开*/
	jboolean iscopy;
	const char *mfile = env->GetStringUTFChars(name, &iscopy); //(*env)->GetStringUTFChars(name, &iscopy); env,
	AAsset* asset = AAssetManager_open(mgr, mfile,AASSET_MODE_UNKNOWN);
	env->ReleaseStringUTFChars(name, mfile); //env,
	if(asset==NULL)
	{
		LOGI(" %s","asset==NULL");
		return 0;
	}
	/*获取文件大小*/
	off_t bufferSize = AAsset_getLength(asset);
	LOGI("file size : %d\n",bufferSize);
	char *buffer=(char *)malloc(bufferSize+1);
	buffer[bufferSize]=0;
	int numBytesRead = AAsset_read(asset, buffer, bufferSize);
	LOGI("readFromAssets: %s",buffer);
	resultStr = env->NewStringUTF(buffer);
	free(buffer);
	/*关闭文件*/
	AAsset_close(asset);
	LOGI("readFromAssets exit...");
	return resultStr;
  }
}

需要注意的是, 使用JNI的接口,Application.mk文件需要做如下设置:

APP_STL:=gnustl_static
APP_CPPFLAGS:=-frtti -fexceptions
APP_ABI:= armeabi-v7a

PlayerPrefs存储路径

我们知道PlayerPrefs在windows下, 储存在注册表中, Android平台Unity又是怎么处理的?

通过上图,可以发现PlayerPrefs的值是以xml的形式存储在 /data/data/youPackageName/shared_prefs的目录下。由于不是直接储存在压缩包内, 这个xml应该可以通过io接口访问(需要获取相应的权限, 并在Androidmenifest.xml中声明), 但是由于非root手机一般都进入不到此文件夹, 我们平时也无法直接在资源管理器里访问或者编辑此xml。

/data/data/youPackageName/即内部存储路径下。所有内部存储中保存的文件在用户卸载应用的时候会被删除。Java访问的接口:

  • files
    • Context.getFilesDir(),该方法返回/data/data/youPackageName/files的File对象。
    • Context.openFileInput()与Context.openFileOutput(),只能读取和写入files下的文件,返回的是FileInputStream和FileOutputStream对象。
    • Context.fileList(),返回files下所有的文件名,返回的是String[]对象。
    • Context.deleteFile(String),删除files下指定名称的文件。
  • cache
    • Context.getCacheDir(),该方法返回/data/data/youPackageName/cache的File对象。
  • customDir
    • getDir(String name, int mode),返回/data/data/youPackageName/下的指定名称的文件夹File对象,如果该文件夹不存在则用指定名称创建一个新的文件夹。

persistentDataPath, temporaryCachePath

这两个目录路径在storage/Android/data/youPackageName目录下, 一般情况下可以通过手机自带的文件浏览器可以直接 编辑/删除 这些文件, 权限给的比较高,卸载程序此两个文件夹也会一起删除。temporaryCachePath是一个临时目录, 在操作系统磁盘不足的情况下, 会自动被系统清除, 适合存一些临时的文件, 而persistentDataPath适合存一些持久化的文件, 比如说游戏的bundle。

persistentDataPath: storage/Android/data/yourpackageName/files
temporaryCachePath: storage/Android/data/yourpackageName/cache

使用原生的方式获取路径:

MLog.d(TAG, "output: " + gameContext.getExternalFilesDir("").getAbsolutePath();
MLog.d(TAG, "package: " + gameContext.getPackageResourcePath());
MLog.d(TAG, "cache: " + gameContext.getCacheDir().getPath());
MLog.d(TAG, "exCache: " + gameContext.getExternalCacheDir().getPath());
MLog.d(TAG, "file: " + gameContext.getFilesDir().getPath());
MLog.d(TAG, "code: " + gameContext.getPackageCodePath());

对应的输出是:

内部存储,外部存储的概念

内部存储

概念:注意内部存储不是内存。内部存储位于系统中很特殊的一个位置,如果你想将文件存储于内部存储中,那么文件默认只能被你的应用访问到,且一个应用所创建的所有文件都在和应用包名相同的目录下。也就是说应用创建于内部存储的文件,与这个应用是关联起来的。当一个应用卸载之后,内部存储中的这些文件也被删除。从技术上来讲如果你在创建内部存储文件的时候将文件属性设置成可读,其他app能够访问自己应用的数据,前提是他知道你这个应用的包名,如果一个文件的属性是私有(private),那么即使知道包名其他应用也无法访问。 内部存储空间十分有限,因而显得可贵,另外,它也是系统本身和系统应用程序主要的数据存储所在地,一旦内部存储空间耗尽,手机也就无法使用了。所以对于内部存储空间,我们要尽量避免使用。Shared Preferences和SQLite数据库都是存储在内部存储空间上的。内部存储一般用Context来获取和操作。
访问内部存储的API方法:
1、Environment.getDataDirectory()
2、getFilesDir().getAbsolutePath()
3、getCacheDir().getAbsolutePath()
4、getDir(“myFile”, MODE_PRIVATE).getAbsolutePath()

外部存储

概念:最容易混淆的是外部存储,因为老的Android系统的跟新的Android系统是有差别的,很多人去网上查找资料,看了一下以前的资料,又看了一下现在的资料,但是发现它们说法不一样然后就困惑了。首先说一个大家普遍的概念“如果在pc机上是区分外部存储和内部存储的话,那么电脑自带的硬盘算是内部存储,U盘或者移动硬盘就是外部存储了。”因此很多人带着这样的理解去看待安卓手机,把内置存储(机身存储)当做内部存储,而把扩展的SD卡当做是外部存储。这么认为确实没错,因为在4.4(API19)以前的手机上确实是这样的,手机自身带的存储卡就是内部存储,而扩展的SD卡就是外部存储。但是从4.4的系统开始,很多的中高端机器都将自己的机身存储扩展到了8G以上,比如有的人的手机是16G的,有的人的手机是32G的,但是这个16G,32G是内部存储吗,不是的!!!,它们依然是外部存储,也就是说4.4系统及以上的手机将机身存储存储(手机自身带的存储叫做机身存储)在概念上分成了”内部存储internal” 和”外部存储external” 两部分。既然16G,32G是外部存储,那有人又有疑惑了,那4.4系统及以上的手机要是插了SD卡呢,SD卡又是什么呢,如果SD卡也是外部存储的话,那怎么区分机身存储的外部存储跟SD卡的外部存储呢?对,SD卡也是外部存储,那怎么区分呢,在4.4以后的系统中,API提供了这样一个方法来遍历手机的外部存储路径:

File[] files;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    files = getExternalFilesDirs(Environment.MEDIA_MOUNTED);
    for(File file:files){
        Log.e("main",file);
    }
}

如果你的手机插了SD卡的话,那么它打印的路径就有两条了,例如我的华为荣耀7插了SD卡,它的结果如下:
/storage/emulated/0/Android/data/packname/files/mounted
/storage/B3E4-1711/Android/data/packname/files/mounted
其中/storage/emulated/0目录就是机身存储的外部存储路径, 而/storage/B3E4-1711/就是SD卡的路径

参考