【转载】【Unity】WebSocket通信

1 前言


        Unity客户端常用的与服务器通信的方式有socket、http、webSocket。本文主要实现一个简单的WebSocket通信案例,包含客户端、服务器,实现了两端的通信以及客户端向服务器发送关闭连接请求的功能。实现上没有使用Unity相关插件,使用的就是.Net本身的WebSocket。

2 WebSocket简介


        WebSocket是一种基于TCP的应用层网络协议,客户端与服务器经过一次 HTTP 握手,两者之间便可以建立持久性的连接,进而使得客户端与服务器之间能够进行双向实时通信(全双工通信)。PS:网上有更详细的信息,这里就不展开了。

3 代码


        代码分客户端代码、服务器代码。客户端为Unity客户端,服务器是VS控制台程序。首先运行服务器代码,之后再运行客户端代码,完成连接后,在客户端输入框中输入内容,之后点击“发送信息”按钮,向服务器发送信息,点击“断开连接”按钮,向服务器发送断开连接请求,在服务器命令行窗口内输入内容按下回车即可向客户端发送信息。
        PS:先运行客户端再运行服务器也行,但客户端请求连接短时间内得不到回复便会抛出异常,手速得快。所以最好先运行服务器提前开启监听。

3.1 客户端代码


GameStart.cs

using UnityEngine;
 
public class GameStart : MonoBehaviour
{
    //发送的消息变量
    private string msg = null;
 
    void Start()
    {
        //连接服务器。
        NetManager.M_Instance.Connect("ws://127.0.0.1:8888");   //本机地址
    }
 
    //绘制UI
    private void OnGUI()
    {
        //绘制输入框,以及获取输入框中的内容
        //PS:第二参数必须是msg,否则在我们输入后,虽然msg可以获得到输入内容,但马上就被第二参数在下一帧重新覆盖。
        msg = GUI.TextField(new Rect(10, 10, 100, 20), msg);
 
        //绘制按钮,以及按下发送信息按钮,发送信息
        if (GUI.Button(new Rect(120, 10, 80, 20), "发送信息") && msg != null)
        {
            NetManager.M_Instance.Send(msg);
        }
 
        //绘制按钮,以及按下断开连接按钮,发送断开连接请求
        if (GUI.Button(new Rect(210, 10, 80, 20), "断开连接"))
        {
            Debug.Log("向服务器请求断开连接......");
            NetManager.M_Instance.CloseClientWebSocket();
        }
        
    }
}


NetManager.cs(单例,不需要挂到游戏对象上)


using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using UnityEngine;
 
public class NetManager
{
    #region 实现单例的代码
    //变量
    private volatile static NetManager m_instance;          //单例本身。使用volatile关键字修饰,禁止优化,确保多线程访问时访问到的数据都是最新的
    private static object m_locker = new object();          //线程锁。当多线程访问时,同一时刻仅允许一个线程访问
 
    //属性
    public static NetManager M_Instance
    {
        get
        {
            //线程锁。防止同时判断为null时同时创建对象
            lock (m_locker)
            {
                //如果不存在对象则创建对象
                if (m_instance == null)
                {
                    m_instance = new NetManager();
                }
            }
            return m_instance;
        }
    }
    #endregion
 
    //私有化构造
    private NetManager() { }
 
    //客户端webSocket
    private ClientWebSocket m_clientWebSocket;
    //处理接收数据的线程
    private Thread m_dataReceiveThread;
    //线程持续执行的标识符
    private bool m_isDoThread;
 
 
    /// <summary>
    /// ClientWebSocket,与服务器建立连接。
    /// </summary>
    /// <param name="uriStr"></param>
    public void Connect(string uriStr)
    {
        try
        {
            //创建ClientWebSocket
            m_clientWebSocket = new ClientWebSocket();
 
            //初始化标识符
            m_isDoThread = true;
 
            //创建线程
            m_dataReceiveThread = new Thread(ReceiveData);  //创建数据接收线程  
            m_dataReceiveThread.IsBackground = true;        //设置为后台可以运行,主线程关闭时,此线程也会关闭(实际在Unity中并没什么用,还是要手动关闭)
 
            //设置请求头部
            //m_clientWebSocket.Options.SetRequestHeader("headerName", "hearValue");
 
            //开始连接
            var task = m_clientWebSocket.ConnectAsync(new Uri(uriStr), CancellationToken.None);
            task.Wait();    //等待
 
            //启动数据接收线程
            m_dataReceiveThread.Start(m_clientWebSocket);
 
            //输出提示
            if (m_clientWebSocket.State == WebSocketState.Open)
            {
                Debug.Log("连接服务器完毕。");
            }
        }
        catch (WebSocketException ex)
        {
            Debug.LogError("连接出错:" + ex.Message);
            Debug.LogError("WebSokcet状态:" + m_clientWebSocket.State);
            //关闭连接
            //函数内可能需要考虑WebSokcet状态不是WebSocketState.Open时如何关闭连接的情况。目前没有处理这种情况。
            //比如正在连接时出现了异常,当前状态还是Connecting状态,那么该如何停止呢?
            //虽然我有了解到ClientWebSocket包含的Abort()、Dispose()方法,但并未出现过这种异常情况所以也没继续深入下去,放在这里当个参考吧。
            CloseClientWebSocket();
        }
 
    }
 
    /// <summary>
    /// 持续接收服务器的信息。
    /// </summary>
    /// <param name="socket"></param>
    private void ReceiveData(object socket)
    {
        //类型转换
        ClientWebSocket socketClient = (ClientWebSocket)socket;
        //持续接收信息
        while (m_isDoThread)
        {
            //接收数据
            string data = Receive(socketClient);
            //数据处理(可以和服务器一样使用事件(委托)来处理)
            if (data != null)
            {
                Debug.Log("接收的服务器消息:" + data);
            }
        }
        Debug.Log("接收信息线程结束。");
    }
 
    /// <summary>
    /// 接收服务器信息。
    /// </summary>
    /// <param name="socket"></param>
    /// <returns></returns>
    private string Receive(ClientWebSocket socket)
    {
        try
        {
            //接收消息时,对WebSocketState是有要求的,所以加上if判断(如果不是这两种状态,会报出异常)
            if (socket != null && (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent))
            {
                byte[] arrry = new byte[1024];  //注意长度,如果服务器发送消息过长,这也需要跟着调整
                ArraySegment<byte> buffer = new ArraySegment<byte>(arrry);  //实例化一个ArraySegment结构体
                //接收数据
                var task = socket.ReceiveAsync(buffer, CancellationToken.None);
                task.Wait();//等待
 
                //仅作状态展示。在客户端发送关闭消息后,服务器会回复确认信息,在收到确认信息后状态便是CloseReceived,这里打印输出。
                Debug.Log("socekt当前状态:" + socket.State);
 
                //如果是结束消息确认,则返回null,不再解析信息
                if (socket.State == WebSocketState.CloseReceived || task.Result.MessageType == WebSocketMessageType.Close)
                {
                    return null;
                }
                //将接收数据转为string类型,并返回。注意只解析我们接收到的字节数目(task.Result.Count)
                return Encoding.UTF8.GetString(buffer.Array, 0, task.Result.Count);
            }
            else
            {
                return null;
            }
        }
        catch (WebSocketException ex)
        {
            Debug.LogError("接收服务器信息错误:" + ex.Message);
            CloseClientWebSocket();
            return null;
        }
    }
 
    /// <summary>
    /// 发送消息
    /// </summary>
    /// <param name="content"></param>
    public void Send(string content)
    {
        try
        {
            //发送消息时,对WebSocketState是有要求的,加上if判断(如果不是这两种状态,会报出异常)
            if (m_clientWebSocket != null && (m_clientWebSocket.State == WebSocketState.Open || m_clientWebSocket.State == WebSocketState.CloseReceived))
            {
                ArraySegment<byte> array = new ArraySegment<byte>(Encoding.UTF8.GetBytes(content)); //创建内容的字节编码数组并实例化一个ArraySegment结构体
                var task = m_clientWebSocket.SendAsync(array, WebSocketMessageType.Binary, true, CancellationToken.None);  //发送
                task.Wait();  //等待
 
                Debug.Log("发送了一个消息到服务器。");
            }
        }
        catch (WebSocketException ex)
        {
            Debug.LogError("向服务器发送信息错误:" + ex.Message);
            CloseClientWebSocket();
        }
    }
 
    /// <summary>
    /// 关闭ClientWebSocket。
    /// </summary>
    public void CloseClientWebSocket()
    {
        //关闭Socket
        if (m_clientWebSocket != null && m_clientWebSocket.State == WebSocketState.Open)
        {
            var task = m_clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
            Debug.Log("如果打印过快,↓下面↓这个socket状态可能为Open,出现Open就多试几次,我们想看的是CloseSent状态。");
            Debug.Log("socekt当前状态:" + m_clientWebSocket.State);
            task.Wait();
            Debug.Log("socekt当前状态:" + m_clientWebSocket.State);
            Debug.Log("连接已断开。");
        }
        //关闭线程
        if (m_dataReceiveThread != null && m_dataReceiveThread.IsAlive)
        {
            m_isDoThread = false;   //别想Abort()了,unity中的线程关闭建议使用bool来控制线程结束。
            m_dataReceiveThread = null;
        }
    }
}


 3.2 服务器代码


Program.cs


using System;
using System.Threading.Tasks;
 
internal class Program
{
    //创建一个WebSocketService
    private static WebSocketService m_serviceSocket;
    static void Main(string[] args)
    {
        //开启后台线程,监听客户端连接
        Task.Run(() =>
        {
            m_serviceSocket = new WebSocketService();           //实例化一个WebSocketService
            m_serviceSocket.m_DataReceive += HandleDataRecive;    //监听消息事件,处理函数,当有接收到客户端消息时会调用此处理函数来处理
            m_serviceSocket.Listening();                        //开始监听
        });
 
        //持续接收键盘输入,为了能多次向客户端发消息,同时起到不关闭控制台程序的作用
        while (true)
        {
            //输入内容,发送消息到客户端
            string msg = Console.ReadLine();
            m_serviceSocket.Send(msg);
        }
    }
 
    /// <summary>
    /// 消息事件处理函数
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private static void HandleDataRecive(object sender, string e)
    {
        Console.WriteLine("接收的客户端消息:" + e);
    }
}

WebSocketService.cs 

using System;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
internal class WebSocketService
{
    HttpListener m_httpListener;                //监听者
    private WebSocket m_webSocket;              //socket
    public event EventHandler<string> m_DataReceive;  //事件(委托),消息处理函数添加到这里。
    private bool m_isDoThread;              //线程持续执行标识符
 
    public void Listening()
    {
        Console.WriteLine("正在监听...");
        //监听Ip、端口
        m_httpListener = new HttpListener();
        m_httpListener.Prefixes.Add("http://127.0.0.1:8888/");  //监听本机地址
        m_httpListener.Start();
        var httpListenContext = m_httpListener.GetContext();    //这里就等待客户端连接了。
        var webSocketContext = httpListenContext.AcceptWebSocketAsync(null);
        m_webSocket = webSocketContext.Result.WebSocket;
        //初始化标识符
        m_isDoThread = true;
        //开启后台线程,持续接收客户端消息
        Task.Run(() =>
        {
            while (m_isDoThread)
            {
                //接收消息
                string msg = Receive(m_webSocket);
                if (msg != null)
                {
                    m_DataReceive?.Invoke(m_webSocket, msg);  //数据处理
                }
            }
            Console.WriteLine("接收信息线程结束。");
        });
        Console.WriteLine("连接建立成功!");
    }
 
    /// <summary>
    /// 发送信息
    /// </summary>
    /// <param name="content">发送的内容</param>
    public void Send(string content)
    {
        //同客户端,WebSocketState要求
        if (m_webSocket != null && (m_webSocket.State == WebSocketState.Open || m_webSocket.State == WebSocketState.CloseReceived))
        {
            ArraySegment<byte> array = new ArraySegment<byte>(Encoding.UTF8.GetBytes(content)); //创建数组,并存储发送内容字节编码
            var task = m_webSocket.SendAsync(array, WebSocketMessageType.Binary, true, CancellationToken.None);  //发送   
            task.Wait();          //等待
            Console.WriteLine("发送了一个消息到客户端。");
        }
    }
 
    /// <summary>
    /// 接收信息
    /// </summary>
    /// <param name="webSocket"></param>
    /// <returns></returns>
    private string Receive(WebSocket webSocket)
    {
        //同客户端,WebSocketState要求
        if (webSocket != null && (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseSent))
        {
            //接收消息
            byte[] arrry = new byte[1024];  //大小根据情况调整
            ArraySegment<byte> buffer = new ArraySegment<byte>(arrry);
            var task = webSocket.ReceiveAsync(buffer, CancellationToken.None);
            task.Wait();
 
            Console.WriteLine("当前socket状态:" + webSocket.State);
            //当收到关闭连接的请求时(关闭确认)
            if (webSocket.State == WebSocketState.CloseReceived || task.Result.MessageType == WebSocketMessageType.Close)
            {
                webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Acknowledge Close frame", CancellationToken.None);//关闭确认
                Console.WriteLine("当前socket状态:" + webSocket.State);
                Console.WriteLine("连接已断开。");
                //关闭线程
                m_isDoThread = false;
 
                return null;
            }
            //将接收数据转为string类型,并返回。注意只解析我们接收到的字节数目(task.Result.Count)
            return Encoding.UTF8.GetString(buffer.Array, 0, task.Result.Count);
        }
        else
        {
            return null;
        }
    }
}


4 演示


通信、断开连接: 

socket状态变化截图:

5 webSocket状态变化


        这里说一下客户端在发送断开连接请求时,客户端与服务器socket的状态变化,在代码中socket状态变化时都会有打印出来。
        状态变化:客户端使用CloseAsync申请关闭,客户端socket转为CloseSent状态;服务器接收到请求后,服务器socket转为CloseReceived状态;服务器执行CloseOutputAsync确认关闭,自己转为Closed状态;客户端受到确认转为CloseReceived,经过一小段时间(非常短)转为Closed状态。

6 结束语


        开始在找Unity WebSocket通信这方面资料时,发现大多数的方案都是使用插件,插件的确很方便,用起来也比较舒服,但我个人还是倾向于使用非插件的方法,所以就研究了下。所提供的代码只是实现了简单的通信与控制,演示了相关API,在具体到项目中时肯定还要根据需要进行修改补充。

        列举目前代码中可优化的部分内容(想要真正应用到项目,那么要完善和考虑的东西非常多,这里只列举几个):

  1. 发送和关闭方法处于主线程中(异常抛出时,关闭方法在非主线程中执行,直接调用在主线程),当task.Wait()时便会阻塞主线程,当时间等待过长时就会出现卡死主线程的情况,所以可以考虑创建两个新线程来处理发送、关闭,就像接收消息线程一样,发送线程负责整个客户端的消息发送,关闭线程负责客户端断开连接请求,避免可能出现的卡死主线程的情况。
  2. 在CloseClientWebSocket()方法中还应该考虑到连接到一半时出错该如何关闭socket的处理情况,但目前并未遇到这种情况,所以也没有针对这种情况进行处理。我是在查阅资料时看到有人出现过这种错误,但后来再找那篇文章时找不到了.......所以目前就先这样吧,以后有机会再更新。更具体的内容在代码注释中有说明。
  3. 关于catch抛出的异常的问题,在代码中我catch的是WebSocketException,但到具体项目中要根据情况再修改,如服务器不监听的情况下客户端连接超时时会抛出异常,但使用WebSocketException我们就catch不到抛出的异常,使用Exception才可以catch到,那如果我们想捕获此异常并提示连接超时之类信息时就需要将WebSocketException修改为Exception了。
  4. 使用事件(委托)来处理接收到的消息时,最好单独开个线程来处理消息,接收消息的线程只负责接收并存储消息,而处理则由另一个线程负责(也可以多个线程处理),当然,也可以直接使用主线程来处理消息。这样做是为了减少接收线程的任务量,让其可以更专注于接收与存储,及时接收数据。
  5. 代码中接收数据每次都会new一个新数组,接收数据不频繁时无所谓,但如果过于频繁,最好把数组放到外面只new一个,之后每次接收之前Clear一下,以Clear替换new操作,这样性能上会好些。
  6. 断线重连。
  7. 心跳包。

————————————————
版权声明:本文为CSDN博主「EucliwoodXT」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Davidorzs/article/details/131994649

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【经典LeetCode算法题目专栏分类】【第5期】贪心算法:分发饼干、跳跃游戏、模拟行走机器人

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能AI、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 分发饼干 class Solutio…

换热站数字孪生 | 图扑智慧供热 3D 可视化

换热站作为供热系统不可或缺的一部分&#xff0c;其能源消耗对城市环保至关重要。在双碳目标下&#xff0c;供热企业可通过搭建智慧供热系统&#xff0c;实现供热方式的低碳、高效、智能化&#xff0c;从而减少碳排放和能源浪费。通过应用物联网、大数据等高新技术&#xff0c;…

MongoDB中的关系

本文主要介绍MongoDB中的关系。 目录 MongoDB的关系嵌入关系引用关系 MongoDB的关系 MongoDB是一个非关系型数据库&#xff0c;它使用了键值对的方式来存储数据。因此&#xff0c;MongoDB没有像传统关系型数据库中那样的表、行和列的概念。相反&#xff0c;MongoDB中的关系是通…

美颜SDK是什么?视频美颜SDK在直播平台中的集成与接入教程详解

当下&#xff0c;主播们追求更加自然、精致的外观&#xff0c;而观众也期待在屏幕前欣赏到更为清晰、美丽的画面。为了满足这一需求&#xff0c;美颜SDK应运而生&#xff0c;成为直播平台的重要利器之一。 一、什么是美颜SDK&#xff1f; 通过美颜SDK&#xff0c;开发者可以…

docker在线安装minio

1、下载最新minio docker pull minio/minio 2、在宿主机创建 /usr/local/data/miniodocker/config 和 /usr/local/data/miniodocker/data,执行docker命令 docker run -p 9000:9000 -p 9090:9090 --name minio -d --restartalways -e MINIO_ACCESS_KEYminio -e MINIO_SECRET_K…

数据结构--图

树具有灵活性&#xff0c;并且存在许多不同的树的应用&#xff0c;但是就树本身而言有一定的局限性&#xff0c;树只能表示层次关系&#xff0c;比如父子关系。而其他的比如兄弟关系只能够间接表示。 推广--- 图 图形结构中&#xff0c;数据元素之间的关系是任意的。 一、图…

Shell编程基础 – C语言风格的Bash for循环

Shell编程基础 – C语言风格的Bash for循环 Shell Programming Essentials - C Style For Loop in Bash By JacksonML 循环是编程语言的基本概念之一&#xff0c;同样也是Bash编程的核心。当用户需要一遍又一遍地运行一系列命令直到达到特定条件时&#xff0c;例如&#xff1…

输电线路定位:精确导航,确保电力传输安全

在现代社会中&#xff0c;电力作为生活的基石&#xff0c;其安全稳定运行至关重要。而输电线路作为电力传输的重要通道&#xff0c;其故障定位和修复显得尤为重要。恒峰智慧科技将为您介绍一种采用分布式行波测量技术的输电线路定位方法&#xff0c;以提高故障定位精度&#xf…

06-部署knative-eventing

环境要求 For prototyping purposes 单节点的Kubernetes集群&#xff0c;有2个可用的CPU核心&#xff0c;以及4g内存&#xff1b; For production purposes 单节点的Kubernetes集群&#xff0c;需要至少有6个CPU核心、6G内存和30G磁盘空间多节点的Kubernetes集群中&#xff0c;…

电影小镇智慧旅游项目技术方案:PPT全文111页,附下载

关键词&#xff1a;智慧旅游项目平台&#xff0c;智慧文旅建设&#xff0c;智慧城市建设&#xff0c;智慧文旅解决方案&#xff0c;智慧旅游技术应用&#xff0c;智慧旅游典型方案&#xff0c;智慧旅游景区方案&#xff0c;智慧旅游发展规划 一、智慧旅游的起源 智慧地球是IB…

windows 安装jenkins

下载jenkins 官方下载地址&#xff1a;Jenkins 的安装和设置 清华源下载地址&#xff1a;https://mirrors.tuna.tsinghua.edu.cn/jenkins/windows-stable/ 最新支持java8的版本时2.346.1版本&#xff0c;在清华源中找不到&#xff0c;在官网中没找到windows的下载历史&#xff…

MySQL数据库 约束

目录 约束概述 外键约束 添加外键 删除外键 删除/更新行为 约束概述 概念&#xff1a;约束是作用于表中字段上的规则&#xff0c;用于限制存储在表中的数据。 目的&#xff1a;保证数据库中数据的正确、有效性和完整性。 分类: 注意&#xff1a;约束是作用于表中字段上…

java继承

1.为什么需要继承 我们编写了两个类,一个是Puppil类(小学生),一个是Graduate(大学生),问题:两个类的属性和方法有很多是相同的,怎么办&#xff1f; 把共有的属性和方法抽离出来: 父类&#xff1a; package com.hspedu.extends01;//父类,是Pupil和Graduate的父类 public cla…

50ms时延工业相机

华睿工业相机A3504CG000 参数配置&#xff1a; 相机端到端理论时延&#xff1a;80ms 厂家同步信息&#xff0c;此款设备帧率上线23fps&#xff0c;单帧时延&#xff1a;43.48ms&#xff0c;按照一图缓存加上传输显示的话&#xff0c;厂家预估时延在&#xff1a;80ms 厂家还有…

AXure的情景交互

目录 导语&#xff1a; 1.erp多样性登录界面 2.主页跳转 3.省级联动​编辑 4. 下拉加载 导语&#xff1a; Axure是一种流行的原型设计工具&#xff0c;可以用来创建网站和应用程序的交互原型。通过Axure&#xff0c;设计师可以创建情景交互&#xff0c;以展示用户与系统的交…

机器学习之线性回归(Linear Regression)

概念 线性回归(Linear Regression)是机器学习中的一种基本的监督学习算法,用于建立输入变量(特征)与输出变量(目标)之间的线性关系。它假设输入变量与输出变量之间存在线性关系,并试图找到最佳拟合线来描述这种关系。 在简单线性回归中,只涉及两个变量:一个是自变量…

Spark编程实验二:RDD编程初级实践

目录 一、目的与要求 二、实验内容 三、实验步骤 1、pyspark交互式编程 2、编写独立应用程序实现数据去重 3、编写独立应用程序实现求平均值问题 4、三个综合实例 四、结果分析与实验体会 一、目的与要求 1、熟悉Spark的RDD基本操作及键值对操作&#xff1b; 2、熟悉使…

传统软件集成AI大模型——Function Calling

传统软件和AI大模型的胶水——Function Calling 浅谈GPT对传统软件的影响Function Calling做了什么&#xff0c;为什么选择Function CallingFunction Calling简单例子&#xff0c;如何使用使用场景 浅谈GPT对传统软件的影响 目前为止好多人对chatGPT的使用才停留在OpenAI自己提…

详细教程 - 从零开发 Vue 鸿蒙harmonyOS应用 第六节(js版) ——模块化设计实现复杂页面

随着HarmonyOS生态的日渐完善,越来越多的厂商加入鸿蒙系统应用开发的行列。然而从其他系统转到鸿蒙开发,很多开发者还是需要一个适应的过程,特别是面对比较复杂的页面,应该如何合理进行模块化拆分是一个难点。 本文将通过一个实例,来分析如果采用模块化的方式实现一个包含丰富内…

“去 Android化”为何蔚然成风?

早在2008年时&#xff0c;国内市场诞生了第一批自研手机OS&#xff0c;由于种种缘由铩羽而归&#xff0c;“优化Android ”貌似成为了本土特色。而从2023年下半年开始掀起了一股"去安卓化"的热潮&#xff0c;像华为、小米、vivo等都不约而同的站在了同一战线。 “去…
最新文章