`

Android MediaScanner 详尽分析

 
阅读更多

MediaScanner分析

MediaScannerService

多媒体扫描是从MediaScannerService开始的。这是一个单独的package。位于

packages/providers/MediaProvider:含以下java文件

l MediaProvider.java

l MediaScannerReceiver.java

l MediaScannerService.java

l MediaThumbRequest.java

分析这个目录的Android.mk文件,发现它运行的进程名字就是android.process.media

application android:process=android.process.media

1.1 MediaScannerReceiver

这个类从BroadcastReceiver中派生,用来接收任务的。

MediaScannerReceiver extends BroadcastReceiver

在它重载的onRecieve函数内有以下几种走向:

if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {

// 收到启动完毕“广播后,扫描内部存储

scan(context, MediaProvider.INTERNAL_VOLUME);

} else {

……….

if (action.equals(Intent.ACTION_MEDIA_MOUNTED) &&

externalStoragePath.equals(path)) {

/收到MOUNT信息后,扫描外部存储

scan(context, MediaProvider.EXTERNAL_VOLUME);

}

else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) &&

path != null && path.startsWith(externalStoragePath + "/")) {

//收到请求要求扫描某个文件,注意不会扫描内部存储上的文件

scanFile(context, path);

…………………………..

}

……下面是它调用的scan函数:

scan(Context context, String volume)

Bundle args = new Bundle();

args.putString("volume", volume);

//直接启动MediaScannerService了,

context.startService(

new Intent(context, MediaScannerService.class).putExtras(args));

总结:

MediaScannerReceiver是用来接收任务的,它收到广播后,会启动MediaService进行扫描工作。

下面看看MediaScannerService.

1.2 MediaScannerService

MSS标准的从Service中派生下来,

MediaScannerService extends Service implements Runnable

//注意:是一个Runnable…,可能有线程之类的东西存在

下面从Service的生命周期的角度来看看它的工作。

1. onCreate

public void onCreate()

PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);

mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);

//获得电源锁,防止在扫描过程中休眠

//单独搞一个线程去跑扫描工作,防止ANR

Thread thr = new Thread(null, this, "MediaScannerService");

thr.start();

2. onStartCommand

@Override

public int onStartCommand(Intent intent, int flags, int startId)

{

//注意这个handler,是在另外一个线程中创建的,往这个handlersendMessage

//都会在那个线程里边处理

//不明白的可以去查看handlerLooper机制

//这里就是同步机制,等待mServiceHandler在另外那个线程创建完毕

while (mServiceHandler == null) {

synchronized (this) {

try {

wait(100);

} catch (InterruptedException e) {

}

}

}

if (intent == null) {

Log.e(TAG, "Intent is null in onStartCommand: ",

new NullPointerException());

return Service.START_NOT_STICKY;

}

Message msg = mServiceHandler.obtainMessage();

msg.arg1 = startId;

msg.obj = intent.getExtras();

//MediaScannerReceiver发出的消息传递到另外那个线程去处理。

mServiceHandler.sendMessage(msg);

………….

基本上MSR(MediaScannerReceiver)发出的请求都会传到onStartCommand中处理。如果有多个存储的话,也只能一个一个扫描了。

下面看看那个线程的主函数

3. run

public void run()

{

// reduce priority below other background threads to avoid interfering

// with other services at boot time.

Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +

Process.THREAD_PRIORITY_LESS_FAVORABLE);

//不明白的去看看Looperhandler的实现

Looper.prepare();//把这个looper对象设置到线程本地存储

mServiceLooper = Looper.myLooper();

mServiceHandler = new ServiceHandler();//创建handler,默认会把这个looper

//的消息队列赋值给handler的消息队列,这样往handler中发送消息就是往这个线程的looper

Looper.loop();//消息循环,内部会处理消息队列中的消息

//也就是handleMessage函数

}

上面handler中加入了一个扫描请求(假设是外部存储的),所以要分析handleMessage函数。

4. handleMessage

private final class ServiceHandler extends Handler

{

@Override

public void handleMessage(Message msg)

{

Bundle arguments = (Bundle) msg.obj;

String filePath = arguments.getString("filepath");

try {

……… 这里不讲了

} else {

String volume = arguments.getString("volume");

String[] directories = null;

if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {

//是扫描内部存储的请求?

// scan internal media storage

directories = new String[] {

Environment.getRootDirectory() + "/media",

};

}

else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {

//是扫描外部存储的请求?获取外部存储的路径

directories = new String[] {

Environment.getExternalStorageDirectory().getPath(),

};

}

if (directories != null) {

//真正的扫描开始了,上面只不过是把存储路径取出来罢了.

scan(directories, volume);

…..

//扫描完了,就把service停止了

stopSelf(msg.arg1);

}

};

5. scan函数

private void scan(String[] directories, String volumeName) {

mWakeLock.acquire();

//下面这三句话很深奥

// getContentResolver获得一个ContentResover,然后直接插入

//根据AIDL,这个ContentResover的另一端是MediaProvider。只要去看看它的

//insert函数就可以了

//反正这里知道获得了一个扫描URI即可。

ContentValues values = new ContentValues();

values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);

Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);

Uri uri = Uri.parse("file://" + directories[0]);

//发送广播,通知扫描开始了

sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));

try {

if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {

openDatabase(volumeName);

}

//创建真正的扫描器

MediaScanner scanner = createMediaScanner();

//交给扫描器去扫描文件夹 scanDirectories

scanner.scanDirectories(directories, volumeName);

} catch (Exception e) {

Log.e(TAG, "exception in MediaScanner.scan()", e);

}

//删除扫描路径

getContentResolver().delete(scanUri, null, null);

//通知扫描完毕

sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));

mWakeLock.release();

}

说说上面那个深奥的地方,在MediaProvider中重载了insert函数,insert函数会调用insertInternal函数。

如下:

private Uri insertInternal(Uri uri, ContentValues initialValues) {

long rowId;

int match = URI_MATCHER.match(uri);

// handle MEDIA_SCANNER before calling getDatabaseForUri()

//刚才那个insert只会走下面这个分支,其实就是获得一个地址….

//太绕了!!!!!

if (match == MEDIA_SCANNER) {

mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);

return MediaStore.getMediaScannerUri();

}

……..

再看看它创建了什么样的Scanner,这就是MSS中的createMediaScanner

private MediaScanner createMediaScanner() {

//下面这个MediaScannerframework/base/中,待会再分析

MediaScanner scanner = new MediaScanner(this);

//设置当前的区域,这个和字符编码有重大关系。

Locale locale = getResources().getConfiguration().locale;

if (locale != null) {

String language = locale.getLanguage();

String country = locale.getCountry();

String localeString = null;

if (language != null) {

if (country != null) {

//给扫描器设置当前国家和语言。

scanner.setLocale(language + "_" + country);

} else {

scanner.setLocale(language);

}

}

}

return scanner;

}

至此,MSS的任务完成了。接下来是MediaScanner的工作了。

6. 总结

MSS的工作流程如下:

l 1 单独启动一个带消息循环的工作线程。

l 2 主线程接收系统发来的任务,然后发送给工作线程去处理。

l 3 工作线程接收任务,创建一个MediaScanner去扫描。

l 4 MSS顺带广播一下扫描工作启动了,扫描工作完毕了。

MediaScanner

MediaScanner位置在

frameworks/base/media/下,包括jnijava文件。

先看看java实现。

这个类巨复杂,而且和MediaProvider交互频繁。在分析的时候要时刻回到MediaProvider去看看。

1. 初始化

public class MediaScanner

{

static {

//libmedia_jni.so的加载是在MediaScanner类中完成的

//这么重要的so为何放在如此不起眼的地方加载???

System.loadLibrary("media_jni");

native_init();

}

public MediaScanner(Context c) {

native_setup();//调用jni层的初始化,暂时不用看了,无非就是一些

//初始化工作,待会在再进去看看

……..

}

刚才MSS中是调用scanDirectories函数,我们看看这个。

2. scanDirectories

public void scanDirectories(String[] directories, String volumeName) {

try {

long start = System.currentTimeMillis();

initialize(volumeName);//初始化

prescan(null);//扫描前的预处理

long prescan = System.currentTimeMillis();

for (int i = 0; i < directories.length; i++) {

//扫描文件夹,这里有一个很重要的参数 mClient

// processDirectory是一个native函数

processDirectory(directories[i], MediaFile.sFileExtensions, mClient);

}

long scan = System.currentTimeMillis();

postscan(directories);//扫描后处理

long end = System.currentTimeMillis();

…..打印时间,异常处理没了

下面简单讲讲initialize preScanpostScan都干嘛了。

private void initialize(String volumeName) {

//打开MediaProvider,获得它的一个实例

mMediaProvider = mContext.getContentResolver().acquireProvider("media");

//得到一些uri

mAudioUri = Audio.Media.getContentUri(volumeName);

mVideoUri = Video.Media.getContentUri(volumeName);

mImagesUri = Images.Media.getContentUri(volumeName);

mThumbsUri = Images.Thumbnails.getContentUri(volumeName);

//外部存储的话,可以支持播放列表之类的东西,搞了一些个缓存池之类的

//mGenreCache

if (!volumeName.equals("internal")) {

// we only support playlists on external media

mProcessPlaylists = true;

mGenreCache = new HashMap<String, Uri>();

preScan,这个函数很复杂:

大概就是创建一个FileCache,用来缓存扫描文件的一些信息,例如last_modified等。这个FileCache是从MediaProvider中已有信息构建出来的,也就是历史信息。后面根据扫描得到的新信息来对应更新历史信息。

postScan,这个函数做一些清除工作,例如以前有video生成了一些缩略图,现在video文件被干掉了,则对应的缩略图也要被干掉。

另外还有一个mClient,这个是从MediaScannerClient派生下来的一个东西,里边保存了一个文件的一些信息。后续再分析。

刚才说到,具体扫描工作是在processDirectory函数中完成的。这个是一个native函数。

frameworks/base/media/jni/android_media_MediaScanner.cpp中。

MediaScanner JNI层分析

MediaScanner JNI层内容比较多,单独搞一节分析吧。

先看看android_media_MediaScanner这个文件。

1. native_init函数,jni对应的函数如下

static void

android_media_MediaScanner_native_init(JNIEnv *env)

{

jclass clazz;

clazz = env->FindClass("android/media/MediaScanner");

//得都JAVA类中mNativeContext这个成员id

fields.context = env->GetFieldID(clazz, "mNativeContext", "I");

//不熟悉JNI的自己去学习下吧

}

3. native_setup函数,jni对应函数如下:

android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)

{

//创建MediaScanner对象

MediaScanner *mp = createMediaScanner();

//太变态了,自己不保存这个对象指针.

//却把它设置到java对象的mNativeContext去保存

env->SetIntField(thiz, fields.context, (int)mp);

}

//创建MediaScanner函数

static MediaScanner *createMediaScanner() {

#if BUILD_WITH_FULL_STAGEFRIGHT

..

//使用google自己的

return new StagefrightMediaScanner;

#endif

#ifndef NO_OPENCORE

//使用opencore提供的

….

return new PVMediaScanner();

#endif

4. processDirectories函数,jni对应如下:

android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject client)

{

MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

//每次都要回调到JAVA中去取这个Scanner!!

………

const char *pathStr = env->GetStringUTFChars(path, NULL);

const char *extensionsStr = env->GetStringUTFChars(extensions, NULL);

…….

//又在C++这里搞一个client,然后把javaclient放到C++Client中去保存

//而且还是栈上的临时变量..

MyMediaScannerClient myClient(env, client);

//scanner扫描文件夹,用得是C++client

mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);

env->ReleaseStringUTFChars(path, pathStr);

env->ReleaseStringUTFChars(extensions, extensionsStr);

}

到这里似乎就没有了,那么扫描后的数据库是怎么更新的呢?为什么要传入一个client进去呢?看来必须得tracescanner中去才知道了。

PVMediaScanner

这个在external/opencore/android/mediascanner.cpp中。

1. processDirectory

status_t MediaScanner::processDirectory(const char *path, const char* extensions,

MediaScannerClient& client, ExceptionCheck exceptionCheck, void* exceptionEnv)

{

InitializeForThread();

int error = 0;

status_t result = PVMFSuccess;

….

//调用client的设置区域函数

client.setLocale(mLocale);

//扫描文件夹,咋还没开始??

result = doProcessDirectory(pathBuffer, pathRemaining, extensions, client, exceptionCheck, exceptionEnv);

..

2. doProcessDirectory

status_t MediaScanner::doProcessDirectory(char *path, int pathRemaining, const char* extensions,

MediaScannerClient& client, ExceptionCheck exceptionCheck, void* exceptionEnv)

{

终于看到点希望了

//打开这个文件夹,枚举其中的内容。

//题外话,这个时候FileManager肯定删不掉这个文件夹!!

DIR* dir = opendir(path);

while ((entry = readdir(dir))) {

const char* name = entry->d_name;

//不处理...文件夹

if (isDirectory) {

……..

//不处理.开头的文件夹。如果是文件夹,递归调用doProcessDirectory

//深度优先啊!

int err = doProcessDirectory(path, pathRemaining - nameLength - 1, extensions, client, exceptionCheck, exceptionEnv);

if (err) {

LOGE("Error processing '%s' - skipping/n", path);

continue;

}

} else if (fileMatchesExtension(path, extensions)) {

//是一个可以处理的文件,交给client处理

//彻底疯掉了….这是干嘛呢???

client.scanFile(path, statbuf.st_mtime, statbuf.st_size);

这里要解释下,刚才createMediaScanner中,明明创建的是PVMediaScanner,为何这里看得是MediaScanner代码呢?

l 因为PVMediaScannerMediaScanner中派生下来的,而且没有重载processDirectory函数

l Eclaire没有PVMediaScanner这样的东西,估计是froyo又改了点啥吧。

FTprocessDirctory无非是列举一个目录内容,然后又反回去调用clientscanFile处理了。为何搞这么复杂?只有回去看看C++client干什么了。

3. MediaScannerClient---JNI

JNI中的这个类是这样的:

class MyMediaScannerClient : public MediaScannerClient

//这是它的scanFile实现

virtual bool scanFile(const char* path, long long lastModified, long long fileSize)

{

//再次崩溃了,C++client调用了刚才传进去的java Client

//scanFile函数不过这次还传进去了该文件的路径,最后修改时间和文件大小。

mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);

…..想死,,,

}

没办法了,只能再去看看MediaScanner.java传进去的那个client了。

4. MediaScannerClient----JAVA

这个类在MediaScanner.java中实现。

private class MyMediaScannerClient implements MediaScannerClient

public void scanFile(String path, long lastModified, long fileSize) {

//调用doScanFile..很烦..

doScanFile(path, null, lastModified, fileSize, false);

//下面是doScanFile

public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) {

//预处理,看看之前创建的文件缓存中有没有这个文件

FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize);

// rescan for metadata if file was modified since last scan

if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {

//如果事先有这个文件的信息,则需要修改一些信息,如长度,最后修改时间等

…..

//真正的扫描文件

processFile(path, mimeType, this);

//扫描完了,做最后处理

endFile(entry, ringtones, notifications, alarms, music, podcasts);

processFile又是jni层的。

对应android_media_MediaScanner_processFile函数

android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)

{

MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

//无语了,又搞一个 MyMediaScannerClient

MyMediaScannerClient myClient(env, client);

mp->processFile(pathStr, mimeTypeStr, myClient);

…}

第一次到PVMediaScanner中来了

status_t PVMediaScanner::processFile(const char *path, const char* mimeType, MediaScannerClient& client)

{

status_t result;

InitializeForThread();

//调用clientbeginFile,估计是做一些啥预处理

client.setLocale(locale());

client.beginFile();

//LOGD("processFile %s mimeType: %s/n", path, mimeType);

const char* extension = strrchr(path, '.');

if (extension && strcasecmp(extension, ".mp3") == 0) {

result = parseMP3(path, client);//client又传进去了

根据后缀名去扫描….

}

client.endFile();

parseMP3去看看。这个在

static PVMFStatus parseMP3(const char *filename, MediaScannerClient& client)

{//这个函数太专业了,和编解码有关,我们重点关注client在这里干什么了

//原来,解析器从文件中解析出一个信息,就调用clientaddStringTag

if (!client.addStringTag("duration", buffer))

….

addStringTagJNIclient中处理。

这个MediaScannerClient是在opencore中的那个MediaScanner.cpp中实现的,而android_media_MediaScanner.cpp中的是MyMediaScannerClient,MediaScannerClient派生下来的

bool MediaScannerClient::addStringTag(const char* name, const char* value)

{

if (mLocaleEncoding != kEncodingNone) {

//字符串编码之类的转换。不详述了

bool nonAscii = false;

const char* chp = value;

char ch;

while ((ch = *chp++)) {

if (ch & 0x80) {

nonAscii = true;

break;

}

}

//如果不是ASCII编码的话,内部先保存一下这些个tag信息

//待会扫描完后再集中做一次字符串编码转换

if (nonAscii) {

// save the strings for later so they can be used for native encoding detection

mNames->push_back(name);

mValues->push_back(value);

return true;

}

// else fall through

}

//调用子类的handleStringTag

return handleStringTag(name, value);

}

class MyMediaScannerClient : public MediaScannerClient{

//调用到子类的handleStringTag

virtual bool handleStringTag(const char* name, const char* value)

{

//又传递到JAVA层的handleStringTag来处理

//麻木了..

mEnv->CallVoidMethod(mClient, mHandleStringTagMethodID, nameStr, valueStr);

}

JAVA

MediaScannerService中的MyMediaScannerClient

public void handleStringTag(String name, String value) {

//下层扫描的文件tag信息,全部处理后赋值给java层这个MyScannerClient

例如MP3title,专辑名等等。

….

int num = parseSubstring(value, 0, 0);

mTrack = (num * 1000) + (mTrack % 1000);

} else if (name.equalsIgnoreCase("duration")) {

mDuration = parseSubstring(value, 0, 0);

} else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {

mWriter = value.trim();

到这里了,还没写到数据库呢?啥时候更新数据库?看来是在client.endFile()中了。

但是这个endClient并没有调用到JAVA层去。那在哪里结束呢?

还记得JAVA中的doScanFile函数吗,对了,这个endFile就是在那里直接由JAVA调用的。

private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,

boolean alarms, boolean music, boolean podcasts)

throws RemoteException {

// update database

Uri tableUri;

boolean isAudio = MediaFile.isAudioFileType(mFileType);

boolean isVideo = MediaFile.isVideoFileType(mFileType);

boolean isImage = MediaFile.isImageFileType(mFileType);

….

//来了一个新文件,直接插入数据库

result = mMediaProvider.insert(tableUri, values);

//或者更新数据库

mMediaProvider.update(result, values, null, null);

这回真算是完了。

5.流程总结

l MediaScanner(MS)调用scanDirectories中的processDirectory,进入到JNI

l JNI调用PVMediaScannerprocessDirectory

l PVMediaScannerprocessDirectory为目录下的文件调用MyMediaScannerClientscanFile

l MyMediaScannerClient

分享到:
评论

相关推荐

    Android_MediaScanner__详尽分析

    该文是对Android_MediaScanner 的详尽分析,欢迎大家下载学习

    android MediaProvider和MediaScanner详解

    关于Android媒体存储和手机数据库扫描流程以及优化的部分代码贴图

    mediascanner流程分析

    mediascanner 流程 分析 mediascannerservice 分析

    mediascanner流程详细分析

    mediascanner 流程详细分析

    MediaScanner 源代码分析

    MediaScanner 源代码分析。我从网上搜集的关于MediaScanner 的源代码分析资料。欢迎大家下载学习

    《深入理解Android》卷Ⅰ

    2.3 Java层的MediaScanner分析 2.3.1 加载JNI库 2.3.2 Java的native函数和总结 2.4 JNI层MediaScanner的分析 2.4.1 注册JNI函数 2.4.2 数据类型转换 2.4.3 JNIEnv介绍 2.4.4 通过JNIEnv操作jobject 2.4.5 jstring...

    android_media_MediaScanner.rar_android_mediascanner

    helper function to throw an exception.

    android mediaScannder框架介绍

    Android 多媒体扫描过程详细介绍,MediaScannerReceiver 会在任何的 ACTION_BOOT_COMPLETED, ACTION_MEDIA_MOUNTED 或 ACTION_MEDIA_SCANNER_SCAN_FILE 意图( intent )发出的时候启动。因为解析媒体文件 的元数据 ...

    Android媒体库框架(mediascanner).doc

    Android媒体库框架(mediascanner)

    《深度理解Android:第一卷》

    《深入理解Android:卷I》是一本以情景方式对Android的源代码进行深入分析的书。内容广泛,以对Framework层的分析为主,兼顾Native层和Application层;分析深入,每一部分源代码的分析都力求透彻;针对性强,注重...

    《深入理解Android:卷I》试读本

    第2章通过对Android系统中的MediaScanner进行分析,详细讲解了Android中十分重要的JNI技术;第3章分析了init进程,揭示了通过解析init.rc来启动Zygote以及属性服务的工作原理;第4章分析了Zygote、SystemServer等...

    深入理解Android 卷1.pdf

    第2章通过对Android系统中的MediaScanner进行分析,详细讲解了Android中十分重要的JNI技术;第3章分析了init进程,揭示了通过解析init.rc来启动Zygote以及属性服务的工作原理;第4章分析了Zygote、SystemServer等...

    深入理解Android++卷1pdf电子书

    第2章通过对Android系统中的MediaScanner进行分析,详细讲解了Android中十分重要的JNI技术;第3章分析了init进程,揭示了通过解析init.rc来启动Zygote以及属性服务的工作原理;第4章分析了Zygote、SystemServer等...

    深入理解Android卷1

    第2章通过对Android系统中的MediaScanner进行分析,详细讲解了Android中十分重要的JNI技术;第3章分析了init进程,揭示了通过解析init.rc来启动Zygote以及属性服务的工作原理;第4章分析了Z ygote、SystemServer等...

    深入理解Android:卷2

    第2章通过对Android系统中的MediaScanner进行分析,详细讲解了Android中十分重要的JNI技术;第3章分析了init进程,揭示了通过解析init.rc来启动Zygote以及属性服务的工作原理;第4章分析了Z ygote、SystemServer等...

    深入理解Android:卷I--详细书签版

     结合实际应用开发需求,以情景分析的方式有针对性地对Android的源代码进行了十分详尽的剖析,深刻揭示Android系统的工作原理  机锋网、51CTO、开源中国社区等专业技术网站一致鼎力推荐 内容简介  《深入理解...

    深入理解Android 卷I

    第2章通过对android系统中的mediascanner进行分析,详细讲解了android中十分重要的jni技术;第3章分析了init进程,揭示了通过解析init.rc来启动zygote以及属性服务的工作原理;第4章分析了zygote、systemserver等...

    深入理解Android卷1全

    2.3 Java层的MediaScanner分析 / 16 2.3.1 加载JNI库 / 16 2.3.2 Java的native函数和总结 / 17 2.4 JNI层MediaScanner的分析 / 17 2.4.1 注册JNI函数 / 18 2.4.2 数据类型转换 / 22 2.4.3 JNIEnv介绍 / 24 2.4.4 ...

Global site tag (gtag.js) - Google Analytics