MediaProvider源码分析

Android  源码分析  2023年6月26日 am8:08发布1年前 (2023)更新 城堡大人
99 0 0

前言

MediaProvider继承自ContentProvider,是Android用于存储图片、音频、视频和文档等多媒体信息,提供给其他需要的应用使用。

今天就对MediaProvider应用代码进行一定学习。参考网上大佬文章,记录一下过程。

这里是用了Android P源码分析

正文

MediaProvider存在路径:

\packages\providers\MediaProvider

扫描后存在的数据目录

/data/data/com.android.providers.media/databases
# 或
/data/user_de/0/com.android.providers.media/databases

一般是在第一个目录,但也存在放在第二个目录,这个具体看平台是否有改动。

MediaProvider存在两个数据库

internal.db #内部数据库
external.db #外部数据库
重点

internal.db是查询指定目录的数据

//[/system/media, /oem/media, /product/media]
directories = new String[] {
        Environment.getRootDirectory() + "/media",
        Environment.getOemDirectory() + "/media",
        Environment.getProductDirectory() + "/media",
};

external.db 是包括sd目录和插入的磁盘目录中所有数据。

进入扫描

在继续跟进时,我们需要知道有几个入口让MediaProvider进入扫描,先看大佬们整理的

MediaProvider源码分析

从上面看,可以知道,进入扫描于两种方式

  1. 通过广播监听

  2. 通过服务绑定

通过这两种方式,我们进入分析MediaProvider。

广播监听

一般广播监听有两种,一种是动态广播,一种是静态广播,各有优点。一般看一个应用会先看AndroidManifest.xml配置。

AndroidManifest.xml

我们关注需要的,比如磁盘挂载等广播,我们会发现MediaScannerReceiver就是我们需要找的广播。

<receiver android:name="MediaScannerReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        <action android:name="android.intent.action.LOCALE_CHANGED" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_MOUNTED" />
        <data android:scheme="file" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_UNMOUNTED" />
        <data android:scheme="file" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE" />
        <data android:scheme="file" />
    </intent-filter>
</receiver>

上面监听了好些重要的广播,我们直接进入看MediaScannerReceiver怎么处理的。

MediaScannerReceiver.java
public void onReceive(Context context, Intent intent) {
    final String action = intent.getAction();
    final Uri uri = intent.getData();
    if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
        //开机广播,只扫描内置磁盘,内置的磁盘一定存在
        //INTERNAL_VOLUME = "internal"
        scan(context, MediaProvider.INTERNAL_VOLUME);
    } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
        //语言切换
        scanTranslatable(context);
    } else {
        if (uri.getScheme().equals("file")) {
            String path = uri.getPath();
            // 我这平台上 externalStoragePath = "/storage/emulated/0";
            String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
            // 我这平台上 legacyPath = "/sdcard";
            String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();
            try {
                path = new File(path).getCanonicalPath();
            } catch (IOException e) {
                return;
            }
            if (path.startsWith(legacyPath)) {
                //需要用绝对路径,确保统一
                path = externalStoragePath + path.substring(legacyPath.length());
            }
            if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                //根据磁盘挂载进行扫描外置磁盘
                //EXTERNAL_VOLUME = "external"
                scan(context, MediaProvider.EXTERNAL_VOLUME);
            } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                path != null && path.startsWith(externalStoragePath + "/")) {
                //只对内置sdcard多媒体更新进行扫描
                scanFile(context, path);
            }
        }
    }
}

上面涉及如下几个方法

  1. scan(Context context, String volume) //扫描内置或外置磁盘

  2. void scanFile(Context context, String path) //扫描指定文件[只能是文件!]

  3. scanTranslatable(Context context) // 设备语言变化了调用

上面是三个方法有以个共同点,都是startService()启动了MediaScannerService

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

MediaScannerService是个服务,一般都先看onCreate()和onStartCommand()。

  1. onCreate()

    public void onCreate() {
        //防止扫描过程中CPU睡死过去,扫描中需要
        PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
        //略
        //启动子线程,第二个参数Runnable传入的是this,也就是说MediaScannerService实现了run()
        Thread thr = new Thread(null, this, "MediaScannerService");
        thr.start();
    }
  2. onStartCommand()

    public int onStartCommand(Intent intent, int flags, int startId) {
        //等待mServiceHandler初始化
        while (mServiceHandler == null) {
            synchronized (this) {
                try {
                    wait(100);
                } catch (InterruptedException e) {
                }
            }
        }
    	//略
    	//放入消息队列,让ServiceHandler处理。
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent.getExtras();
        mServiceHandler.sendMessage(msg);
        return Service.START_REDELIVER_INTENT;
    }

这里主要是把intent中参数丢入消息队列,让mServiceHandler处理。

  1. run()

    上面启动了子线程,MediaScannerService也实现了run()

    public void run() {
        //将优先级降低到其他后台线程以下以避免干扰其他服务。
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
                Process.THREAD_PRIORITY_LESS_FAVORABLE);
    	//很熟悉吧,启动Looper,这样就可以使用ServiceHandler
        Looper.prepare();
        mServiceLooper = Looper.myLooper();
        mServiceHandler = new ServiceHandler();
        Looper.loop();
    }
  2. ServiceHandler

    private final class ServiceHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Bundle arguments = (Bundle) msg.obj;
            if (arguments == null) {
                return;
            }
            String filePath = arguments.getString("filepath");
            try {
    		    //文件扫描
                if (filePath != null) {
                    IBinder binder = arguments.getIBinder("listener");
                    //如果是从广播来的,binder= null
                    IMediaScannerListener listener =
                            (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
                    Uri uri = null;
                    try {
                     	//【重点】scanFile
                        uri = scanFile(filePath, arguments.getString("mimetype"));
                    } catch (Exception e) {
                        Log.e(TAG, "Exception scanning file", e);
                    }
                    if (listener != null) {
                        listener.scanCompleted(filePath, uri);
                    }
                } else if (arguments.getBoolean(MediaStore.RETRANSLATE_CALL)) {
    			    //设备语言改变后的
                    ContentProviderClient mediaProvider = getBaseContext().getContentResolver()
                        .acquireContentProviderClient(MediaStore.AUTHORITY);
                    mediaProvider.call(MediaStore.RETRANSLATE_CALL, null, null);
                } else {
    				//扫描挂载的磁盘
                    String volume = arguments.getString("volume");
                    String[] directories = null;
    				//内置磁盘
                    if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
                        directories = new String[] {
                                Environment.getRootDirectory() + "/media",
                                Environment.getOemDirectory() + "/media",
                                Environment.getProductDirectory() + "/media",
                        };
                    } //外置磁盘
                    else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
                        if (getSystemService(UserManager.class).isDemoUser()) {
                            directories = ArrayUtils.appendElement(String.class,
                                    mExternalStoragePaths,
                                 Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
                        } else {
                            directories = mExternalStoragePaths;
                        }
                    }
                    if (directories != null) {
                        //【重点】scan
                        scan(directories, volume);
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "Exception in handleMessage", e);
            }
    		//扫描结束,自动销毁
            stopSelf(msg.arg1);
        }
    }

    上面涉及scanFile()和scan()方法。主要是的区别是前者扫描文件,后者是扫描磁盘。

  3. scan()

    private void scan(String[] directories, String volumeName) {
        Uri uri = Uri.parse("file://" + directories[0]);
    	//扫描中不让休眠
        mWakeLock.acquire();
        try {
            ContentValues values = new ContentValues();
    		//MEDIA_SCANNER_VOLUME = "volume"
            values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
    		//让MeidaProvider做一些准备工作??(没有具体看,网上大佬这么说的)
    		//scanUri = content://media/none/media_scanner
            Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
    		//发出开始扫描广播
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
            try {
                if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
    				// 打开EXTERNAL数据库[为啥要判断,可以看文章中【重点】部分]
                    openDatabase(volumeName);
                }
    			//创建MediaScanner,调用scanDirectories扫描目标文件夹。
                try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
                	//directories可能是[/storage/emulated/0, /storage/udisk0]
                    scanner.scanDirectories(directories);
                }
            } catch (Exception e) {
                Log.e(TAG, "exception in MediaScanner.scan()", e);
            }
    		// 通过 delete 这个 uri,让 MeidaProvider 做一些清理工作
            getContentResolver().delete(scanUri, null, null);
        } finally {
    		//发送扫描完成的广播
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
    		Log.d(TAG, "scan +++++ ACTION_MEDIA_SCANNER_FINISHED +++++  : ");
            mWakeLock.release();
        }
    }

    上面正在扫描工作是在MediaScanner中。

  4. scanFile()

    private Uri scanFile(String path, String mimeType) {
        String volumeName = MediaProvider.EXTERNAL_VOLUME;
        try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
            String canonicalPath = new File(path).getCanonicalPath();
            return scanner.scanSingleFile(canonicalPath, mimeType);
        } catch (Exception e) {
            return null;
        }
    }

    scanFile()指定扫描某个文件,最终的扫描逻辑依旧在MediaScanner中。

MediaScanner后续单独分析,这里不做过多的介绍。从上面看,从广播进入扫描的就分析完了。

服务绑定

除了ACTION_MEDIA_SCANNER_SCAN_FILE广播,Android还可以通过bindService机制得到IMediaScannerService代理接口,再通过该接口的requestScanFile()或scanFile()请求扫描,不过这个只能扫描文件。

先看看MediaScannerService返回的IBinder

@Override
public IBinder onBind(Intent intent) {
    //返回binder代理
    return mBinder;
}

private final IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub() {
	//方法一
    public void requestScanFile(String path, String mimeType, IMediaScannerListener listener) {
        Bundle args = new Bundle();
        args.putString("filepath", path);
        args.putString("mimetype", mimeType);
        if (listener != null) {
            args.putIBinder("listener", listener.asBinder());
        }
        startService(new Intent(MediaScannerService.this,
                MediaScannerService.class).putExtras(args));
    }

	//方法二
    public void scanFile(String path, String mimeType) {
        requestScanFile(path, mimeType, null);
    }
};

有两个方法requestScanFile()和scanFile(),最终还是调用了requestScanFile,也就是带上参数并启动服务。

其中IMediaScannerService定义的的目录

frameworks\base\media\java\android\media\IMediaScannerService.aidl

由于一般不涉及这块,懒得继续看。推荐最后的参考文章,此文对这块讲解比较详细。

参考文章

  1. Android MediaScanner:(二)MediaScannerReceiver

  2. MediaProvider流程分析Android 9和Android 10版本

  3. MediaProvider流程分析

  4. Android MediaProvider

  5. MediaScannerService 研究》[推荐看这个]

 历史上的今天

  1. 2024: 林语堂:有丰富的心灵才有悠闲的生活(0条评论)
  2. 2021: Android调试,删除adb devices显示的多余设备(0条评论)
  3. 2021: 席慕容 :盼望(0条评论)
  4. 2020: [摘]Android开发之Shape详细解读(0条评论)
  5. 2019: 龙应台:目送(0条评论)
版权声明 1、 本站名称: 笔友城堡
2、 本站网址: https://www.biumall.com/
3、 本站部分文章来源于网络,仅供学习与参考,如有侵权,请留言

暂无评论

暂无评论...

随机推荐

个人常用的GridView方法简介

前言Android中GridView还是比较常用的,GridView有些方法或者配置属性都是比较常用也比较容易忘记的。因此,今天抽空整(抄)理(袭)一下,以便查阅。PS: 现在RecyclerView比较多了正文GridView跟ListView有很多一样的属性或者方法。因此这里就更简...

Java多线程编程核心技术的笔记之1

1、什么是进程,线程和多线程1. 进程在百度百科中的定义是进程(process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.程序是指令,数据以及组织形式的描述,进程是程序的实体.我们可以把win系统中运行的一个exe程序看做一个...

Windows软件开机多个

前言有时候软件需要多开,只开一个操作不方便,比如Notepad++进行日志查询时,多开一个可以拖入其他更多的文件,因此想是开启多个界面。正文百度和谷歌后发现真的有人这么干,如下。找到一个Notepad++的快捷方式(桌面或启动菜单中)属性,选中"Shortcut"(快捷键) Tab,编辑 T...

Android的5个进程等级

 一、进程:进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。进程是系统进行资源分配和调度的一个独立单位。可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体,是一个“执行中的程序”。不只是程序的代码,还包括当前的活动。二、线程:线程是进程的一个实体,是CPU调度和...

余华:麦田里

我在南方长大成人,一年四季、一日三餐的食物都是大米,由于很少吃包子和饺子,这类食物就经常和节日有点关系了。小时候,当我看到做外科医生的父亲手里提着一块猪肉,捧着一袋面粉走回家时,我就知道这一天是什么日子了。在我小时候有很多节日,五月一日是劳动节,六月一日是儿童节,七月一日是建党节,八月一日是建军节,...

沈从文 :生命

我好像为什么事情很悲哀,我想起“生命”。有什么人能用绿竹作弓矢,射入云空,永不落下?我之想象,犹如长箭,向云空射去,去即不返。长箭所注,在碧蓝而明静之广大虚空。明智者若善用其明智,即可从此云空中,读示一小文,文中有微叹与沉默,色与香,爱和怨。无著者姓名。无年月。无故事。无……然而内容极柔美。虚空...