unity3d多核优化

2019-05-11 18:27:00 weixin_33929309 阅读数 97
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

Entity-Component-System

 

官方文档 

 https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/index.html

 

TODO

 

转载于:https://my.oschina.net/kkkkkkkkkkkkk/blog/3048414

2019-05-29 17:09:42 qq_33994566 阅读数 633
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

前几天做的热力图中,

            foreach (var pos in PositionList)
            {
                PosArrayAdd(posArray, pos,size);
            }
void PosArrayAdd(float[,] posArray, Position pos,INT size)
        {
            //由于放大scale倍  需要计算给周围点贡献值  距离越近 贡献值越大 本点贡献值加scale/2 最远点加1  我们设定贡献值范围为scale/2 
            //限制贡献范围 加入把贴图放在第一象限且有一个顶点在原点,则所有点不能离开第一象限 xy大于等于0且小于等于最大点坐标
            int minx = Mathf.Clamp(pos.x - scale / 2, 0, size.x + 1);
            int maxx = Mathf.Clamp(pos.x - scale / 2 + scale, 0, size.x + 1);
 
            int miny = Mathf.Clamp(pos.y - scale / 2, 0, size.y + 1);
            int maxy = Mathf.Clamp(pos.y - scale / 2 + scale, 0, size.y + 1);
 
            for (int i = minx; i < maxx; i++)
            {
                for (int j = miny; j < maxy; j++)
                {
                    float dis = Vector2.Distance(new Vector2(i, j), new Vector2(pos.x, pos.y));
                    if (dis <= scale / 2)
                    {
                        //距离小于等于scale / 2的  我们要插值计算贡献值 
                        posArray[i, j] += Mathf.Lerp(1, scale / 2, 1 - dis * 2 / scale);
                    }
                }
            }
        }

这里的计算量比较大,我们在放大100倍的情况下,假如有一千个数据,要计算一千万次。领导怀疑我的这个算法在大量数据的情况下会不会死机。测试中发现在1800个数据即1800万次计算(4个循环,每个循环里有600个数据)的时候就会有些卡,在12000万次计算的时候基本要等待4-5秒。很慢很慢。于是我想是否可以通过多线程优化这里。是的,可以!

我们将此计算方法放到自己封装的线程类中计算,这样就不会影响主线程的运算。下面是我封装的线程类:

namespace Unite
{
    public class ThreadManager
    {
        public bool IsOver = false;

        private Thread thread;

        private List<PeoplePosition.Position> PositionList = new List<PeoplePosition.Position>();

        private int scale;
        private float[,] posArray;
        private PeoplePosition.INT size;

        public ThreadManager(List<PeoplePosition.Position> PositionList,int scale, float[,] posArray, PeoplePosition.INT size)
        {
            this.PositionList = PositionList;
            this.scale = scale;
            this.posArray = posArray;
            this.size = size;
            thread = new Thread(new ThreadStart(Cal));
        }

        public float[,]  Start()
        {
            IsOver = false;
            thread.Start();
            return posArray;
        }

        public void Cal()
        {
            Debug.Log(PositionList.Count);
            foreach (var pos in PositionList)
            {
                PosArrayAdd(posArray, pos, size);
            }

            Debug.Log("线程结束");
            IsOver = true;
            thread.Abort();
            
        }


        void PosArrayAdd(float[,] posArray, PeoplePosition.Position pos, PeoplePosition.INT size)
        {
            //由于放大scale倍  需要给点周围插值打点 离得近打的多 离得远打的少  本点打scale/2个值 边缘打1个点
            int minx = Mathf.Clamp(pos.x - scale / 2, 0, size.x + 1);
            int maxx = Mathf.Clamp(pos.x - scale / 2 + scale, 0, size.x);

            int miny = Mathf.Clamp(pos.y - scale / 2, 0, size.y + 1);
            int maxy = Mathf.Clamp(pos.y - scale / 2 + scale, 0, size.y);
            for (int i = minx; i <= maxx; i++)
            {
                for (int j = miny; j <= maxy; j++)
                {
                    float dis = Vector2.Distance(new Vector2(i, j), new Vector2(pos.x, pos.y));
                    if (dis <= scale / 2)
                    {
                        posArray[i, j] += Mathf.Lerp(1, scale / 2, 1 - dis * 2 / scale);
                    }
                }
            }
        }
    }

  
}

从代码中我们可以看到我们在自己定义的类中声明了标志线程是否结束的字段IsOver。我们在协程中就可以如下调用:

  ThreadManager threadManager = new ThreadManager(PositionList, scale, posArray, size);
            threadManager.Start();

            while (!threadManager.IsOver)
            {
                yield return new WaitForEndOfFrame();
            }

            Debug.Log("运算结束");
            //TODO

运算速度超快。在计算4个6000个数据,即四个线程同时计算6000个数据要2.4s,对于主线程的UI显示也没有造成卡顿影响。而主线程算4个600个数据基本要在1.33s左右。

2017-03-31 18:34:30 wuming22222 阅读数 437
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

项目进入了中期后就需要对程序在移动设备上的表现做分析评估和针对性的优化了,首先前期做优化,很多瓶颈没表现出来,能做的东西不多,而且很多指标会凭预想,如果后期做优化又会太晚,到时候发现一些问题改起来返工量就有些太大。前一阵子花了大量时间从 cpu gpu 内存 启动时间 到发热量对项目做了一翻大规模的体检和优化,效果还是显著的。

  1. 项目情况:笔者所在项目是一个非常重度的手游,甚至开始就是瞄着端游做的,3D世界,2.5D视角,RPG,即时战斗,美术品质要求极高(模型贴图精度高 ,超过目前市场同类产品)。对于目前大多数移动设备来看,挑战不小,对手机的各种硬件都是挑战。
  2. 目标机型:偏中高端,尽量兼容低端,Android至少sumsung S3能流畅,iOS至少iphone4s流畅。
  3. 性能指标:内存占用250M以下(这样大量512的机器不会挂掉),初始包100m之内(太多运营不干,太少实在是装不下。。)

用于分析和测试性能的一些利器:
1. 首先是unity在编辑器下的statics窗口:提供了dc和顶点数这两个重要指标的查看。缺点在设备看不到,但是对于dc数和顶点数来说,设备和编辑器差不多,用它可以大体看出渲染的压力。
2. .unity自带的profiler:可以连接设备看到设备上cpu gpu mem的信息,使用的时候需要勾选development模式。有点是cpu的占用在脚本的层面看的非常仔细,哪个函数占用了太多时间一眼就能看出,基本是分析脚本效率的最佳工具,但是gpu大部分设备不支持看不到,显示的mem信息不太准确,基本上偏离实际占用的内存
3. unity的internal profiler:在playersetting上可以勾选这个选项,勾选后,连接设备,在android的adb或者mac的xcode里会每隔几秒打印出很多关键指标,这个其实非常有用,不过这个功能直到很后期才发现,详细文档见http://docs.unity3d.com/Manual/iphone-InternalProfiler.html
4. android上的adb:adb提供了一组非常强大的分离android程序的工具,
http://developer.android.com/tools/help/adb.html
而最常用的是用adb的dumpsys 指令,
https://source.android.com/devices/tech/input/dumpsys.html
最常见的包括: adb shell dumpsys meminfo appname 查看实时的内存占用,android的内存分为ps rs,我们一般看ps为准,关于ps rs这些概念可参看
http://stackoverflow.com/questions/2298208/how-to-discover-memory-usage-of-my-application-in-android
adb shell dumpsys cpuinfo appname 查看实时的cpu占用,注意这里的cpu可能过百,这是因为多核的原因
adb shell dumpsys gpuinfo appname 查看实时的gpu情况
5. android 的monitor
安装adt后,在sdk\tools\monitor.bat下面有个monitor,是我认为android看性能最好的工具之一,因为它是图形化的,而且基本集成了adb的功能,从内存到cpu到gpu,还有很有用的网络流量使用情况,它的cpu占用是c++层面的统计,看不到脚本,这需要突破那个profilor结合。
6.android上的mongkey测试:它可以模拟随机的用户输入,用来验证你的程序的强壮性吧
adb shell monkey -p -v packname 1000
随机模拟1000条用户事件
7.ios:ios上的工具则显得更加专业更加统一一些,ios就用xcode自带的instruments了
这里有个详细的文档
https://developer.apple.com/library/mac/documentation/developertools/Conceptual/InstrumentsUserGuide/Introduction/Introduction.html
看来这么多工具,其实很多是要配合使用的,做u3d开发,其实不只是学会U3D的事情,要让U3D在手机上运行的好,还需要对各个平台有较深的了解。
/———————->

利用这些工具法线了一些瓶颈,同时也采取了各种策略来提高性能,反正目标就是cpu占用降低,内存占用减少,启动快,发热小,帧率高,GPU占用少,发现的一些问题和做出的具体的一些努力列举如下:

1.使用assetbundle,实现资源分离和共享,将内存控制到200m之内,同时也可以实现资源的在线更新
2.顶点数对渲染无论是cpu还是gpu都是压力最大的贡献者,降低顶点数到8万以下,fps稳定到了30帧左右
3.只使用一盏动态光,不是用阴影,不使用光照探头
粒子系统是cpu上的大头
4.剪裁粒子系统
5.合并同时出现的粒子系统
6.自己实现轻量级的粒子系统
animator也是一个效率奇差的地方
7.把不需要跟骨骼动画和动作过渡的地方全部使用animation,控制骨骼数量在30根以下
8.animator出视野不更新
9.删除无意义的animator
10.animator的初始化很耗时(粒子上能不能尽量不用animator)
11.除主角外都不要跟骨骼运动apply root motion
12.绝对禁止掉那些不带刚体带包围盒的物体(static collider )运动
NUGI的代码效率很差,基本上runtime的时候对cpu的贡献和render不相上下
13每帧递归的计算finalalpha改为只有初始化和变动时计算
14去掉法线计算
15不要每帧计算viewsize 和windowsize
16filldrawcall时构建顶点缓存使用array.copy
17.代码剪裁:使用strip level ,使用.net2.0 subset
18.尽量减少smooth group
19.给美术定一个严格的经过科学验证的美术标准,并在U3D里面配以相应的检查工具

转载:
http://blog.csdn.net/leonwei/article/details/39233921

2019-05-11 09:43:00 weixin_34399060 阅读数 428
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

JobSystem在旧版本中只是在C++ native层,供unity内部使用,unity2018中把这个功能开放给了managed层 c#使用,
再加上burst编译器,能够在C#层更容易编写出多线程的代码。

官方指引文档    https://docs.unity3d.com/Manual/JobSystem.html

 

1.通过拷贝和只读等手段,避免竞争。

2.提出NativeContainer 来解决多线程数据交互解决方案。

 

A ParallelFor job dividing batches across cores

 

JobSystem的一些注意事项:https://docs.unity3d.com/Manual/JobSystemTroubleshooting.html

1.Do not access static data from a job

2.Flush scheduled batches

        由于发起调度是在主线程,比如在循环里面大量发起schedule 那么已经被schedule的job就要等待循环结束后才能执行,从这方面来说如果主线程有大量计算的话,反而加大了延迟 可用 JobHandle.ScheduleBatchedJobs,来立刻执行,而不用等到Complete调用才执行。

3.Don’t try to update NativeContainer contents

4.Call JobHandle.Complete to regain ownership

5.Use Schedule and Complete in the main thread

6.Use Schedule and Complete at the right time

7.Mark NativeContainer types as read-only

8.Do not allocate managed memory in jobs

 

JobSystem 提升性能示例

开启jobsystem 帧数稳定在60FPS左右(CPU使用率68%) 关闭的话帧数在20FPS(CPU使用率 13%) 左右

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Jobs;
using Unity.Jobs.LowLevel.Unsafe;
using Unity.Collections;
using Unity.Transforms;
using UnityEngine.Jobs;

public struct Job3 : IJobParallelForTransform
{
    //   public NativeArray<int> result;//ref
    //  public Job3()//NativeArray<int> result)
    //{
    //    this.result = result;
    //}
    public void Execute(int index, TransformAccess transform)
    {
        //calc operation
        {
            var rot = Quaternion.LookRotation(Vector3.forward);
            for (int i = 0; i < 256; i++)
            {
                rot = Quaternion.LookRotation(Vector3.forward);
            }
            transform.rotation = Quaternion.LookRotation(Vector3.forward);
            transform.position = Vector3.one;
        }
        //   Debug.LogError(System.Threading.Thread.CurrentThread.ManagedThreadId + "  " + index);
    }
}


public class Test1 : MonoBehaviour
{
    public List<GameObject> _list = new List<GameObject>();
    public GameObject prototype;
    TransformAccessArray result;
    void t3()
    {
        //use job system
        var j1 = new Job3();
        var handle = j1.Schedule(result);
        //wait for job done
        handle.Complete();

        //do in main thread 
        /*  foreach (var p in _list)
          {
              p.transform.position += Vector3.one;
              p.transform.rotation = Quaternion.LookRotation(Vector3.forward);
              var rot = Quaternion.LookRotation(Vector3.forward);
              for (int i = 0; i < 256; i++)
              {
                  rot = Quaternion.LookRotation(Vector3.forward);
              }
          }*/
    }

    void Start()
    {
        const int len = 1024;
        TransformAccessArray.Allocate(len, len, out result);
        for (int i = 0; i < len; i++)
        {
            _list.Add(GameObject.Instantiate<GameObject>(prototype));
            result.Add(_list[i].transform);
        }
    }

    void Update()
    {
        t3();
    }
}

 

 

转载于:https://my.oschina.net/kkkkkkkkkkkkk/blog/3048273

2016-12-14 18:41:13 UWA4D 阅读数 7976
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

原文链接:http://blog.uwa4d.com/archives/optimzation_cpu.html

性能优化是游戏项目开发过程中一个永恒的话题。玩家的需求和项目的要求永远在不停增长,同屏人数、屏幕特效和场景复杂度永远在向着“榨干”硬件的趋势逼近。所以,无论硬件设备发展到何种程度,无论研发团队有多么丰富的经验积累,性能优化永远是一个非常棘手而又无法绕开的问题。

就当前游戏而言,性能优化主要是围着CPU、GPU和内存三大方面进行。下面,我们就这三方面来说说当前移动游戏项目中存在的普遍问题和相应的解决方案。


CPU模块

就目前的Unity移动游戏而言,CPU方面的性能开销主要可归结为两大类:引擎模块性能开销和自身代码性能开销。其中,引擎模块中又可细致划分为渲染模块、动画模块、物理模块、UI模块、粒子系统、加载模块和GC调用等等。正因如此,我们在UWA测评报告中,就这些模块进行详细的性能分析,以方便大家快速定位项目的性能瓶颈,同时,根据我们的分析和建议对问题进行迅速排查和解决。

UWA Tech Doc

通过大量的性能测评数据,我们发现渲染模块、UI模块和加载模块,往往占据了游戏CPU性能开销的Top3。

一、渲染模块

渲染模块可以说是任何游戏中最为消耗CPU性能的引擎模块,因为几乎所有的游戏都离不开场景、物体和特效的渲染。对于渲染模块的优化,主要从以下两个方面入手:

(1)降低Draw Call

Draw Call是渲染模块优化方面的重中之重,一般来说,Draw Call越高,则渲染模块的CPU开销越大。究其原因,要从底层Driver和GPU的渲染流程讲起,限于篇幅我们不在这里做过多的介绍。有兴趣的朋友可以查看这里,或者自行Google相关的技术文献。

UWA Tech Doc

降低Draw Call的方法则主要是减少所渲染物体的材质种类,并通过Draw Call Batching来减少其数量。Unity文档对于Draw Call Batching的原理和注意事项有非常详细的讲解,感兴趣的朋友可以直接查看 Unity官方文档

但是,需要注意的是,游戏性能并非Draw Call越小越好。这是因为,决定渲染模块性能的除了Draw Call之外,还有用于传输渲染数据的总线带宽。当我们使用Draw Call Batching将同种材质的网格模型拼合在一起时,可能会造成同一时间需要传输的数据(Texture、VB/IB等)大大增加,以至于造成带宽“堵塞”,在资源无法及时传输过去的情况下,GPU只能等待,从而反倒降低了游戏的运行帧率。

Draw Call和总线带宽是天平的两端,我们需要做的是尽可能维持天平的平衡,任何一边过高或过低,对性能来说都是无益的。

(2)简化资源

简化资源是非常行之有效的优化手段。在大量的移动游戏中,其渲染资源其实是“过量”的,过量的网格资源、不合规的纹理资源等等。所以,我们在UWA测评报告中对资源的使用进行了详细的展示(每帧渲染的三角形面片数、网格和纹理资源的具体使用情况等),以帮助大家快速查找和完善存在问题的资源。

UWA Tech Doc

关于渲染模块在CPU方面的优化方法还有很多,比如LOD、Occlusion Culling和Culling Distance等等。我们会在后续的渲染模块技术专题中进行更为详细的讲解,敬请期待。

二、UI模块

UI模块同样也是几乎所有的游戏项目中必备的模块。一个性能优异的UI模块可以将游戏的用户体验再抬高一个档次。在目前国内的大量项目中,NGUI作为UI解决方案的占比仍然非常高。所以,UWA测评报告对NGUI的性能分析进行了极大的支持,我们会根据用户所使用的UI解决方案(UGUI或NGUI)的不同提供不同的性能分析和优化建议。

UWA Tech Doc

在NGUI的优化方面,UIPanel.LateUpdate为性能优化的重中之重,它是NGUI中CPU开销最大的函数,没有之一。UI模块制作的难点并不在于其表现上,因为UI界面的表现力是由设计师来决定的,但两套表现完全一致的UI系统,其底层的性能开销则可能千差万别。如何让UI系统使用尽可能小的CPU开销来达到设计师所设计的表现力,则足以考验每一位UI开发人员的制作功底。

目前,我们在UWA测评报告中将统计意义上CPU开销最为耗时的几个函数进行展示,并将其详细的CPU占用和堆内存分配进行统计,从而让研发团队对UI系统的性能开销进行直接地掌握,同时结合项目截图对UI模块何时存在较大开销进行直观地定位。

UWA Tech Doc

对于UIPanel.LateUpdate的优化,主要着眼于UIPanel的布局,其原则如下:

  • 尽可能将动态UI元素和静态UI元素分离到不同的UIPanel中(UI的重建以UIPanel为单位),从而尽可能将因为变动的UI元素引起的重构控制在较小的范围内;
  • 尽可能让动态UI元素按照同步性进行划分,即运动频率不同的UI元素尽可能分离放在不同的UIPanel中;
  • 控制同一个UIPanel中动态UI元素的数量,数量越多,所创建的Mesh越大,从而使得重构的开销显著增加。比如,战斗过程中的HUD运动血条可能会出现较多,此时,建议研发团队将运动血条分离成不同的UIPanel,每组UIPanel下5~10个动态UI为宜。这种做法,其本质是从概率上尽可能降低单帧中UIPanel的重建开销。

另外,限于篇幅限制,我们在此仅介绍NGUI中重要性能问题,而对于UGUI系统以及UI系统自身的Draw Call问题,我们将在后续的UI模块技术专题中进行详细的讲解,敬请期待。

三、加载模块

加载模块同样也是任何游戏项目中所不可缺少的组成成分。与之前两个模块不同的是,加载模块的性能开销比较集中,主要出现于场景切换处,且CPU占用峰值均较高。

这里,我们先来说说场景切换时,其性能开销的主要体现形式。对于目前的Unity版本而言,场景切换时的主要性能开销主要体现在两个方面,前一场景的场景卸载和下一场景的场景加载。下面,我们就具体来说说这两个方面的性能瓶颈:

(1)场景卸载
对于Unity引擎而言,场景卸载一般是由引擎自动完成的,即当我们调用类似Application.LoadLevel的API时,引擎即会开始对上一场景进行处理,其性能开销主要被以下几个部分占据:

  • Destroy
    引擎在切换场景时会收集未标识成“DontDestoryOnLoad”的GameObject及其Component,然后进行Destroy。同时,代码中的OnDestory被触发执行,这里的性能开销主要取决于OnDestroy回调函数中的代码逻辑。
  • Resources.UnloadUnusedAssets
    一般情况下,场景切换过程中,该API会被调用两次,一次为引擎在切换场景时自动调用,另一次则为用户手动调用(一般出现在场景加载后,用户调用它来确保上一场景的资源被卸载干净)。在我们测评过的大量项目中,该API的CPU开销主要集中在500ms~3000ms之间。其耗时开销主要取决于场景中Asset和Object的数量,数量越多,则耗时越慢。

UWA Tech Doc

(2)场景加载
场景加载过程的性能开销又可细分成以下几个部分:

  • 资源加载
    资源加载几乎占据了整个加载过程的90%时间以上,其加载效率主要取决于资源的加载方式(Resource.Load或AssetBundle加载)、加载量(纹理、网格、材质等资源数据的大小)和资源格式(纹理格式、音频格式等)等等。不同的加载方式、不同的资源格式,其加载效率可谓千差万别,所以我们在UWA测评报告中,特别将每种资源的具体使用情况进行展示,以帮助用户可以立刻查找到问题资源并及时进行改正。
  • Instantiate实例化
    在场景加载过程中,往往伴随着大量的Instantiate实例化操作,比如UI界面实例化、角色/怪物实例化、场景建筑实例化等等。在Instantiate实例化时,引擎底层会查看其相关的资源是否已经被加载,如果没有,则会先加载其相关资源,再进行实例化,这其实是大家遇到的大多数“Instantiate耗时问题”的根本原因,这也是为什么我们在之前的AssetBundle文章中所提倡的资源依赖关系打包并进行预加载,从而来缓解Instantiate实例化时的压力(关于AssetBundle资源的加载,则是另一个很大的Story了,我们会在以后的AssetBundle加载技术专题中进行详细的讲解)。

UWA Tech Doc

另一方面,Instantiate实例化的性能开销还体现在脚本代码的序列化上,如果脚本中需要序列化的信息很多,则Instantiate实例化时的时间亦会很长。最直接的例子就是NGUI,其代码中存在很多SerializedField标识,从而在实例化时带来了较多的代码序列化开销。因此,在大家为代码增加序列化信息时,这一点是需要大家时刻关注的。

以上是游戏项目中性能开销最大的三个模块,当然,游戏类型的不同、设计的不同,其他模块仍然会有较大的CPU占用。比如,ARPG游戏中的动画系统和物理系统,音乐休闲类游戏中的音频系统和粒子系统等。对此,我们会在后续的技术专题中进行详细的讲解,敬请期待。

四、代码效率

逻辑代码在一个较为复杂的游戏项目中往往占据较大的性能开销。这种情况在MOBA、ARPG、MMORPG等游戏类型中非常常见。

UWA Tech Doc

在项目优化过程中,我们经常会想知道,到底是哪些函数占据了大量的CPU开销。同时,绝大多数的项目中其性能开销都遵循着“二八原则”,即80%的性能开销都集中在20%的函数上。所以,我们在UWA测评报告中将项目中代码占用的CPU开销进行统计,不仅可以提供代码的总体累积CPU占用,还可以更近一步看到函数内部的性能分配,从而帮助大家更快地定位问题函数。

UWA Tech Doc
当然,我们还希望可以为大家提供更多的代码性能信息,比如函数任何一帧中更为详细的性能分配、更为准确的截图信息等等。这些都是我们目前正在努力研发的功能,并在后续版本中提供给大家进行使用。

UWA Tech Doc



Unity3D 性能优化

阅读数 48