Android Framework源码解析篇~

news2024/11/18 11:28:38

应用启动流程

Activity启动流程

关键debug节点:

//左侧 ActivithThread
//右侧 ActivityTaskManagerService>..>ActivityTaskSupervisor
//中继 debug:ClientTransaction.schedule>mclient.scheduleTransaction
//这里是 mclient是 ApplicationThread,从这里通过binder回到ActivithThread的
//父类 ClientTransactionHandler.scheduleTransaction>TransactionExecutor>
//TransactionExecutor负责解析ClientTransaction,分发回执到ATMS到消息与
//调用ActivityThread的各个launch方法
//要注意TransactionExecutor.performLifecycleSequence中的switch基本只会进入ON_CREATE,其他的在各Launch
//方法中自己串联的
//回到左侧 ActivithThread各Launch,创建activity,执行resume,发送idle消息执行stop,destory等
从启动看重点:ActivivtyRecord在何时创建,

ActivityThread extends ClientTransactionHandler

下面都是 通过binder发送到ApplicationThread由它调用ActivityThread父类的 scheduleTransaction

最后在TransactionExecutor(ClientTransactionHandler)的 executeCallbacks,executeLifecycleState 中调用对应的 execute方法,分配到对应的 ActivityThread 的各个生命周期调用里

第一次一个消息: TopResumedActivityChangeItem

ActivityTaskSupervisor:mAtmService.getLifecycleManager().scheduleTransaction(binder:ApplicationThread, appToken,
        TopResumedActivityChangeItem.obtain(onTop));
ClientLifecycleManager.scheduleTransaction{
ClientTransaction clientTransaction = transactionWithCallback(client, activityToken, callback);
{new ClientTransaction(client=ApplicationThread)}
}
clientTransaction.schedule()

第二次一个消息: PauseActivityItem

ActivityTaskSupervisor:mAtmService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
        prev.appToken, PauseActivityItem.obtain(prev.finishing, userLeaving,
                prev.configChangeFlags, pauseImmediately));

第三次 2个 消息:launch and resume

ActivityTaskSupervisor:
final ClientTransaction clientTransaction = ClientTransaction.obtain(
        proc.getThread(), r.appToken);
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
//还有一个变量:mLifecycleStateRequest:ResumeActivityItem
mService.getLifecycleManager().scheduleTransaction(clientTransaction);

第四次一个消息:TopResumedActivityChangeItem

ActivityTaskSupervisor:
ActivityRecord.scheduleTopResumedActivityChanged{
mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
        TopResumedActivityChangeItem.obtain(onTop));
}

第五个消息 : StopActivityItem

ActivityTaskSupervisor:
ActivityTaskSupervisor.activityIdleInternal>processStoppingAndFinishingActivities>
ActivityRecord.stopPossible{
mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
        StopActivityItem.obtain(configChangeFlags));
}

重点关注ActivityThread.handleXXX方法:

handleReLaunchActivity>

performDestroyActivity>
r.activity.retainNonConfigurationInstances();//保存 viewmodel等信息传递到新Activity

handleLaunchActivity>

unscheduleGcIdler();//取消在后台接受GC的消息
WindowManagerGlobal.initialize();//WMS 初始化
performLaunchActivity>
newActivity//反射创建 activity
makeApplicationif: (mApplication != null)    return mApplication;
activity.attach>
  attachBaseContext
  new PhoneWindow()
  if (theme != 0) {
    activity.  setTheme(theme)  ;
}
  if (r.isPersistable()) {//  区分持久化,调用onCreat  
    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
    mInstrumentation.callActivityOnCreate(activity, r.state);
}

handleResumeActivity>

unscheduleGcIdler();
performResumeActivity>
activity.performResume>
  dispatchActivityPreResumed//全局callback 回调
  performStart/performRestart
  mInstrumentation.callActivity  OnResume  (this);
  mFragments.dispatchResume();
  dispatchActivityPostResumed();全局callback 回调
if (r.window == null && !a.mFinished && willBeVisible) >//不考虑 onCreate中setContrentView触发了Window绑定
r.window = r.activity.getWindow();//PhoneWindow
View decor = r.window.getDecorView();
ViewManager wm = a.getWindowManager();//WindowManagerGlobal
wm.addView(decor, l)>//完成 viewRootImpl 与DecorView关联
  Root = new ViewRootImpl()
  root.setView(view:DecorView)>
    requestLayout();//触发第一个粗略布局
Looper.myQueue().addIdleHandler(new Idler())>;//插入一条Idler消息
ac.activityIdle(a.token, a.createdConfig, stopProfiling)>//给所有没有
  ActivityClientController.activityIdle>//ActivityClientController是一个 IBinder
    activityIdleInternal>
      mHandler.removeMessages(      IDLE_TIMEOUT_MSG      , r);
      mHandler.removeMessages(      LAUNCH_TIMEOUT_MSG      );
      processStoppingAndFinishingActivities>
        if (r.isInHistory()) {
    if (        r.finishing        ) {
        // TODO(b/137329632): Wait for idle of the right activity, not just any.
        r.destroyIfPossible(reason);
    } else {
        r.stopIfPossible();>
    }
}
        ..  r.destroyImmediately(reason);
      r.stopIfPossible();>
    mAtmService.getLifecycleManager().scheduleTransaction(    StopActivityItem    );
    r.destroyImmediately(reason);>
    mAtmService.getLifecycleManager().scheduleTransaction(    DestroyActivityItem    )
    在resume之后发送idle消息,触发 stop
    同时设置Idle超时消息,如果 10s没有执行则会主动再次触发 activityIdleInternal   
    mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
        StopActivityItem.obtain(configChangeFlags));     
    mAtmService.mH.postDelayed(mStopTimeoutRunnable, STOP_TIMEOUT);//stop 超时时间 11s
    mStopTimeoutRunnable:Slog.w(TAG, "Activity stop timeout for " + ActivityRecord.this);
    notifyAppStopped();//应用停止运行
    //如何取消stopTimeOut消息 需要去handleTopActivity看

关于 IdleHandler的执行时机

Looper.myQueue().addIdleHandler(new Idler())//mIdleHandlers.add()

MessageQueue.next()>MessageQueue:链表

每次根据时间查找满足执行的message

此次时间上没有要执行的message时,执行截至目前存在的全部Idle消息

轮训下一个时间可执行的消息

handleStopActivity

performStopActivityInner>
    performPauseActivityIfNeeded //再次确认puase了没
    callActivityOnStop>
        callActivityOnSaveInstanceState // targetSdkVersion<28
        activity.performStop>
            dispatchActivityPreStopped
            mFragments.dispatchStop()
            mInstrumentation.callActivityOnStop
            dispatchActivityPostStopped
        callActivityOnSaveInstanceState(r) // targetSdkVersion>=28
if(targetSdkVersion>11){
    QueuedWork.waitToFinish()//等待完成全部finish runnable
}
pendingActions.setStopInfo(stopInfo);//这里是重点
//-----如何回传到 ATMS 去 removeStopTimeOut消息的逻辑
TransactionExecutor中对所有的 ClientTransactionItem 调用
item.execute(mTransactionHandler:ClientTransactionHandler, token, mPendingActions);
item.postExecute(mTransactionHandler, token, mPendingActions);
回到 ActivityThread.reportStop(mPendingActions)>mH.post(pendingActions.getStopInfo());
执行 PendingTransactionActions.StopInfo 的run方法 通知ATMS
ActivityClient.getInstance().activityStopped(
        mActivity.token, mState, mPersistentState, mDescription);
//binder:ActivityClientController>ActivityRecor.activityStopped>removeStopTimeOut

关于waitToFinish导致ANR的问题:在SharedPreferenceImpl中会调用 QueuedWork.addFinisher(sp 内存写入磁盘的runnable),单低于26(8.0)的SharedPreferenceImpl的实现性能较差,每次都是全部写入磁盘,有大量SP数据需要写入时,触发 finish 就会导致ANRq

handleDestroyActivity>

performDestroyActivity>
    if (getNonConfigInstance) {r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances()}
    mInstrumentation.callActivityOnDestroy(r.activity);
cleanUpPendingRemoveWindows>
    WindowManagerGlobal.closeAll()>
        ViewRootImpl.die>
            //解绑输入时间,销毁硬件渲染器,dispatchDetachedFromWindow,解绑 decorView
            WindowManagerGlobal.getInstance().doRemoveView(this);//移除ViewRootImpl

事件:

第一块:接受native事件

WindowInputEventReceiver extends InputEventReceiver
Native>
InputEventReceiver.dispatchInputEvent>WindowInputEventReceiver.onInputEvent>
ViewRootImpl.enqueueInputEvent>
ViewRootImpl.doProcessInputEvents>....>
processPointerEvent>View.dispatchPointerEvent>
DecorView.dispatchTouchEvent>
mWindow.CallBack.dispatchTouchEvent | super(ViewGroup).dispatchTouchEvent {
默认是走Activity dispatchTouchEvent,但会优先 回到View上,
如果重写了可以自己截胡或者消费完再调super,
只重写 onTouchEvent不重写dispatchTouchEvent 则在View消费后才有可能获取到事件},

activity代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) { // 这里的调用链条:IphoneWindow>mDecorView.superDispatchTouchEvent>ViewGroup.dispatchTouchEvent()
        return true;
    }
    return onTouchEvent(ev);
}
Window.Callback.dispatchTouchEvent>Activity.dispatchTouchEvent>DOWN:onUserInteraction,Activity.onTouchEvent)
ViewGroup.dispatchTouchEvent>

第二块:ViewGroup

在Down事件或已有事件消费目标时

ViewGroup.disallowIntercept:false>ViewGroup.onInterceptTouchEvent
                                              ViewGroup.disallowIntercept:true>intercepted = false

如果子view没有为父view设置DisallowIntercept,就查看父view自己是否在onInterceptTouchEvent自己拦截

第三块:ViewGroup if (!canceled && !intercepted) {}

未取消,父View也未拦截,处于初始事件,遍历childView 寻找消费目标:dispatchTransformedTouchEvent>addTouchTarget>

child.dispatchTouchEvent>realView.dispatchTouchEvent>onTouch | onTouchEvent

第四块:ViewGroup if(mFirstTouchTarget == null)

找不到消费目标丢给 父类:View.dispatchTouchEvent 处理

第五块:View

将后续事件传给上面找到的TargetView进行消费

布局:

第一块:onCreate:Activity的window 创建

ActivityThread.handleLaunchActivity>...>activit.attach>attachBaseContext,mWindow = PhoneWindow(ActivityClientRecord.mPendingRemoveWindow) 
if(mPreserveWindow) mDecorView 将被复用否则 new PhoneWindow
UserActivity.setContentView>AppCompatActivity.setContentView>:initViewTreeOwners>getWindow().getDecorView
>PhoneWindow.getDecorView>PhoneWindow.installDecor>:createDecorView,mDecor.setWindow(this);
创建DecorView完成window与 DecorView的关联。DecorView父类是FrameLayout
DecorView创建时会出实话导航栏背景色,半透明状态栏颜色,导航栏进出动画等,                                                                             AppCompatActivitysetContentView>:
getDelegate.setContentView>AppCompatDelegateImpl.setContentView>:ensureSubDecor()再次确保decorView已创建好

第二块:onResume

ActivityThread.handleResumeActivity>:WindowManager(WindowManagerGlobal).addView(decorView:window.getDecorView)>:
root = new ViewRootImpl(view.getContext(), display)
global.mViews.add(decorView)//全局保存
global.mRoots.add(root)//全局保存
root.setView(decorView..)//完成ViewRootImpl 与DecorView 关联>:
requestLayout()//进行首次布局,确保接受系统事件前有布局
mWindowSession.addToDisplayAsUser(InputChannel)
mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
        Looper.myLooper());//这里没看懂,没到如何被回调
view.assignParent(this);将ViewRootImpl 指定为DecorView的ParentView
requestLayout()>:checkThread(指判断是否为创建线程),scheduleTraversals()
scheduleTraversals()>mChoreographer.postCallback(horeographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
mTraversalRunnable.run> doTraversal> :
performMeasure
performLayout
performDraw

自定义View嵌套滑动冲突

核心是结合多个 NestedScrollingParent,NestedScrollingChild 处理好上下游滑动事件的消费逻辑

ANR

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    println("eventView:click")
    Thread.sleep(10*10000)
    return super.dispatchTouchEvent(ev)
}

上面的代码在小米K30 上 如果没有新的系统消息执行获取反馈,则不会触发 处理超时ANR

ROOM厂商会优化ANR的体验,模拟器会ANR

官方文档:

  • 输入调度超时:如果您的应用在 5 秒内未响应输入事件(例如按键或屏幕触摸)。

  • 执行服务:如果应用声明的服务无法在几秒内完成 Service.onCreate()Service.onStartCommand()/Service.onBind() 执行。

  • 未调用 Service.startForeground():如果您的应用使用 Context.startForegroundService() 在前台启动新服务,但该服务在 5 秒内未调用 startForeground()

  • intent 广播:如果 BroadcastReceiver 在设定的一段时间内没有执行完毕。如果应用有任何前台 activity,此超时期限为 5 秒。

其他实际细节:

前台广播5s,后台广播60s

诊断ANR

  • 应用在主线程上非常缓慢地执行涉及 I/O 的操作。
  • 应用在主线程上进行长时间的计算。
  • 主线程在对另一个进程进行同步 binder 调用,而后者需要很长时间才能返回。
  • 主线程处于阻塞状态,为发生在另一个线程上的长操作等待同步的块。
  • 主线程在进程中或通过 binder 调用与另一个线程之间发生死锁。主线程不只是在等待长操作执行完毕,而且处于死锁状态。

总结:

除了API调用的逻辑错误外,主要是在主线程有长时间执行的代码或锁,阻塞了后续系统or用户的操作,系统会发出ANR提示

为了帮助到大家更好的了解Android Framework框架中的知识点,这边查阅大量的素材,整理了一下的 Android Framework 核心知识点手册,里面记录了:有Handler、Binder、AMS、WMS、PMS、事件分发机制、UI绘制……等等,几乎把更Framework相关的知识点全都记录在册了

《Framework 核心知识点汇总手册》:https://qr18.cn/AQpN4J

Handler 机制实现原理部分:
1.宏观理论分析与Message源码分析
2.MessageQueue的源码分析
3.Looper的源码分析
4.handler的源码分析
5.总结

Binder 原理:
1.学习Binder前必须要了解的知识点
2.ServiceManager中的Binder机制
3.系统服务的注册过程
4.ServiceManager的启动过程
5.系统服务的获取过程
6.Java Binder的初始化
7.Java Binder中系统服务的注册过程

Zygote :

  1. Android系统的启动过程及Zygote的启动过程
  2. 应用进程的启动过程

AMS源码分析 :

  1. Activity生命周期管理
  2. onActivityResult执行过程
  3. AMS中Activity栈管理详解

深入PMS源码:

1.PMS的启动过程和执行流程
2.APK的安装和卸载源码分析
3.PMS中intent-filter的匹配架构

WMS:
1.WMS的诞生
2.WMS的重要成员和Window的添加过程
3.Window的删除过程

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/451610.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

++i与i++有什么区别?

在编程时&#xff0c;经常会用到变量的自增或自减操作&#xff0c;尤其在循环中用的最多。以自增为例&#xff0c;有两种自增方式&#xff1a;前置和后置&#xff0c;即i和i&#xff0c;它们的不同点在于i是在程序执行完毕后进行自增&#xff0c;而i是在程序开始执行前就进行自…

基于Java+Springboot+Vue+elememt社区疫情返乡管控系统设计实现

基于JavaSpringbootVueelememt社区疫情返乡管控系统设计实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系…

Python爬取电商数据:商品采集接口 商品详情数据API 商品主图接口

学习Python自动化的一个好办法就是构建一个价格追踪器。由于这项任务生成的脚本可以立即投入使用&#xff0c;所以对于初学者来说尤为方便。本文将向大家介绍如何用Python采集器建立一个可立即实现电商价格监控的可扩展价格追踪器。 价格监控的目的 价格监控的好处多多。对于…

公司招人,面了一个4年经验不会自动化的测试人,他凭什么要15K?

在深圳这家金融公司也待了几年&#xff0c;被别人面试过也面试过别人&#xff0c;大大小小的事情也见识不少&#xff0c;今天又是团面的一天&#xff0c; 一百多个人都聚集在一起&#xff0c;因为公司最近在谈项目出来面试就2个人&#xff0c;无奈又被叫到面试房间。 整个过程…

No manual entry for ls解决办法

No manual entry for ls解决办法 如果在 Linux 中运行 man ls 命令时提示“no manual entry for ls”&#xff0c;则可能是因为您的系统没有安装 man 页面或者该页面已经被删除。此外&#xff0c;也有可能是您输入的命令不是标准命令。 可以通过以下方式进行排查&#xff1a;…

Git 时间线管理

Git 时间线管理 这一部分主要讲的是 取消(undo) 变化 和在不同的时间锚点跳来跳去&#xff0c;以 command 为主。 设计到的commits有&#xff1a; checkoutrestoreresetrevert checkout checkout 的一部分作用&#xff0c;即切换分枝在 git 分支操作 中有提到过&#xff0…

ESP32-s2芯片esp32-s2-saola-1开发板 micropython的repl连接

本文只是解决通过esp32-s2-saola-1开发板 自带microUSB 作为repl与micro python通信的问题。 如果你对esp32&#xff0c;micropython不熟&#xff0c;本文不适合你。 如果你用的不是esp32-s2&#xff0c;你不需要关心。 区分两个USB&#xff1a; 1. esp32-s2原生USB&#x…

25K 入职阿里的那天,我特么哭了

悲催的经历&#xff1a; 先说一下自己的个人情况&#xff0c;计算机专业&#xff0c;17 年本科毕业&#xff0c;一毕业就进入了“腾讯”测试 岗(进去才知道是接了个腾讯外包项目&#xff0c;可是刚毕业谁知道什么外包不外包的)。 更悲催的是&#xff1a;刚入职因为家里出现一…

什么是CDN加速?CDN加速有哪些作用?

一、什么是 CDN CDN 的全称是 Content Delivery Network&#xff0c;即内容分发网络。CDN 是在现有 Internet 基础上增加一层新的网络架构&#xff0c;通过部署边缘服务器&#xff0c;采用负载均衡、内容分发、调度等功能&#xff0c;使用户可以就近访问获取所需内容&#xff…

VMware 安装 MS-DOS7.10 并配置网络

VMware 安装 MS-DOS7.10 并设置软盘共享 1. 新建虚拟机2. 开机2.1. 这几个地方都可以开机2.2. 手速慢&#xff0c;进不了BIOS的朋友可以点这里 安装 MS-DOS7.101. 先选 1 安装 MS-DOS7.10 回车2. 欢迎页面&#xff0c;客气一下而已&#xff0c;继续 Next3. 继续王婆卖瓜4. 这步…

HDCTF 2023 Pwn WriteUp

Index 前言Pwnner分析EXP: KEEP_ON分析EXP: Minions分析EXP: 后记&#xff1a; 前言 本人是菜狗&#xff0c;比赛的时候只做出来1题&#xff0c;2题有思路但是不会&#xff0c;还是太菜了。 栈迁移还是不会&#xff0c;但又都是栈迁移的题&#xff0c;真头大。得找时间好好学学…

如何在 Java 8 中使用 Streams?结合多种案例剖析学习!

Java 8 Streams 是一个非常强大的功能&#xff0c;它提供了一种简洁、优雅的方式来处理数据集合。通过使用 Streams&#xff0c;我们可以轻松地过滤、映射、排序、聚合等操作数据。本教程将介绍 Streams 的基本概念&#xff0c;以及如何在 Java 8 中使用 Streams。本教程还包括…

【计算机视觉 | 语义分割】OVSeg:分割一切后,SAM又能分辨类别了,Meta/UTAustin提出全新开放类分割模型

文章目录 一、前言二、研究背景三、论文解读3.1 动机3.2 方法3.3 结果 一、前言 前几日&#xff0c;Meta 推出了「分割一切」AI 模型 Segment Anything&#xff0c;令网友直呼 CV 不存在了&#xff1f;&#xff01; 而在另一篇被 CVPR 2023 收录的论文中&#xff0c;Meta、UT…

《计算机网络——自顶向下方法》精炼——2.2.3-2.2.5

文章目录 引言正文HTTP报文请求行首部行实体体其他方法 HTTP响应报文实体体和初始状态行首部行 cookiecookie的运行过程Web缓存条件GET方法 引言 计算机网络在这一学科中的重要性毋庸置疑&#xff0c;而黑皮书又是这一学科的教科书级经典&#xff0c;因此本书是计算机从业者的…

微服务学习之面试知识相关总结(Redis)

文章目录 前言Redis常见面试知识1 Redis与Memcache的区别2 Redis的单线程问题3 Redis的持久化方案3.1 基础知识3.2 面试话术 4 Redis的集群方式4 Redis的常用数据类型5 Redis事务机制6 Redis的Key过期策略6.1 过期删除策略6.2 内存淘汰策略6.3 面试话术&#xff1a; 7 Redis在项…

SpringBoot(7)消息处理

消息处理 消息Java处理消息的标准规范JMSAMQPMQTTkafka 案例准备整合ActiveMQ整合RabbitMQ整合RocketMQ整合Kafka 消息 对于消息的生产者与消费者的工作模式&#xff0c;还可以将消息划分成两种模式&#xff0c;同步消费与异步消息。 同步消息就是生产者发送完消息&#xff0…

SpringCloud入门实战(七)-Hystrix服务降级入门案例

&#x1f4dd; 学技术、更要掌握学习的方法&#xff0c;一起学习&#xff0c;让进步发生 &#x1f469;&#x1f3fb; 作者&#xff1a;一只IT攻城狮 。 &#x1f490;学习建议&#xff1a;1、养成习惯&#xff0c;学习java的任何一个技术&#xff0c;都可以先去官网先看看&…

广州华锐互动:AR远程协作系统为电力设备状态监测提供有力支持

电力设备是电网运行的重要组成部分&#xff0c;对电网的安全稳定运行具有至关重要的作用。在电力设备状态监测中&#xff0c;如何快速、准确地诊断和解决设备故障&#xff0c;是电力企业和电力设备维护人员需要面对的重要问题。 广州华锐互动将AR增强现实技术运用到电力设备维…

美颜sdk是什么?探索美颜sdk的技术内幕

目前&#xff0c;美颜sdk作为美颜功能的实现方式&#xff0c;已经成为了各大应用开发者的热门选择之一。那么&#xff0c;美颜sdk到底是什么&#xff1f;它的技术内幕又是怎样的呢&#xff1f;本文将会为您揭开它的神秘面纱。 一、美颜sdk简述 美颜sdk顾名思义&#xff0c;就…

瑞萨开发环境搭建

使用keil环境&#xff0c;开发瑞萨renase A4M2 下载MDK 下载MDK&#xff0c;5.37 其它版本 最好使用5.30以上 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5k3XGSK5-1682182139410)(https://secure2.wostatic.cn/static/reEunrWa2vsfrcpVZC1nbo…