Android大量文件扫描及存储方式

最近在做多媒体应用,由于是TV应用,所以我们插入U盘之后就需要扫描U盘文件,并存到数据库里面,这就需要遍历U盘所有文件。

一、遇到的问题

第一种方式,使用系统自带的MediaScanner,使用系统的一些接口来获取数据的信息,但是感觉系统对于扫描过程中的监听不够友好,而且MediaScanner对于大量文件扫描极慢,由于TV上面会有对于移动硬盘的需求,所以我们要考虑到扫描速度,不然的话,用户体验会很差。

既然第一种方式没法使用,就只好使用文件遍历的方式了,监听U盘拔插广播,并启动Scan的服务,在后台进行扫描服务。这种方式存在的问题是:对于文件规模较小的磁盘,不会出现什么问题,但是如果是移动硬盘,尤其是文件很多的话,遍历中会产生大量临时对象,Java堆内存占用呈直线上升,最终到达192M(也就是系统规定的单个应用最大内存),引发GC,程序崩溃,而且在操作的过程中卡顿严重。

通过Android Studio的Profiler分析发现,产生的很多对象都是文件的path名,对于这种情况,java并不能进行很好地内存回收,导致占用过多引发GC。

二、解决方案

既然不能使用Java进行遍历,所以我们就使用Jni来进行文件遍历,由于C++和C可以对内存进行显式回收,所以对于这种大量对象可以创建完之后直接回收,我们也可以通过Profiler看到内存使用比之前低了很多,基本上是native使用20M左右。但是依然存在一个问题,Jni不能直接调用Sqlite,所以我们只能在需要插入文件的时候再回调到Java层,进行文件插入,这里相对于之前的大量临时对象,我们需要插入的文件类型只是很小的一部分。

首先写一个native方法以及加载jni库:

	static {
        try {
            System.loadLibrary("FileScanner");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static native void scanFiles(String path);

写了native方法,我们可以通过javah生成对应的Jni头文件,并可以开始C++逻辑的编写。
具体代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define LOGE(...) \
  ((void)__android_log_print(ANDROID_LOG_ERROR, "FileScanner::", __VA_ARGS__))
#define LOGD(...) \
  ((void)__android_log_print(ANDROID_LOG_DEBUG, "FileScanner::", __VA_ARGS__))

jclass baseData_cls;
jmethodID baseData_constructor;
jmethodID baseData_setFilePath;
jmethodID baseData_setFileName;
jmethodID baseData_setFileType;

jobject context;
const int PATH_MAX_LENGTH = 256;

jobject serviceObject;
jmethodID insertData;
jmethodID setFinishAction;
jint videoType;
jint musicType;
jint pictureType;
jobjectArray videoTypes;
jobjectArray musicTypes;
jobjectArray pictureTypes;

extern "C"
void JNICALL Java_com_ktc_media_scan_FileScannerJni_scanFiles
        (JNIEnv *env, jclass thiz, jstring str) {
    init(env);
    char *path = (char *) env->GetStringUTFChars(str, nullptr);
    doScanFile(env, str);
    env->CallVoidMethod(serviceObject, setFinishAction);
    env->ReleaseStringUTFChars(str, path);
}

void init(JNIEnv *env) {
    context = getGlobalContext(env);
    baseData_cls = env->FindClass(
            "com/ktc/media/model/BaseData");
    baseData_constructor = env->GetMethodID(baseData_cls, "", "()V");
    baseData_setFilePath = env->GetMethodID(baseData_cls, "setPath",
                                            "(Ljava/lang/String;)V");
    baseData_setFileName = env->GetMethodID(baseData_cls, "setName",
                                            "(Ljava/lang/String;)V");
    baseData_setFileType = env->GetMethodID(baseData_cls, "setType",
                                            "(I)V");

    jclass service = env->FindClass("com/ktc/media/scan/FileScanManager");
    jmethodID serviceConstructor = env->GetMethodID(service, "",
                                                    "(Landroid/content/Context;)V");
    serviceObject = env->NewObject(service,
                                   serviceConstructor, context);
    insertData = env->GetMethodID(service, "insertData",
                                  "(ILcom/ktc/media/model/BaseData;)V");
    setFinishAction = env->GetMethodID(service, "sendFinishAction",
                                       "()V");

    jfieldID video = env->GetFieldID(service, "videoType", "I");
    jfieldID music = env->GetFieldID(service, "musicType", "I");
    jfieldID picture = env->GetFieldID(service, "pictureType", "I");
    jfieldID videoArray = env->GetFieldID(service, "videoTypes", "[Ljava/lang/String;");
    jfieldID musicArray = env->GetFieldID(service, "musicTypes", "[Ljava/lang/String;");
    jfieldID pictureArray = env->GetFieldID(service, "pictureTypes", "[Ljava/lang/String;");

    videoType = env->GetIntField(serviceObject, video);
    musicType = env->GetIntField(serviceObject, music);
    pictureType = env->GetIntField(serviceObject, picture);
    videoTypes = (jobjectArray) env->GetObjectField(serviceObject, videoArray);
    musicTypes = (jobjectArray) env->GetObjectField(serviceObject, musicArray);
    pictureTypes = (jobjectArray) env->GetObjectField(serviceObject, pictureArray);
}

void doScanFile(JNIEnv *env, jstring dirPath_) {

    if (dirPath_ == nullptr) {
        LOGE("dirPath is null!");
        return;
    }
    const char *dirPath = env->GetStringUTFChars(dirPath_, nullptr);
    if (strlen(dirPath) == 0) {
        LOGE("dirPath length is 0!");
        return;
    }
    //打开文件夹读取流
    DIR *dir = opendir(dirPath);
    if (nullptr == dir) {
        LOGE("can not open %s,  check path or permission!", dirPath);
        return;
    }

    struct dirent *file;
    while ((file = readdir(dir)) != nullptr) {
        //判断是不是 . 或者 .. 文件夹
        if (strcmp(file->d_name, ".") == 0 || strcmp(file->d_name, "..") == 0) {
            //LOGD("ignore . and ..");
            continue;
        }
        if (file->d_type == DT_DIR) {
            char *path = new char[PATH_MAX_LENGTH];
            memset(path, 0, PATH_MAX_LENGTH);
            strcpy(path, dirPath);
            strcat(path, "/");
            strcat(path, file->d_name);
            jstring tDir = env->NewStringUTF(path);
            if (path[0] != '.') {
                doScanFile(env, tDir);
            }
            //释放文件夹路径内存
            env->DeleteLocalRef(tDir);
            free(path);
        } else {
            insertVideoData(env, dirPath, file, videoTypes, videoType);
            insertVideoData(env, dirPath, file, musicTypes, musicType);
            insertVideoData(env, dirPath, file, pictureTypes, pictureType);
            //LOGD("%s/%s", dirPath, file->d_name);
        }
    }
    //关闭读取流
    closedir(dir);
    env->ReleaseStringUTFChars(dirPath_, dirPath);
}

void
insertVideoData(JNIEnv *env, const char *dirPath, dirent *file, jobjectArray types, jint fileType) {
    int size = env->GetArrayLength(types);
    for (int i = 0; i < size; i++) {
        jstring type = (jstring) env->GetObjectArrayElement(types, i);
        jstring nameStr = env->NewStringUTF(file->d_name);
        char *typeStr = (char *) env->GetStringUTFChars(type, nullptr);
        char *name = file->d_name;
        char *extension = strrchr(name, '.');
        if (extension == nullptr
            || name == nullptr) {
            continue;
        }
        char *path = new char[PATH_MAX_LENGTH];
        memset(path, 0, PATH_MAX_LENGTH);
        strcpy(path, dirPath);
        strcat(path, "/");
        strcat(path, file->d_name);
        jstring tDir = env->NewStringUTF(path);
        bool isTargetFile = false;
        if (strcmp(typeStr, extension) == 0) {
            if (nullptr != insertData) {
                jobject baseData_obj = env->NewObject(baseData_cls,
                                                      baseData_constructor);
                if (baseData_obj != nullptr
                    && tDir != nullptr
                    && nameStr != nullptr) {
                    env->CallVoidMethod(baseData_obj, baseData_setFilePath, tDir);
                    env->CallVoidMethod(baseData_obj, baseData_setFileName, nameStr);
                    env->CallVoidMethod(baseData_obj, baseData_setFileType, fileType);
                    isTargetFile = true;
                    env->CallVoidMethod(serviceObject, insertData, fileType, baseData_obj);
                    env->DeleteLocalRef(baseData_obj);
                }
            }
        }
        if (isTargetFile) {
            continue;
        }
        env->DeleteLocalRef(type);
        env->DeleteLocalRef(tDir);
        env->DeleteLocalRef(nameStr);
        free(path);
        env->ReleaseStringUTFChars(type, typeStr);
    }
}


jobject getGlobalContext(JNIEnv *env) {
    jclass activityThread = env->FindClass("android/app/ActivityThread");
    jmethodID currentActivityThread = env->GetStaticMethodID(activityThread,
                                                             "currentActivityThread",
                                                             "()Landroid/app/ActivityThread;");
    jobject at = env->CallStaticObjectMethod(activityThread, currentActivityThread);
    jmethodID getApplication = env->GetMethodID(activityThread, "getApplication",
                                                "()Landroid/app/Application;");
    jobject context = env->CallObjectMethod(at, getApplication);
    return context;
}

在主要方法里面调用init以及doScanFile具体扫描的方法,init是初始化Context以及一些所用到的类和方法,doScanFile是遍历文件并在insertVideoData方法中通过不同的文件类型创建baseData_cls对象并回调到Java层,进行插入操作。

三、优化

进行大量文件的扫描与插入,虽然已经放到Jni去做了,但是还是难免会占用一部分内存,况且我们要写的是多媒体应用,多媒体应用在媒体播放的时候会占用大量CPU,在同一个进程进行工作的话,多媒体播放会明显卡顿。所以需要把文件扫描放到单独的进程进行操作。由于主进程需要知道扫描进度,所以需要进行进程间通信,这里我们使用AIDL。

首先将接收广播的BroadcastReceiver与ScanService通过android:process标记为单独进程。然后编写Aidl文件,需要一个事件的Listener以及一个事件的管理器Manager:

interface IFileScanUpdateListener {

    void updateFile(String type);
}


import com.ktc.media.IFileScanUpdateListener;

interface IFileScanManager {
    void registerListener(IFileScanUpdateListener listener);

    void unregisterListener(IFileScanUpdateListener listener);
}

这里即便两个文件在一个目录下,依然需要import一下,是Aidl的特性。
然后需要一个单独的Service来管理这些事件的接收与分发,Aidl的接口必须使用RemoteCallbackList:

public class FileScanUpdateService extends Service implements FileScanObserver {

    private RemoteCallbackList<IFileScanUpdateListener> mListenerList = new RemoteCallbackList<>();

    public FileScanUpdateService() {
        FileObserverInstance.getInstance().addFileScanObserver(this);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        FileObserverInstance.getInstance().removeFileScanObserver(this);
    }

    private Binder mBinder = new IFileScanManager.Stub() {
        @Override
        public void registerListener(IFileScanUpdateListener listener) throws RemoteException {
            mListenerList.register(listener);
        }

        @Override
        public void unregisterListener(IFileScanUpdateListener listener) throws RemoteException {
            mListenerList.unregister(listener);
        }
    };

    @Override
    public void update(String type) {
        synchronized (FileScanUpdateService.class) {
            try {
                int size = mListenerList.beginBroadcast();
                for (int i = 0; i < size; i++) {
                    IFileScanUpdateListener fileScanUpdateListener = mListenerList.getBroadcastItem(i);
                    if (fileScanUpdateListener != null) {
                        try {
                            fileScanUpdateListener.updateFile(type);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } finally {
                mListenerList.finishBroadcast();
            }
        }
    }
}

这里的主要逻辑是,每个activity单独进行registerListener与unregisterListener,事件进来之后,也就是update的时候再遍历事件列表,进行事件传递。RemoteCallbackList的遍历只能使用beginBroadcast与finishBroadcast进行,而且如果进来的事件过多,可能回导致两个方法对应不起来,所以这里使用了同步。

四、总结

有些时候使用Java可能会过于冗杂,Java的垃圾回收机制有些时候限制了Java的一些速度,有些内存敏感的应用也只能使用c/c++这种方式进行。

你可能感兴趣的:(Android大量文件扫描及存储方式)