星巴克shot什么意思DmF是什么意思

如何在unity中实现Task
VR开发教程
penguin_ku
之前写了文章介绍了如何把Task移植到unity中使用,毕竟unity不支持4.x,貌似也没打算支持。但那个Task里面的东西全是在后台线程执行的。虽然可以用类似invoke的方式(这个我有单独的封装,曾经在官网介绍过),切入到主线程进行操作,但是如果遇到我的任务就是个协程,就歇菜了。因为他可不没办法支持协程当任务。于是我就又花了两天时间捣鼓,写了个UTask的模型。这种模型,有好多人已经封装过,但大家出发点各不相同,我是纯粹的希望任务式编程。好了废话不多说了。 & & & 先来看结果吧:
public class Test : MonoBehaviour
& & // Use this for initialization
& & void Start()
& & & & var task1 = Dpjia.UnityTask.UTask.Run&int&(() =& AA()).ContinueWith&int&(t =& BB(t));
& & IEnumerator AA()
& & & & int res = 100;
& & & & Debug.Log(&AA: & & &+ res &+ & &&+ Time.time);
& & & & yield return new WaitForSeconds(2);
& & & & res += 50;
& & & & Debug.Log(&AA: & & + res + & && + Time.time);
& & IEnumerator BB(UTask&int& p_task)
& & & & int res = p_task.R
& & & & yield return new WaitForSeconds(1);
& & & & Debug.Log(&BB: & & + res + & && + Time.time);
& & & & yield return new WaitForSeconds(1);
& & & & res *= 2;
& & & & Debug.Log(&BB: & & + res + & && + Time.time);
使用这种方案,我们可以拆分我们的业务为一个个独立的任务,然后顺序去执行就行了。
&顺着之前的那两篇文章,一篇是Task,一篇是协程。这里的思路和之前的Task模式是一致的,唯一的区别就是毕竟这里是unity的主线程,不能直接一个action作为任务,那样会卡死主线程的,所以用了协程来作为任务的单位。顺其自然,任务执行中无选择余地的用了MonoBehaviour中的StartCoroutine和StopCoroutine来启动与取消任务。
/// Dispatches an asynchronous message to coroutine.///&
///public Coroutine Post(IEnumerator p_action, Action&object& p_completeCallback)
& & return g_defaultContext.StartCoroutine(Excute(p_action, p_completeCallback));
public void Stop(Coroutine p_coroutine)
& & StopCoroutine(p_coroutine);
但这里有所不同的是我做了一层封装,也就是你看到的Excute这个方法
private static IEnumerator Excute(IEnumerator p_coroutine, Action&object& p_completeCallback)
& & while (p_coroutine.MoveNext())
& & & & yield return p_coroutine.C
& & if (p_completeCallback != null)
& & & & p_completeCallback(p_coroutine.Current);
这么写看起来很诡异吧,因为这样写我可以拿到最后一个yield return的值作为一个协程的返回值。(记住协程本身是没有返回值的,他实际上是个类,他的主体就是movenext,这个在前一篇有过讲解,当然也就不可以使用ref,out来输出了。我也是想了各种办法,最后测试,发现可以用yield return来实现的。)
其实只要你熟悉了之前的Task的机制,再熟悉了这里对任务原型的选择以及任务的启动结束的使用,你也就可以实现了。因为是链式编程,所以有两行代码对你比较有价值,如果你想实现一个和我一样的东西的话。
protected List& m_continuationActions = new List&();
protected List m_continuationTasks = new List();
m_continuationActions存的是任务结束后的action,这个action就是彻底结束了,不会再返回个任务让你继续链路下去。
下一个存在的就是后续的任务,一个任务可以有多个后续任务,虽然一般情形我们是A-&b-&c-&d。但也有很多A结束后同时运行b和c两个或若干个任务。
当然有扇出,肯定有扇入。所以有WhenAll和WhenAny
/// Creates a task that is complete when all of the provided tasks are complete.
/// If any of the tasks has an exception, all exceptions raised in the tasks will
/// be aggregated into the returned task. Otherwise, if any of the tasks is cancelled,
/// the returned task will be cancelled.
///The tasks to aggregate.
/// A task that is complete when all of the provided tasks are complete.
public static UTask WhenAll(IEnumerable p_tasks)
& & var taskArr = p_tasks.ToArray();
& & if (taskArr.Length == 0)
& & & & return UTask.FromResult(0);
& & var tcs = new UTaskCompletionSource&object&();
& & UTask.Factory.ContinueWhenAll(taskArr, _ =&
& & & & var exceptions = taskArr.Where(p =& p.IsFaulted).Select(p =& p.Exception).ToArray();
& & & & if (exceptions.Length & 0)
& & & & & & tcs.SetException(new System.Threading.Tasks.AggregateException(exceptions));
& & & & else if (taskArr.Any(t =& t.IsCanceled))
& & & & & & tcs.SetCanceled();
& & & & else
& & & & & & tcs.SetResult(0);
& & return tcs.T
/// Waits for any of the provided Task objects to complete execution.
internal static UTask WhenAny(IEnumerable p_tasks)
& & var tcs = new UTaskCompletionSource();
& & foreach (var task in p_tasks)
& & & & task.ContinueWith(t =& tcs.TrySetResult(t));
& & return tcs.T
另外可能需要补充下的就是,启动任务的方法原型为
/// Creates and starts a Task
public UTask StartNew(Func p_func)
至此,如果你愿意,花费一两天时间,你也能实现你的版本。欢迎交流。
上面的是MainThread的Task,前面的那篇是Backgroud的Task。但是我们正常使用中很多场景是需要前台切后台,后台处理完了切入前台。以前我们都是通过各种锁信号来实现异步与同步的概念。如果使用Task的概念,则只要针对foregroud的task提供切入后台task的方法,backgroud的task提供切入前台的方法即可。
/// switch to backgroud processor, thread pool
public static Task ContinueToBackground(this UTask p_task, Func p_continuation)
& & return p_task.ContinueToBackground(p_continuation, CancellationToken.None);
/// switch to backgroud processor, thread pool
public static Task ContinueToBackground(this UTask p_task, Func p_continuation, CancellationToken p_cancellationToken)
& & TaskCompletionSource tcs = new TaskCompletionSource();
& & var cancellation = p_cancellationToken.Register(() =& tcs.TrySetCanceled());
& & p_task.ContinueWith(t =&
& & & & TaskScheduler.FromCurrentSynchronizationContext().Post(() =&
& & & & & & try
& & & & & & {
& & & & & & & & tcs.SetResult(p_continuation());
& & & & & & & & cancellation.Dispose();
& & & & & & }
& & & & & & catch (Exception e)
& & & & & & {
& & & & & & & & tcs.SetException(e);
& & & & & & & & cancellation.Dispose();
& & & & & & }
& & & & });
& & return tcs.T
/// switch to backgroud processor, thread pool
public static Task ContinueToBackground(this UTask p_task, Func p_continuation)
& & return p_task.ContinueToBackground(p_continuation, CancellationToken.None);
/// switch to backgroud processor, thread pool
public static Task ContinueToBackground(this UTask p_task, Func p_continuation, CancellationToken p_cancellationToken)
& & TaskCompletionSource tcs = new TaskCompletionSource();
& & var cancellation = p_cancellationToken.Register(() =& tcs.TrySetCanceled());
& & p_task.ContinueWith(t =&
& & & & TaskScheduler.FromCurrentSynchronizationContext().Post(() =&
& & & & & & try
& & & & & & {
& & & & & & & & tcs.SetResult(p_continuation(t));
& & & & & & & & cancellation.Dispose();
& & & & & & }
& & & & & & catch (Exception e)
& & & & & & {
& & & & & & & & tcs.SetException(e);
& & & & & & & & cancellation.Dispose();
& & & & & & }
& & & & });
& & return tcs.T
/// switch to backgroud processor, thread pool
public static Task ContinueToBackground(this UTask p_task, Action p_continuation, CancellationToken p_cancellationToken)
& & TaskCompletionSource&object& tcs = new TaskCompletionSource&object&();
& & var cancellation = p_cancellationToken.Register(() =& tcs.TrySetCanceled());
& & p_task.ContinueWith(t =&
& & & & TaskScheduler.FromCurrentSynchronizationContext().Post(() =&
& & & & & & try
& & & & & & {
& & & & & & & & p_continuation();
& & & & & & & & tcs.SetResult(null);
& & & & & & & & cancellation.Dispose();
& & & & & & }
& & & & & & catch (Exception e)
& & & & & & {
& & & & & & & & tcs.SetException(e);
& & & & & & & & cancellation.Dispose();
& & & & & & }
& & & & });
& & return tcs.T
/// switch to backgroud processor, thread pool
public static Task ContinueToBackground(this UTask p_task, Action p_continuation)
& & return p_task.ContinueToBackground(p_continuation, CancellationToken.None);
/// switch to backgroud processor, thread pool
public static Task ContinueToBackground(this UTask p_task, Action p_continuation, CancellationToken p_cancellationToken)
& & TaskCompletionSource&object& tcs = new TaskCompletionSource&object&();
& & var cancellation = p_cancellationToken.Register(() =& tcs.TrySetCanceled());
& & p_task.ContinueWith(t =&
& & & & TaskScheduler.FromCurrentSynchronizationContext().Post(() =&
& & & & & & try
& & & & & & {
& & & & & & & & p_continuation(t);
& & & & & & & & tcs.SetResult(null);
& & & & & & & & cancellation.Dispose();
& & & & & & }
& & & & & & catch (Exception e)
& & & & & & {
& & & & & & & & tcs.SetException(e);
& & & & & & & & cancellation.Dispose();
& & & & & & }
& & & & });
& & return tcs.T
/// switch to backgroud processor, thread pool
public static Task ContinueToBackground(this UTask p_task, Action p_continuation)
& & return p_task.ContinueToBackground(p_continuation, CancellationToken.None);
/// switch to backgroud processor, ForegroundInvoker
public static void ContinueToForeground(this Task p_task, Action p_continuation)
& & p_task.ContinueWith(t =&
& & & & Dpjia.Processor.ForegroundInvoker.Invoke(() =&
& & & & & & p_continuation();
& & & & });
/// switch to backgroud processor, ForegroundInvoker
public static void ContinueToForeground(this Task p_task, Action p_continuation)
& & p_task.ContinueWith(t =&
& & & & Dpjia.Processor.ForegroundInvoker.Invoke(() =&
& & & & & & p_continuation(t);
& & & & });
/// switch to backgroud processor, ForegroundInvoker
public static UTask ContinueToForeground(this Task p_task, Func p_continuation, CancellationToken p_cancellationToken)
& & UTaskCompletionSource tcs = new UTaskCompletionSource();
& & var cancellation = p_cancellationToken.Register(() =& tcs.TrySetCanceled());
& & tcs.Task.TaskGenerator = p_
& & tcs.Task.ReturnResult = p =&
& & & & try
& & & & & & tcs.SetResult((TResult)p);
& & & & & & cancellation.Dispose();
& & & & catch (Exception e)
& & & & & & tcs.SetException(e);
& & & & & & cancellation.Dispose();
& & p_task.ContinueWith(t =&
& & & & Dpjia.Processor.ForegroundInvoker.Invoke(() =&
& & & & & & UTaskScheduler.FromCurrentSynchronizationContext().Post(tcs.Task.TaskGenerator(), tcs.Task.ReturnResult);
& & & & });
& & return tcs.T
/// switch to backgroud processor, thread pool
public static UTask ContinueToForeground(this Task p_task, Func p_continuation)
& & return p_task.ContinueToForeground(p_continuation, CancellationToken.None);
/// switch to backgroud processor, ForegroundInvoker
public static UTask ContinueToForeground(this Task p_task, Func p_continuation, CancellationToken p_cancellationToken)
& & UTaskCompletionSource tcs = new UTaskCompletionSource();
& & var cancellation = p_cancellationToken.Register(() =& tcs.TrySetCanceled());
& & tcs.Task.TaskGenerator = () =& p_continuation(p_task);
& & tcs.Task.ReturnResult = p =&
& & & & try
& & & & & & tcs.SetResult((TResult)p);
& & & & & & cancellation.Dispose();
& & & & catch (Exception e)
& & & & & & tcs.SetException(e);
& & & & & & cancellation.Dispose();
& & p_task.ContinueWith(t =&
& & & & Dpjia.Processor.ForegroundInvoker.Invoke(() =&
& & & & & & UTaskScheduler.FromCurrentSynchronizationContext().Post(tcs.Task.TaskGenerator(), tcs.Task.ReturnResult);
& & & & });
& & return tcs.T
/// switch to backgroud processor, ForegroundInvoker
public static UTask ContinueToForeground(this Task p_task, Func p_continuation)
& & return p_task.ContinueToForeground(p_continuation, CancellationToken.None);
然后就可以方便的前台后台切换
// Use this for initialization
void Start()
& & Dpjia.Processor.ForegroundInvoker.Initialize();
& & Debug.Log(Thread.CurrentThread.ManagedThreadId);
& & UTask.Run&int&(() =& AA()).ContinueWith&int&(t =& BB(t)).ContinueToBackground(()=&
& & & & Debug.Log(Thread.CurrentThread.ManagedThreadId);
& & }).ContinueToForeground(()=& { Debug.Log(Thread.CurrentThread.ManagedThreadId); });
IEnumerator AA()
& & int res = 100;
& & Debug.Log(&AA: & & &+ res &+ & &&+ Time.time);
& & yield return new WaitForSeconds(2);
& & res += 50;
& & Debug.Log(&AA: & & + res + & && + Time.time);
IEnumerator BB(UTask&int& p_task)
& & int res = p_task.R
& & yield return new WaitForSeconds(1);
& & Debug.Log(&BB: & & + res + & && + Time.time);
& & yield return new WaitForSeconds(1);
& & res *= 2;
& & Debug.Log(&BB: & & + res + & && + Time.time);
nemehviv87v8cv.png.thumb
<font color="#-30<font color="#-30<font color="#-29<font color="#-29<font color="#-29<font color="#-16<font color="#-21<font color="#-28<font color="#-28<font color="#-22
近日,RLTY CHK工作室的联合创始人Nick Robinson发布了一篇文章,以媒体天师麦克卢汉...
VR设计师和开发者们开始着手如何让VR环境及体验更加有趣、逼真。以下是一些关于如何为room...
笔者在为《幻想装置》和《工作模拟》制作出混合现实的预告片以后,研究了更深层次的VR...
关于用EasyAR SDK 搭建AR 开发环境的教程,我已经写过很多了,不懂得朋友可以看下我之...
一篇VR实践指导文章。重点关注的是将传统UX设计流程及方法融入VR产品设计的思路,以及...【代码片-1】 C#多线程与Unity中的使用
标签: unity&&&&c#&&&&多线程&&&&对线&&&&测试&&&&
using UnityE
using System.C
using System.T
public class Test : MonoBehaviour {
Thread myT//定义的线程
bool _myBool =//控制执行线程的变量
// Use this for initialization
void Start () {
this.myThread = new Thread(new ThreadStart(myStartingMethod));
myThread.Start();
// Update is called once per frame
void Update () {
if (Input.GetKeyDown(KeyCode.X))
_myBool =//如果按X,停止线程执行
void myStartingMethod()
int i = 0;
while (_myBool)
if (i & 50000)
我要留言技术领域:
取消收藏确定要取消收藏吗?
删除图谱提示你保存在该图谱下的知识内容也会被删除,建议你先将内容移到其他图谱中。你确定要删除知识图谱及其内容吗?
删除节点提示无法删除该知识节点,因该节点下仍保存有相关知识内容!
删除节点提示你确定要删除该知识节点吗?2950人阅读
unity3d(154)
如果你想在游戏中使用多线程,你应该看看这篇文章,线程是一个相当复杂的话题,但如果你掌握了它,你就可以从容的使用多个硬件处理器或处理很难划分管理数据块.
如在场景中用A*算法进行大量的数据计算.
变形网&#26684;中操作大量的顶点.
持续的要运行上传数据到服务器.
二维码识别等图像处理.
如果同时你要处理很多事情或者与Unity的对象互动小可以用thread,否则使用coroutine.
线程是在你程序中与其他线程同时运行的进行.在多处理器的计算机上可以做到多个线程的真正的同步.更多的线程取决于有多个处理核心.
Unity编程时,总有个主线程执行你的代码,也可以创建额外的线程和主线程同时运行.
而Unity中,你仅能从主线程中访问Unity的组件,对象和Unity系统调用.任何企图访问这些项目的第二个线程都将失败并引发错误.这是一个要重视的一个限制.
所以当你写代码时,你认为一个函数开始并达到它执行的点后返回,同样你做的东西又在另外一个函数中执行,但又没有发生相应的变化.操作系统决定你代码的执行,任何时候,你的代码只能暂时”休眠”掉,然后让另外的代码开始运行,
在这个例子中,在第一个线程将A的&#20540;加载到CPU寄存器中准备&#43;1后被中断,第二个线程来读取A的&#20540;,并减去1000,这时A应该是-950.现在第一个线程重新开始,它在寄存器中的50&#43;1的结果存储于A,A变成了51,而-950已经丢掉了.
从根本上说,要在用多个线程在同时对变量或内存访问时,要采取很多预防措施来确保不会发生这样的事.
所以Unity决定从另外线程访问这些变量或者内存是无效的,只是为了避免所有系统和框架对象出现问题.
所以要确保一次只有一个线程来修改变量,这不意味着你不能用多线程工作,你可以用”排序”来解决这个问题.
C#中有lock这个关键字,以确保只有一个线程可以在特定时间内访问特定的对象.这里说对象是因为无法锁定一个类型&#20540;(value type)或原型(primitive).
int a = 50;
object guard = new object();
void ThreadOneCode()
//一些代码在这
lock(guard)
a = a &#43; 1;
//其余一些代码在这
void ThreadTwoCode()
//一些代码在这
lock(guard)
a = a - 1000;
//其余一些代码在这
所有都锁定在guard内,保证同一个时间只有一个线程通过guard访问它.你可以使用任何合适的对象.
现在你可能会有各种各样的问题,比如你要锁定的不止一件事,可能是互相嵌套的.那我们该怎么办呢?
我们这个类叫Loom,让你可以轻松在另一个线程运行代码,
这里有两个要注意的功能:
RunAsync(Action)-在另一个线程上运行的一组代码.
QueueOnMainThread(Action,[可选]float time)-运行在主线程的语句(可选延迟).
用Loom.Current访问Loom-创建一个看不见的GameObject用来处理游戏主线程的互动.
下面这个例子用Loom来更新一个网&#26684;所有的顶点乘的结果.
//缩放一个网&#26684;在第二个线程
void ScaleMesh(Mesh mesh, float scale)
//Get the vertices of a mesh
var vertices = mesh.
//运行一个Action在新的线程
Loom.RunAsync(()=&{
//遍历所有的顶点
for(var i = 0; i & vertices.L i&#43;&#43;)
//缩放顶点
vertices[i] = vertices[i] *
//在主线程上运行一些代码
//更新网&#26684;
Loom.QueueOnMainThread(()=&{
//设置顶点
mesh.vertices =
//重新计算边界
mesh.RecalculateBounds();
上面这个是个很好的例子,使用lambda函数在第二个线程上做一个没有参数,不需要返回任何内容的操作. closures都是在你自己的类和函数的参数和局部变量的访问.
你可以用 ()=&{ … } 定义一个lambda函数来在新的线程上运行函数内所有的代码.
在主线程上我们需要将修改的网&#26684;顶点更新,所以我们使用QueueOnMainThread在接下来的时间更新周期运行处理(此帧或下一帧被称为接下来的更新周期). QueueOnMainThread也需要一个Action来将更新的顶点更新到原来的网&#26684;.
如果是UnityScript,你可以这样使用Loom:
//缩放一个网&#26684;在第二个线程
function ScaleMesh(mesh : Mesh, scale : float)
//Get the vertices of a mesh
var vertices = mesh.
//运行一个Action在新的线程
Loom.RunAsync(function() {
//遍历所有的顶点
for(var i = 0; i & vertices.L i&#43;&#43;)
//缩放顶点
vertices[i] = vertices[i] *
//在主线程上运行一些代码
//更新网&#26684;
Loom.QueueOnMainThread(function() {
//设置顶点
mesh.vertices =
//重新计算边界
mesh.RecalculateBounds();
&&相关文章推荐
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:277261次
积分:3387
积分:3387
排名:第8929名
转载:311篇
评论:37条
(3)(1)(1)(2)(6)(2)(2)(10)(13)(1)(6)(2)(1)(1)(6)(14)(41)(5)(5)(2)(5)(6)(14)(9)(4)(9)(5)(13)(1)(7)(6)(16)(17)(47)(8)(6)(8)(15)(2)【风宇冲】Unity多线程
原创文章如需转载请注明:转载自
Unity多线程
有些不涉及U3D API的计算可以放在分线程里,能提高多核CPU的使用率。
0. 变量(都能指向相同的内存地址)都是共享的
1. 不是UnityEngine的API能在分线程运行
UnityEngine定义的基本结构(int,float,Struct定义的数据类型)可以在分线程计算&
如 Vector3(Struct)可以 , 但Texture2d(class,根父类为Object)不可以。
UnityEngine定义的基本类型的函数可以在分线程运行,如
int i = 99;
print (i.ToString());
Vector3 x = new Vector3(0,0,9);
x.Normalize();
类的函数不能在分线程运行
实际是get_name函数
分线程报错误:get_name
&can only be called from the main thread.
Texture2D tt = new
Texture2D(10,10);
实际会调用UnityEngine里的Internal_Create
分线程报错误:Internal_Create &can only be
called from the main thread.
其他transform.position,Texture.Apply()等等都不能在分线程里运行。
结论: 分线程可以做 基本类型的计算,
以及非Unity(包括.Net及SDK)的API&
例1:在分线程里print信息
using UnityE
using System.C
using System.T
public class Manager : MonoBehaviour
void Start () {
Thread t = new Thread(new
ThreadStart(Cal));
t.Start();
void Cal()
print ("Hello world!");
运行后在控制台里会输出’Hello world!‘
例2:基本的计算
using UnityE
using System.C
using System.T
public class Manager : MonoBehaviour
public int
void Start () {
index = 0;
Thread t = new Thread(new
ThreadStart(Cal));
t.Start();
void Update () {
print("index: "+index);
void Cal()
index = 10;
例3:计算Vector3
using UnityE
using System.C
using System.T
public class Manager : MonoBehaviour
public Vector3
void Start () {
Vector3(0,0,0);
Thread t = new Thread(new
ThreadStart(Cal));
t.Start();
void Update () {
print(vec);
void Cal()
Vector3(10,20,0);
例4 在分线程里创建并计算Vector3
using UnityE
using System.C
using System.T
public class Manager : MonoBehaviour
public Texture2D t2d;
void Start () {
Thread t = new Thread(new
ThreadStart(Cal));
t.Start();
void Cal()
Vector3 x = new
Vector3(0,0,9);
在分线程里创建并调用Vector3的函数
using UnityE
using System.C
using System.T
public class Manager : MonoBehaviour
public Texture2D t2d;
void Start () {
Thread t = new Thread(new
ThreadStart(Cal));
t.Start();
void Cal()
Vector3 x = new
Vector3(0,0,9);
x.Normalize();
输出(0.0, 0.0, 1.0)
多线程还可以用来Socket传输数据
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。> 问题详情
Unity3D是否支持写成多线程程序?如果支持的话需要注意什么?
悬赏:0&答案豆
提问人:匿名网友
发布时间:
Unity3D是否支持写成多线程程序?如果支持的话需要注意什么?
您可能感兴趣的试题
1Unity3D支持的作为脚本的语言的名称是什么?2.Net与Mono的关系是什么?3下列代码在运行中会发生什么问题?如何避免?List ls = new List(new int[] { 1, 2, 3, 4, 5 });foreach (int item in ls){Console.WriteLine(item * item);ls.Remove(item);}4下列代码在运行中会产生什么问题?string a = new string(&abc&);a = (a.ToUpper() + &123&).Substring(0, 2);
我有更好的答案
请先输入下方的验证码查看最佳答案
图形验证:
验证码提交中……
享三项特权
享三项特权
享三项特权
选择支付方式:
支付宝付款
郑重提醒:支付后,系统自动为您完成注册
请使用微信扫码支付(元)
支付后,系统自动为您完成注册
遇到问题请联系在线客服QQ:
请您不要关闭此页面,支付完成后点击支付完成按钮
遇到问题请联系在线客服QQ:
恭喜您!升级VIP会员成功
常用邮箱:
用于找回密码
确认密码:}

我要回帖

更多关于 星巴克甄选店什么意思 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信