diff --git a/AdavancedPart/AOP.md b/AdavancedPart/AOP.md index 3bcc633b..53c518aa 100644 --- a/AdavancedPart/AOP.md +++ b/AdavancedPart/AOP.md @@ -11,4 +11,41 @@ AOP(Aspect Oriented Programing),面向切面编程。 而AOP,就是将各个模块中的通用逻辑抽离出来。 我们将这些逻辑视为Aspect(切面),然后动态地把代码插入到类的指定方法、指定位置中。 +一句话概括: 在运行时,动态的将代码切入到类的指定方法、指定位置上的编程思想就是面相切面的编程。 + + +一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。 + + +### AOP的实现方式 + +#### 静态AOP + +在编译器,切面直接以字节码的形式编译到目标字节码文件中。 + +1. AspectJ +AspectJ属于静态AOP,它是在编译时进行增强,会在编译时期将AOP逻辑织入到代码中。 + +由于是在编译器织入,所以它的优点是不影响运行时性能,缺点是不够灵活。 + +2. AbstractProcessor +自定义一个AbstractProcessor,在编译期去解析编译的类,并且根据需求生成一个实现了特定接口的子类(代理类) + +#### 动态AOP +1. JDK动态代理 +通过实现InvocationHandler接口,可以实现对一个类的动态代理,通过动态代理可以生成代理类,从而在代理类方法中,在执行被代理类方法前后,添加自己的实现内容,从而实现AOP。 + +2. 动态字节码生成 +在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中,没有接口也可以织入,但扩展类的实例方法为final时,则无法进行织入。比如Cglib + +CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。 + +3. 自定义类加载器 +在运行期,目标加载前,将切面逻辑加到目标字节码里。如:Javassist + +Javassist是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。 + +4. ASM +ASM可以在编译期直接修改编译出的字节码文件,也可以像Javassit一样,在运行期,类文件加载前,去修改字节码。 + diff --git "a/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" "b/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" index a64d0fe2..e268531a 100644 --- "a/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" +++ "b/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" @@ -557,4 +557,4 @@ public void restoreLayoutParams(ViewGroup.LayoutParams params) { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" index 5968cbbe..67820a04 100644 --- "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" +++ "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" @@ -4,12 +4,17 @@ Gradle专题 作用 --- -[Gradle](https://docs.gradle.org/7.3.3/userguide/what_is_gradle.html)是一个开源的自动化构建工具。现在Android项目构建编译都是通过Gradle进行的,Gradle的版本在`gradle/wrapper/gradle-wrapper.properties`下: +[Gradle](https://docs.gradle.org/7.3.3/userguide/what_is_gradle.html)是一个开源的自动化构建工具。现在Android项目构建编译都是通过Gradle进行的。 + +Gradle的版本在`gradle/wrapper/gradle-wrapper.properties`下: ![image](https://github.com/CharonChui/Pictures/blob/master/gradle_version.png?raw=true) 当前Gradle版本为6.7.1。当我们执行assembleDebug/assembleRelease编译命令的时候,Gradle就会开始进行编译构建流程。 +gradle-wrapper是对Gradle的一层包装,便于在团队开发过程中统一Gradle构建的版本号,这样大家都可以使用统一的Gradle版本进行构建。 +里面的distributionUrl属性是用于配置Gradle发行版压缩包的下载地址。 + 简介 --- diff --git "a/KotlinCourse/.8.Kotlin_\345\215\217\347\250\213.md.swp" "b/KotlinCourse/.8.Kotlin_\345\215\217\347\250\213.md.swp" new file mode 100644 index 00000000..996ab163 Binary files /dev/null and "b/KotlinCourse/.8.Kotlin_\345\215\217\347\250\213.md.swp" differ diff --git "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" index 6128245e..5e1b19fa 100644 --- "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" +++ "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" @@ -461,6 +461,20 @@ private var lazyValue: Fragment? = null 当您稍后需要在代码中初始化var时,请选择lateinit,它将被重新分配。当您想要初始化一个val值一次时,特别是当初始化的计算量很大时,请选择by lazy。 +```kotlin +val name: String by lazy {getName()} +``` +这样,当第一次使用name引用时,getName()函数只会被调用一次。此外,还可以使用函数引用代替lambda表达式: +```kotlin +val name: String by lazy(::getName) + +fun getName() : String { + println("computing name") + return "Mockey" +} +``` + + ## 类的定义:使用`class`关键字 diff --git "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" index b1e79261..60f01257 100644 --- "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -7,15 +7,22 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它 ## 起源 -协程是一个无优先级的子程序调用组件,允许子程序在特定的地方挂起恢复。线程包含于进程,协程包含于线程。只要内存足够, -一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。 - 线程是由操作系统来进行调度的,当操作系统切换线程的时候,会产生一定的消耗。而协程不一样,协程是包含于线程的,也就是说协程 是工作在线程之上的,协程的切换可以由程序自己来控制,不需要操作系统进行调度。这样的话就大大降低了开销。 -协程就像一个轻量级的线程在幕后,启动协程就像启动一个单独的执行线程。线程在其他语言中很常见,例如Java,协程和线程可以并行运行,并互相通信。然而,不同点在于使用协程比使用线程更加高效。在性能方面,启动一个线程并使其保持运行是非常昂贵的。处理器通常只能同时运行有限数量的线程,并且运行尽可能少的线程会更高效。而另一方面,协程默认运行在共享的线程池中,同一个线程可以运行多个协程。由于使用的线程较少,当你想要运行异步任务时,使用协程会更加高效。 +协程就像一个轻量级的线程在幕后,启动协程就像启动一个单独的执行线程。线程在其他语言中很常见,例如Java,协程和线程可以并行运行,并互相通信。然而,不同点在于使用协程比使用线程更加高效。在性能方面,启动一个线程并使其保持运行是非常昂贵的。 + +处理器通常只能同时运行有限数量的线程,并且运行尽可能少的线程会更高效。 + +而另一方面,协程默认运行在共享的线程池中,同一个线程可以运行多个协程。由于使用的线程较少,当你想要运行异步任务时,使用协程会更加高效。 + +**也就是说本质上Kotlin协程就是创建了一个可以复用的线程池,并且协程的delay是一个特殊的挂起函数,它不会造成线程堵塞,但是会挂起协程,并且只能在协程中使用。** + +协程是语言层面的东西,线程是系统层面的东西。 +协程就是一段代码块,既然是代码那就离不开CPU的执行,而CPU调度的基本单位是线程。 + ## 进程、线程、协程 进程:一段程序的执行过程,资源分配和调度的基本单位,有其独立地址空间,互相之间不发生干扰 @@ -33,7 +40,15 @@ CPU增加内存管理单元,进行虚拟地址和物理地址的转换 进程是一个实体,包括程序代码以及其相关资源(内存,I/O,文件等),可被操作系统调度。但想一边操作I/O进行输入输出,一边想进行加减计算,就得两个进程,这样写代码,内存就爆表了。于是又想着能否有一轻量级进程呢,只执行程序,不需要独立的内存,I/O等资源,而是共享已有资源,于是产生了线程。 -一个进程可以跑很多个线程处理并发,但是线程进行切换的时候,操作系统会产生中断,线程会切换到相应的内核态,并进行上下文的保存,这个过程不受上层控制,是操作系统进行管理。然而内核态线程会产生性能消耗,因此线程过多,并不一定提升程序执行的效率。正是由于1.线程的调度不能精确控制;2.线程的切换会产生性能消耗。协程出现了。 +一个进程可以跑很多个线程处理并发,但是线程进行切换的时候,操作系统会产生中断,线程会切换到相应的内核态,并进行上下文的保存,这个过程不受上层控制,是操作系统进行管理。 +然而内核态线程会产生性能消耗,因此线程过多,并不一定提升程序执行的效率。 + +正是由于: + +1. 线程的调度不能精确控制; +2. 线程的切换会产生性能消耗。 + +协程出现了。 协程: @@ -43,17 +58,30 @@ CPU增加内存管理单元,进行虚拟地址和物理地址的转换 4. 协程是非抢占式调度,当前协程切换到其他协程是由自己控制;线程则是时间片用完抢占时间片调度 优点: + 1. 用户态,语言级别 2. 无切换性能消耗 3. 非抢占式 4. 同步代码思维 5. 减少同步锁 + 缺点: + 1. 注意全局变量 2. 阻塞操作会导致整个线程被阻塞 -用一句话概括Kotlin Couroutine的特点即是"以同步之名,行异步之实". + +简单地讲,Kotlin 的协程就是一个封装在线程上面的线程框架。 + +它有两个非常关键的亮点: + +- 耗时函数自动后台,从而提高性能; +- 线程的「自动切回」 + +所以,Kotlin 的协程在 Android 开发上的核心好处就是:消除回调地域。 + + ## 使用 如需在 Android 项目中使用协程,请将以下依赖项添加到应用的build.gradle文件中: @@ -400,7 +428,6 @@ main: Now I can quit. 对于回调式的写法,如果并发场景再复杂一些,代码的嵌套可能会更多,这样的话维护起来就非常麻烦。但如果你使用了 Kotlin 协程,多层网络请求只需要这么写: ``` -🏝️ coroutineScope.launch(Dispatchers.Main) { // 开始协程:主线程 val token = api.getToken() // 网络请求:IO 线程 val user = api.getUser(token) // 网络请求:IO 线程 @@ -413,8 +440,6 @@ coroutineScope.launch(Dispatchers.Main) { // 开始协程:主线程 协程最简单的使用方法,其实在前面章节就已经看到了。我们可以通过一个 `launch` 函数实现线程切换的功能: ``` -🏝️ -// 👇 coroutineScope.launch(Dispatchers.IO) { ... } @@ -426,24 +451,20 @@ coroutineScope.launch(Dispatchers.IO) { 所以,什么时候用协程?当你需要切线程或者指定线程的时候。你要在后台执行任务?切! ``` -🏝️ launch(Dispatchers.IO) { val image = getImage(imageId) } -复制代码 ``` 然后需要在前台更新界面?再切! ``` -🏝️ coroutineScope.launch(Dispatchers.IO) { val image = getImage(imageId) launch(Dispatch.Main) { avatarIv.setImageBitmap(image) } } -复制代码 ``` 好像有点不对劲?这不还是有嵌套嘛。 @@ -451,20 +472,17 @@ coroutineScope.launch(Dispatchers.IO) { 如果只是使用 `launch` 函数,协程并不能比线程做更多的事。不过协程中却有一个很实用的函数:`withContext` 。这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。那么可以将上面的代码写成这样: ``` -🏝️ coroutineScope.launch(Dispatchers.Main) { // 👈 在 UI 线程开始 val image = withContext(Dispatchers.IO) { // 👈 切换到 IO 线程,并在执行完成后切回 UI 线程 getImage(imageId) // 👈 将会运行在 IO 线程 } avatarIv.setImageBitmap(image) // 👈 回到 UI 线程更新 UI } -复制代码 ``` 这种写法看上去好像和刚才那种区别不大,但如果你需要频繁地进行线程切换,这种写法的优势就会体现出来。可以参考下面的对比: ``` -🏝️ // 第一种写法 coroutineScope.launch(Dispachers.IO) { ... @@ -491,13 +509,11 @@ coroutineScope.launch(Dispachers.Main) { } ... } -复制代码 ``` 由于可以"自动切回来",消除了并发代码在协作时的嵌套。由于消除了嵌套关系,我们甚至可以把 `withContext` 放进一个单独的函数里面: ``` -🏝️ launch(Dispachers.Main) { // 👈 在 UI 线程开始 val image = getImage(imageId) avatarIv.setImageBitmap(image) // 👈 执行结束后,自动切换回 UI 线程 @@ -506,7 +522,6 @@ launch(Dispachers.Main) { // 👈 在 UI 线程开始 fun getImage(imageId: Int) = withContext(Dispatchers.IO) { ... } -复制代码 ``` 这就是之前说的「用同步的方式写异步的代码」了。 @@ -514,22 +529,14 @@ fun getImage(imageId: Int) = withContext(Dispatchers.IO) { 不过如果只是这样写,编译器是会报错的: ``` -🏝️ fun getImage(imageId: Int) = withContext(Dispatchers.IO) { // IDE 报错 Suspend function'withContext' should be called only from a coroutine or another suspend funcion } -复制代码 ``` 意思是说,`withContext` 是一个 `suspend` 函数,它需要在协程或者是另一个 `suspend` 函数中调用。 - - - - - - #### 挂起函数 当我们调用标记有特殊修饰符`suspend`的函数时,会发生挂起: @@ -539,9 +546,13 @@ suspend fun doSomething(foo: Foo): Bar { …… } ``` -这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式 -获取参数和返回值,但它们只能从协程和其他挂起函数中调用。事实上,要启动协程, -必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起`lambda`表达式)。让我们来看一个例子,一个简化的`async()`函数 +这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。 + +挂起函数能够以与普通函数相同的方式获取参数和返回值,但它们只能从协程和其他挂起函数中调用。 + +事实上,要启动协程,必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起`lambda`表达式)。 + +让我们来看一个例子,一个简化的`async()`函数 (源自`kotlinx.coroutines`库): ```kotlin @@ -558,7 +569,8 @@ async { } ``` -`await()`可以是一个挂起函数(因此也可以在一个`async {}`块中调用),该函数挂起一个协程,直到一些计算完成并返回其结果: +`await()`可以是一个挂起函数(因此也可以在一个`async {}`块中调用),该函数挂起一个协程,直到一些计算完成并返回其结果: + ```kotlin async { …… @@ -569,13 +581,15 @@ async { ``` -请注意,挂起函数`await()`和`doSomething()`不能在像`main()`这样的普通函数中调用: +请注意,挂起函数`await()`和`doSomething()`不能在像`main()`这样的普通函数中调用: + ```kotlin fun main(args: Array) { doSomething() // 错误:挂起函数从非协程上下文调用 } ``` 还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定`suspend`修饰符: + ```kotlin interface Base { suspend fun foo() diff --git a/README.md b/README.md index 5f20491c..234020ac 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Android学习笔记 - [1.音视频基础知识][328] - [2.系统播放器MediaPlayer][329] - [11.播放器组件封装][330] + - [MediaMetadataRetriever][344] - [DNS及HTTPDNS][23] - [流媒体协议][224] - [流媒体协议][246] @@ -721,6 +722,8 @@ Android学习笔记 [341]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E5%B0%81%E8%A3%85%E6%A0%BC%E5%BC%8F/AVI.md "AVI" [342]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/OpenCV "OpenCV" [343]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenCV/1.OpenCV%E7%AE%80%E4%BB%8B.md "1.OpenCV简介" +[344]: "MediaMetadataRetriever" + Developed By diff --git "a/SourceAnalysis/ARouter\350\247\243\346\236\220.md" "b/SourceAnalysis/ARouter\350\247\243\346\236\220.md" new file mode 100644 index 00000000..3468f729 --- /dev/null +++ "b/SourceAnalysis/ARouter\350\247\243\346\236\220.md" @@ -0,0 +1,55 @@ +# ARouter解析 + +[ARouter](https://github.com/alibaba/ARouter) + +> 一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦 + +简单的说: 它是一个路由系统 ——— 给无依赖的组件提供通信和路由的能力。 + +举个例子: +你过节了你想写一个明信片递给远方的朋友,那你就需要通过邮局(ARoter)来把明信片派送给你的朋友(你和你的朋友相当于两个组件)。 + +使用ARouter在进行Activity跳转非常简单: + +- 初始化ARouter `ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化` +- 添加注解@Route + ```java + // 在支持路由的页面上添加注解(必选) + // 这里的路径需要注意的是至少需要有两级,/xx/xx + @Route(path = "/test/activity") + public class YourActivity extend Activity { + ... + } + ``` +- 发起路由 `ARouter.getInstance().build("/test/activity").navigation();` + + + +ARouter框架能将多个服务提供者隔离,减少相互之间的依赖。其实现的流程和我们平常的快递物流管理很类似,每一个具体的快递包裹就是一个独立的服务提供者(IProvider),每一个快递信息单就是一个RouteMeta对象,客户端就是快递的接收方,而使用@Route注解中的path就是快递单号。在初始化流程中,主要完成的工作就是将所有注册的快递信息表都在物流中心(LogisticsCenter)注册,并将数据存储到数据仓库中(Warehouse)。 + +作者:魔焰之 +链接:https://www.jianshu.com/p/11006054f156 +来源:简书 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" index e69de29b..d2d01f24 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" @@ -0,0 +1,35 @@ +# MediaExtractor、MediaCodec、MediaMuxer + + +## MediaExtractor + +MediaExtractor是一个Android系统用于从多媒体文件中提取音频和视频数据的类。 + +它可以从本地文件或网络流中读取音频和视频数据,并将其解码为原始的音频和视频帧。它的主要功能就是解封装,也就是从媒体文件中提取出原始的音频和视频数据流。这些数据流可以被送入解码器进行解码,然后进行播放或者其他处理。 + +MediaExtractor可以用于开发音视频播放器、视频编辑器、音频处理器等应用程序。 + + +## MediaMuxter + +MediaMuxter是Android系统提供的一个用于混合音频和视频数据的API。 + +它可以将音频和视频的原始数据流混合封装成媒体文件,例如MP4、WebM等。 + +MediaMuxter通常与MediaExtractor一起使用,MediaExtractor用于从媒体文件中提取音频和视频数据,MediaMuxter用于将这些数据混合成新的媒体文件。 + +简单说就是: MediaExtractor提供了解封装的能力,而MediaMuxer提供了视频封装的能力。 + + +## MediaCodec + +MediaCodec是Android提供的用于对音视频进行编码的类,是Android Media基础框架的一部分,一般和MediaExtractor、MediaMuxer、Surface和AudioTrack一起使用。 + + + + +MediaExtractor仅仅是解封装数据,不会对数据进行解码。要对媒体数据进行解码,需要使用MediaCodec类。 + +而且MediaExtractor只能解封装媒体文件中的音视频等媒体轨道,而不能解析整个媒体文件的结构。如果需要解析整个媒体文件的结构,需要使用其他库或框架。 + + diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaMetadataRetriever.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaMetadataRetriever.md" new file mode 100644 index 00000000..a57255ab --- /dev/null +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaMetadataRetriever.md" @@ -0,0 +1,33 @@ +# MediaMetadataRetriever + +MediaMetadataRetriever是Android中用于从媒体文件中提取元数据的类,可以获取音频、视频和图片文件的各种信息,例如时长、标题、封面等。 + +```java +MediaMetadataRetriever mRetriever = new MediaMetadataRetriever(); + +mRetriever.setDataSource(mContext, mVideoUri); +``` + +## 获取元数据 + +// 获取歌曲标题 +`mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);` + +根据key的不同还可以是时长、帧率、分辨率等。 + +## 获取缩略图 + +`public Bitmap getFrameAtTime(long timeUs, int option)` +用于获取音视频文件中的一帧画面,可以用于实现缩略图、视频预览等功能。 + +- timeUs:是获取画面对应的时间戳,单位为微妙 +- option:可以设置是获取离指定时间戳最近的一帧画面或者事最近的关键帧画面等。 + +## 获取专辑封面图 + +```java +byte[] bytes = mRetriever.getEmbeddedPicture(); +Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); +``` +需要注意的是,MediaMetadataRetriever只能用于读取已经完成的音视频文件,无法用于实时处理音视频数据流。 +