【开源框架】Glide的图片加载流程

news2024/9/26 1:28:36

引入依赖

以下的所有分析都是基于此版本的Glide分析

//引入第三方库glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

分析

Glide的使用就是短短的一行代码

Glide.with(this).load("xxx").into(imageView);

//拆分成三步
RequestManager with = Glide.with(this);

RequestBuilder<Drawable> load = with.load("");

load.into(iv);
  1. 首先通过构造Glide的单例对象
  2. 调用with给每一个RequestManager绑定一个空白的Fragment来管理图片加载的生命周期
  3. 构建Request对象(真正的实现类SingleRequest)
  4. 请求之前先检测缓存
    1. 先检测活动缓存
    2. 在检测内存缓存
  5. 没有缓存就构建一个新的异步任务
  6. 检测有没有本地磁盘缓存
  7. 没有磁盘缓存,就通过网络请求,返回输入流InputStream
  8. 解析输入流InputStream进行采样压缩,最终拿到Bitmap对象
  9. 对Bitmap进行转换成Drawble
  10. 构建磁盘缓存DiskCache
  11. 构建内存缓存
  12. 最终回到ImageViewTarget显示图片

image.png

with(如何实现生命周期的管控)

调用with方法

public static RequestManager with(@NonNull FragmentActivity activity) {
	return getRetriever(activity).get(activity);
}

会来到RequestManagerRetriever类的get方法中,在这里区分主线程还是在子线程中使用with。
如果在子线程中,绑定的是app的生命周期,在主线程中会将图片的加载与当前activity的生命周期绑定。
这也是为什么不能在子线程中使用Glide的原因,生命周期的管理会失效。

public RequestManager get(@NonNull FragmentActivity activity) {
    if (Util.isOnBackgroundThread()) {
      return get(activity.getApplicationContext());
    } else {
      assertNotDestroyed(activity);
      FragmentManager fm = activity.getSupportFragmentManager();
      return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
    }
  }

最终会返回一个RequestManager对象。

load

调用load最终会返回一个RequestBuilder对象
load负责做什么?
image.png

into

into创建请求

RequestBuilder

RequestBuilder中的into方法,首先会创建一个请求,它是一个接口,真正的实现类是SingleRequest

private <Y extends Target<TranscodeType>> Y into(
      @NonNull Y target,
      @Nullable RequestListener<TranscodeType> targetListener,
      BaseRequestOptions<?> options,
      Executor callbackExecutor) {
    ······

    //1.创建一个请求
    //这里的Request是一个接口,实际构造的对象是SingleRequest实现类
    Request request = buildRequest(target, targetListener, options, callbackExecutor);

	······
	//2.
    requestManager.clear(target);
    target.setRequest(request);
	//3.继续跟踪tarck()
    requestManager.track(target, request);

    return target;
  }

RequestTracker

一个用于跟踪、取消和重新启动正在进行的、已完成的和失败的请求的类
跟踪track方法最终调用的是runRequest方法。
其中有两个列表:

  • requests:运行中的队列
  • pendingRequests:等待中的队列
public void runRequest(@NonNull Request request) {
    //1.将请求加入到运行队列中
    requests.add(request);
    //2.请求没有暂停就调用SingleRequest的begin方法
    if (!isPaused) {
      request.begin();
    //3.请求暂停了就加入到等待运行的队列中
    } else {
      request.clear();
      pendingRequests.add(request);
    }
  }

SingleRequest

SingleRequest类中begin函数是添加了synchronized,保证在多线程下的线程安全
onSizeReady方法,最终返回调用的engine.load()

@Override
  public void begin() {
    synchronized (requestLock) {
    	······
      status = Status.WAITING_FOR_SIZE;
      if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        //1.继续深入此方法
        onSizeReady(overrideWidth, overrideHeight);
      } else {
        target.getSize(this);
      }

      ······
    }
  }

Engine(活动和内存缓存)

这时已经来到了Engine类的load方法中

 public <R> LoadStatus load(
      GlideContext glideContext,
      Object model,
      Key signature,
      int width,
      int height,
      ······) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

     //1.得到一个唯一的key,用于唯一标识图片,用于缓存读取
    EngineKey key =
        keyFactory.buildKey(
            model,
            signature,
            width,
            height,
            ······
        );

     //2.将key传入,从缓存中获取图片
    EngineResource<?> memoryResource;
    synchronized (this) {
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

      if (memoryResource == null) {
        //4.如果缓存中没有则加载
        return waitForExistingOrStartNewJob(
            glideContext,
            model,
            signature,
            width,
            height,
            ······
            key,
            startTime);
      }
    }
	//3.存在图片缓存,则直接将它回调出去
    cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
    return null;
  }

详看loadFromMemory方法
在这里存在活动缓存和内存缓存两级缓存,这两级缓存都是运行时缓存,当APP进程被杀,这这两级缓存是不再存在的。
活动缓存是:直接面向用户正在被展示的图片,是一个弱引用对象,当弱引用对象被回收之后,会把它持有的图片资源放到内存缓存中

@Nullable
  private EngineResource<?> loadFromMemory(
      EngineKey key, boolean isMemoryCacheable, long startTime) {
    ······
	//1.从活动缓存中获取
    EngineResource<?> active = loadFromActiveResources(key);
    if (active != null) {
      return active;
    }
	//2.活动缓存没有,才从内存缓存中获取
    EngineResource<?> cached = loadFromCache(key);
    if (cached != null) {
      return cached;
    }
    //3.都没有则直接退出
    return null;
  }

回过头再看waitForExistingOrStartNewJob方法,这个方法是没有检测到缓存时才会被调用。
首先会再次检测磁盘缓存中是否存在,不存在则创建异步任务

private <R> LoadStatus waitForExistingOrStartNewJob(
      GlideContext glideContext,
      Object model,
      Key signature,
      int width,
      int height,
      ······
      EngineKey key,
      long startTime) {

    //1.get磁盘缓存
    EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
    if (current != null) {
      current.addCallback(cb, callbackExecutor);
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Added to existing load", startTime, key);
      }
      return new LoadStatus(cb, current);
    }
    //2.执行图片各类操作的job
    EngineJob<R> engineJob =
        engineJobFactory.build(
            key,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache);
    //3.真正需要执行的任务,最终放入到engineJob中执行
    DecodeJob<R> decodeJob =
        decodeJobFactory.build(
            glideContext,
            model,
            key,
            ······
        );

    jobs.put(key, engineJob);

    engineJob.addCallback(cb, callbackExecutor);
    //4.将decodeJob放入engineJob开始执行
    engineJob.start(decodeJob);

    if (VERBOSE_IS_LOGGABLE) {
      logWithTimeAndKey("Started new load", startTime, key);
    }
    return new LoadStatus(cb, engineJob);
  }

engineJob启动decodeJob的执行。最终通过线程池去执行decodeJob操作,可见decodeJob肯定是Runnable的实现类。

public synchronized void start(DecodeJob<R> decodeJob) {
    this.decodeJob = decodeJob;
    GlideExecutor executor =
        decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();
    executor.execute(decodeJob);
  }

DecedeJob

执行的是decodeJob中的run方法

@Override
  public void run() {
    ·······
    try {
    //1.重点关注此方法
      runWrapped();
    } catch (CallbackException e) {
      throw e;
    } catch (Throwable t) {
      ·····
    } finally {
  }

runWrapped主要是对不同任务的区分。
具体执行哪一个看此篇文章:https://www.jianshu.com/p/faeeb7bb39a3

private void runWrapped() {
    switch (runReason) {
    //1.首次初始化并获取资源
      case INITIALIZE:
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        runGenerators();
        break;
    //2.从磁盘缓存获取不到数据,重新获取
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
	//3.获取资源成功,解码数据
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }

不管是 INITIALIZE 初始化还是 SWITCH_TO_SOURCE_SERVICE 从磁盘缓存获取不到数据进行重试,都是要调用 runGenerators 来获取数据。我们先来看getNextGenerator,在首次初始化时会调用这个方法。可以看到这里由很多的XXXGenerator
DataFetcherGenerator的三个实现子类:

  • ResourceCacheGenerator:从磁盘缓存中获取原始资源处理后的Resource资源
  • DataCacheGenerator:从磁盘缓存中获取原始资源
  • SourceGenerator:从数据源(例如网络)中获取资源

具体从哪一个生成器中获取资源跟用户的使用配置有关,没有配置时默认是Source
因此上面的currentGeneratorSourceGenerator

private DataFetcherGenerator getNextGenerator() {
    switch (stage) {
      case RESOURCE_CACHE:
        return new ResourceCacheGenerator(decodeHelper, this);
      case DATA_CACHE:
        return new DataCacheGenerator(decodeHelper, this);
      case SOURCE:
        return new SourceGenerator(decodeHelper, this);
      case FINISHED:
        return null;
      default:
        throw new IllegalStateException("Unrecognized stage: " + stage);
    }
  }

继续深入会发现在runGenerators方法中会调用currentGeneratorstartNext方法,很明显是SourceGenenrator的startNext方法。

@Override
  public boolean startNext() {
    ······

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
        //1.注意getLoadData
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
              || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        startNextLoad(loadData);
      }
    }
    return started;
  }

重点关注getLoadData方法,这个方法中调用了modelLoader.buildLoadData,返回一个LoadData,它是工厂接口ModelLoader中的一个内部类。不必深究这个接口为什么设计,主要知道它是返回的它的实现子类HttpGlideUrlLoader,这时在初始化Glide是动态注册的

@Override
  public LoadData<InputStream> buildLoadData(
      @NonNull GlideUrl model, int width, int height, @NonNull Options options) {
    // GlideUrls memoize parsed URLs so caching them saves a few object instantiations and time
    // spent parsing urls.
    GlideUrl url = model;
    if (modelCache != null) {
      url = modelCache.get(model, 0, 0);
      if (url == null) {
        modelCache.put(model, 0, 0, model);
        url = model;
      }
    }
    int timeout = options.get(TIMEOUT);
    return new LoadData<>(url, new HttpUrlFetcher(url, timeout));
  }

以下截图来自Glide类中
image.png

HttpGlideUrlLoader

既然如此自然而然调用的就是HttpGlideUrlLoader类中的buildLoadData方法

@Override
  public LoadData<InputStream> buildLoadData(
      @NonNull GlideUrl model, int width, int height, @NonNull Options options) {
    ······
    int timeout = options.get(TIMEOUT);
    return new LoadData<>(url, new HttpUrlFetcher(url, timeout));
  }

可以看到最终又创建了一个HttpUrlFetcher,往这个类中深入

HttpUrlFetcher

来到HttpUrlFetcher中,可算是柳暗花明,终于看到了网络请求,最终返回的是一个InputStream对象。
而网络请求的实现原理是使用Android中的HttpURLConnection,但是从Android 4.4开始,HttpURLConnection的实现确实是通过调用okhttp完成的,而具体的方法则是通过HttpHandler这个桥梁,以及在OkHttpClient, HttpEngine中增加相应的方法来实现,当然,其实还涉及一些类的增加或删除。所以想研究网络请求的实现不妨再看以下OkHttp这个网络请求框架。

private InputStream loadDataWithRedirects(
      URL url, int redirects, URL lastUrl, Map<String, String> headers) throws IOException {
    ······

	//1.网络请求
    urlConnection = connectionFactory.build(url);
    for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
      urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
    }
    urlConnection.setConnectTimeout(timeout);
    urlConnection.setReadTimeout(timeout);
    urlConnection.setUseCaches(false);
    urlConnection.setDoInput(true);
    urlConnection.setInstanceFollowRedirects(false);
    urlConnection.connect();
    stream = urlConnection.getInputStream();
    if (isCancelled) {
      return null;
    }
    final int statusCode = urlConnection.getResponseCode();
    if (isHttpOk(statusCode)) {
      return getStreamForSuccessfulRequest(urlConnection);
    } else if (isHttpRedirect(statusCode)) {
      ······
  }

DecedeJob

拿到了请求的结果,我们并不能直接显示这张图片,还需要进行采样压缩,否则很容易出现大图OOM。
多级回调最终回到了DecedeJob类中。
在runLoadPath方法中,又继续深入到了LoadPath类中执行采样压缩(这里就不再深入分析是怎么采样压缩的了,目的很明显,最终返回的就是一张被压缩优化的Bitmap图片)

private <Data, ResourceType> Resource<R> runLoadPath(
      Data data, DataSource dataSource, LoadPath<Data, ResourceType, R> path)
      throws GlideException {
    Options options = getOptionsWithHardwareConfig(dataSource);
    DataRewinder<Data> rewinder = glideContext.getRegistry().getRewinder(data);
    try {
      // ResourceType in DecodeCallback below is required for compilation to work with gradle.
      return path.load(
          rewinder, options, width, height, new DecodeCallback<ResourceType>(dataSource));
    } finally {
      rewinder.cleanup();
    }
  }

PathLoad

采样压缩的细节不再深入,最终返回的是一张优化处理后的Bitmap位图
最终将图片一系列的回调,回到了Engine类中

Engine

回调到EngineJob中,再回调到Engine中的onEngineJobComplete,顾名思义就是到是异步请求任务已经完成

@Override
public synchronized void onEngineJobComplete(
    EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    if (resource != null && resource.isMemoryCacheable()) {
        activeResources.activate(key, resource);
    }
    jobs.removeIfCurrent(key, engineJob);
}

调用activate方法将它放入到活动缓存中,注意这是一个弱引用

synchronized void activate(Key key, EngineResource<?> resource) {
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);

    ResourceWeakReference removed = activeEngineResources.put(key, toPut);
    if (removed != null) {
      removed.reset();
    }
  }

SingleRequest

将图片保存到活动缓存中后,再由EngineJob回调到SingeRequest中的onResourceReady,意为资源已经准备好了

@GuardedBy("requestLock")
  private void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {
    ······
    try {
      ······
      if (!anyListenerHandledUpdatingTarget) {
        //1.加载动画
        Transition<? super R> animation = animationFactory.build(dataSource, isFirstResource);
        //2.返回图片和动画
        target.onResourceReady(result, animation);
      }
    } finally {
      isCallingCallbacks = false;
    }
	//3.通知加载成功
    notifyLoadSuccess();
  }

ImageViewTarget

最终回到起点,设置图片,调用的是ImageViewTarget中的setResourceInternal方法,最终调用其中的抽象方法setResource

@Override
  public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
   //1.设置图片资源
    if (transition == null || !transition.transition(resource, this)) {
      setResourceInternal(resource);
    } else {
      maybeUpdateAnimatable(resource);
    }
  }

有3个子类实现了这个方法,最终由它们去显示图片
image.png

DrawableImageViewTarget

这里的view就是一个ImageView对象,至此一张图片就显示在了屏幕上

@Override
  protected void setResource(@Nullable Drawable resource) {
    view.setImageDrawable(resource);
  }

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

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

相关文章

Dell R720服务器已有win10系统下安装Ubuntu10.04双系统

先在win10下进磁盘管理&#xff0c;分配空间 重启电脑&#xff0c;开机时按F11进入BIOS。 one-shot boot&#xff0c;选U盘 datatraveler 我原来装的是ubuntu18&#xff0c;ubuntu18升级成了ubuntu20&#xff0c;但是apt-get upgrade有很多问题&#xff0c;所以只能重装。 …

数据结构与算法之矩阵: Leetcode 134. 螺旋矩阵 (Typescript版)

螺旋矩阵 https://leetcode.cn/problems/spiral-matrix/ 描述 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5]示…

第19章 Dubbo

本文中所有的原理及流程都是针对Dubbo3.0.2.1版本 19.1 谈谈你对Dubbo的理解 难度:★★★★ 重点:★★ 白话解析 1、背景:参考18.13题,这里不在赘述。 2、简介:Dubbo在3.x版本之前都只是一个高性能的RPC框架,但是在3.x版本之后,官网的描述变了,Dubbo已经升级成一个等…

ke8学校陈老师H5

目录 例一&#xff1a; 1label for与表单元素建立关联 2鼠标选中区域 3classlist属性&#xff1a;更换类选择器。添加、删除、切换和查询一个元素上的类。 4nextElementSibling属性 5title属性&#xff1a;鼠标放上去会有提示信息 6placeholder属性&#xff1a;填了就有…

Linux常用命令——cksum命令

在线Linux命令查询工具 cksum 检查文件的CRC是否正确 补充说明 cksum命令是检查文件的CRC是否正确&#xff0c;确保文件从一个系统传输到另一个系统的过程中不被损坏。这种方法要求校验和在源系统中被计算出来&#xff0c;在目的系统中又被计算一次&#xff0c;两个数字进行…

【RuoYi-Vue-Plus】学习笔记 50 - 集成 JSEncrypt 实现请求加密传输(源码)

文章目录 前言框架版本前端服务端 框架集成前端集成1、总览2、代码实现服务端集成1、总览2、代码实现2.1、配置信息 application.yml2.2、配置类 ApiDecryptProperties2.3、过滤器 CryptoFilter2.4、包装类 DecryptRequestBodyWrapper2.5、加解密工具类 EncryptUtils2.6、自动装…

Windows环境部署流媒体服务器ZLMediaKit

参考资料 快速开始 ZLMediaKit/ZLMediaKit Wiki GitHub 环境准备 序号名称版本作用下载地址1Microsoft Visual Studio链接&#xff1a;https://pan.baidu.com/s/1DoWjNZ72Y8YpGpSTY0CNKw 提取码&#xff1a;pv6a2opensslWin32/Win64 OpenSSL Installer for Windows - Shi…

聚观早报 | vivo Y100官宣;极氪001 FR将上市

【聚观365】10月25日消息 vivo Y100官宣 一极氪001 FR将上市 特斯拉加速扩张 苹果扩大招聘力度 小米澎湃OS实现历史性跨越 vivo Y100官宣 vivo Y系列是vivo存在比较久的入门系列&#xff0c;主打千元价位的线下市场&#xff0c;在消费者中有着不错的口碑。而不久前一款型…

R语言代码示例

以下是一个使用R语言和httrOAuth库的下载器程序&#xff0c;用于下载的内容。程序使用以下代码。 # 安装和加载必要的库 install.packages("httr") install.packages("httrOAuth") library(httr) library(httrOAuth) ​ # 设置 http_proxy <- "du…

10 个最佳免费 PDF 压缩工具软件

PDF 是一种全球流行的文件格式&#xff0c;可在不损失质量或文本对齐的情况下传输文档。问题是许多文件共享应用程序和网站限制您可以共享或上传的 PDF 的大小。 10 个最佳免费 PDF 压缩工具软件 在这种情况下&#xff0c;您将需要一个可以为您减小 PDF 文件大小的应用程序。P…

Kafka磁盘写满日志清理操作

最近项目组的kafka集群&#xff0c;老是由于应用端写入kafka topic的消息太多&#xff0c;导致所在的broker节点占满&#xff0c;导致其他的组件接连宕机。 这里和应用端沟通可以删除1天之前的消息来清理磁盘&#xff0c;并且可以调整topic的消息存活时间。 一、调整Topic的消…

手写 Promise(2)实例方法与静态方法的实现

一&#xff1a;什么是 Promise Promise 是异步编程的一种解决方案&#xff0c;其实是一个构造函数&#xff0c;自己身上有all、reject、resolve这几个方法&#xff0c;原型上有then、catch等方法。 Promise对象有以下两个特点。 &#xff08;1&#xff09;对象的状态不受…

[③ADRV902x]: Digital Filter Configuration(接收端)

前言 本篇博客主要总结了ADRV9029 Rx接收端链路中各个滤波器的配置。配置不同的滤波器系数以及不同的参数&#xff0c;可以对输入的数字信号灵活得做decimation处理&#xff0c;decimation信号抽取&#xff0c;就是降低信号采样率的过程。 Receiver Signal Path 下图为接收端…

macbook2024免费mac系统优化清理软件CleanMyMac X

清理电脑的操作系统可能是我们一直以来的习惯&#xff0c;从windows系统到mac系统&#xff0c;我们一直在寻求最好的清理方法&#xff0c;能够有效地清理操作系统对于电脑来说是非常重要的。今天小编想和大家一起讨论使用在macbook上的清理软件&#xff0c;清理macbook的空间可…

在React中,什么是状态(state)?如何更新组件的状态?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

C语言程序设计——输入三个整数x,y,z,请把这三个数由小到大输出

题目&#xff1a;输入三个整数x,y,z&#xff0c;请把这三个数由小到大输出。 程序分析&#xff1a;我们想办法把最小的数放到x上&#xff0c;先将x与y进行比较&#xff0c; 如果x>y则将x与y的值进行交换&#xff0c;然后再用x与z进行比较&#xff0c;如果x>z则将x与z的值…

谈谈Net-SNMP软件

Net-SNMP是一个开源的SNMP软件套件&#xff0c;它提供了SNMP代理&#xff08;snmpd&#xff09;和SNMP工具&#xff08;如snmpget、snmpwalk等&#xff09;&#xff0c;可以用于监控和管理网络设备。 Net-SNMP最初是从UC Davis的SNMP软件衍生而来&#xff0c;现在已经成为广泛…

SpringAOP源码解析之advice构建排序(二)

上一章我们知道Spring开启AOP之后会注册AnnotationAwareAspectJAutoProxyCreator类的定义信息&#xff0c;所以在属性注入之后initializeBean的applyBeanPostProcessorsAfterInitialization方法执行的时候调用AnnotationAwareAspectJAutoProxyCreator父类(AbstractAutoProxyCre…

通过怪物展示Demo理解游戏设计模式中的迭代器模式

点击上方亿元程序员关注和★星标 引言 大家好&#xff0c;我是亿元程序员&#xff0c;一位有着8年游戏行业经验的主程。 本系列是《和8年游戏主程一起学习设计模式》&#xff0c;让糟糕的代码在潜移默化中升华&#xff0c;欢迎大家关注分享收藏订阅。 今天我们要来聊一聊游戏…