最近在做多媒体应用,由于是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 <stdio.h>
#include <android/log.h>
#include <sys/stat.h>
#include <dirent.h>
#include <string.h>
#include <malloc.h>
#include <pthread.h>
#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, "<init>", "()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, "<init>",
"(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++这种方式进行。