您的位置:首页 > 移动开发 > Android开发

Android消息机制,你真的了解Handler吗?

2017-11-17 00:00 267 查看


今日快讯

近日,工业和信息化部公布了第三季度对55家手机应用商店中应用软件的技术检测结果:发现违规软件31款,涉及违规收集使用用户个人信息、恶意“吸费”、强行捆绑推广其他应用软件等问题。排名前三为:安锋网8款,百度手机助手4款,绿化软件站4款。

作者简介

明天又是周末啦,提前祝大家周末愉快,不知道大家双11的快递都收到了没有?反正我是还没收到



本篇来自承包了周五的 凶残的程序员,并非教条式的文章,而是结合一些实际情况来分析Handler机制,希望对大家有所帮助。

[b][b]凶残的程序员[/b][/b] 的博客地址:
http://blog.csdn.net/qian520ao
前言

Android 的消息机制主要是指 Handler 的运行机制,对于大家来说 Handler 已经是轻车熟路了,可是真的掌握了 Handler?本文主要通过几个问题围绕着Handler展开深入并拓展的了解。
看该篇文章可能需要掌握一定的《Activity启动过程》的理论知识。并且对
Handler 有一定的了解。
已经有经典的好文介绍Handler,所以墙裂推荐先了解以下2篇文章:

深入源码解析Handler http://blog.csdn.net/iispring/article/details/47180325
Gityuan–消息机制Handler http://gityuan.com/2015/12/26/handler-message-framework
站在巨人的肩膀上会看的更远。大家有兴趣的也可以到 Gityuan的博客 上多了解了解,全部都是干货。而且他写的东西比较权威,毕竟也是小米系统工程师的骨干成员。

Questions

Looper 死循环为什么不会导致应用卡死,会消耗大量资源吗?

主线程的消息循环机制是什么(死循环如何处理其它事务)?

ActivityThread 的动力是什么?(ActivityThread 执行 Looper 的线程是什么)

Handler 是如何能够线程切换,发送 Message 的?(线程间通讯)

子线程有哪些更新UI的方法。

子线程中 Toast,showDialog 的方法。(和子线程不能更新UI有关吗)

如何处理 Handler 使用不当导致的内存泄露?

1. Looper 死循环为什么不会导致应用卡死?

线程默认没有 Looper 的,如果需要使用 Handler 就必须为线程创建 Looper。我们经常提到的主线程,也叫UI线程,它就是 ActivityThread,ActivityThread 被创建时就会初始化 Looper,这也是在主线程中默认可以使用 Handler 的原因。

首先我们看一段代码:
new Thread(new Runnable() {
   @Override
   public void run() {
       Log.e("qdx", "step 0 ");
       Looper.prepare();
       Toast.makeText(MainActivity.this, "run on Thread",
                               Toast.LENGTH_SHORT).show();
       Log.e("qdx", "step 1 ");
       Looper.loop();
       Log.e("qdx", "step 2 ");
   }
}).start();

我们知道 Looper.loop(); 里面维护了一个死循环方法,所以按照理论,上述代码执行的应该是 step 0 –>step 1
也就是说循环在 Looper.prepare(); 与 Looper.loop(); 之间。



在子线程中,如果手动为其创建了 Looper,那么在所有的事情完成以后应该调用 quit 方法来终止消息循环,否则这个子线程就会一直处于等待(阻塞)状态,而如果退出 Looper 以后,这个线程就会立刻(执行所有方法并)终止,因此建议不需要的时候终止 Looper。

执行结果也正如我们所说,这时候如果了解了 ActivityThread,并且在 main 方法中我们会看到主线程也是通过 Looper 方式来维持一个消息循环。
public static void main(String[] args) {

   ...
   Looper.prepareMainLooper();//创建Looper和MessageQueue对象,用于处理主线程的消息

   ActivityThread thread = new ActivityThread();
   thread.attach(false);//建立Binder通道 (创建新线程)

   if (sMainThreadHandler == null) {
       sMainThreadHandler = thread.getHandler();
   }

   Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
   Looper.loop();

   //如果能执行下面方法,说明应用崩溃或者是退出了...
   throw new RuntimeException("Main thread loop unexpectedly exited");
}

那么回到我们的问题上,这个死循环会不会导致应用卡死,即使不会的话,它会慢慢的消耗越来越多的资源吗?

(摘自:Gityuan)对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder 线程也是采用死循环的方法,通过循环方式不同与
Binder 驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法 onCreate/onStart/onResume 等操作时间过长,会导致掉帧,甚至发生 ANR,looper.loop 本身不会导致应用卡死。

主线程的死循环一直运行是不是特别消耗 CPU 资源呢? 其实不然,这里就涉及到 Linux  pipe/epoll机制,简单说就是在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往
pipe 管道写端写入数据来唤醒主线程工作。这里采用的 epoll 机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

Gityuan–Handler(Native层) http://gityuan.com/2015/12/27/handler-message-native

2. 主线程的消息循环机制是什么?

事实上,会在进入死循环之前便创建了新 binder 线程,在代码 ActivityThread.main() 中:
public static void main(String[] args) {
   ....

   //创建Looper和MessageQueue对象,用于处理主线程的消息
   Looper.prepareMainLooper();

   //创建ActivityThread对象
   ActivityThread thread = new ActivityThread();  

   //建立Binder通道 (创建新线程)
   thread.attach(false);

   Looper.loop(); //消息循环运行
   throw new RuntimeException("Main thread loop unexpectedly exited");
}

Activity 的生命周期都是依靠主线程的 Looper.loop,当收到不同 Message 时则采用相应措施:一旦退出消息循环,那么你的程序也就可以退出了。
从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响UI线程的刷新速率,造成卡顿的现象。

thread.attach(false) 方法函数中便会创建一个 Binder 线程(具体是指 ApplicationThread,Binder 的服务端,用于接收系统服务 AMS 发送来的事件),该 Binder 线程通过 Handler 将 Message 发送给主线程。
比如收到 msg=H.LAUNCH_ACTIVITY,则调用 ActivityThread.handleLaunchActivity() 方法,最终会通过反射机制,创建 Activity 实例,然后再执行 Activity.onCreate() 等方法;
再比如收到 msg=H.PAUSE_ACTIVITY,则调用 ActivityThread.handlePauseActivity() 方法,最终会执行 Activity.onPause() 等方法。
主线程的消息又是哪来的呢?当然是App进程中的其他线程通过 Handler 发送给主线程

system_server进程

system_server 进程是系统进程,java framework 框架的核心载体,里面运行了大量的系统服务, 比如这里提供 ApplicationThreadProxy(简称 ATP), ActivityManagerService(简称 AMS),这个两个服务都运行在 system_server 进程的不同线程中,由于
ATP 和 AMS 都是基于 IBinder接口,都是 binder线程,binder线程 的创建与销毁都是由 binder驱动 来决定的。

App进程

App 进程则是我们常说的应用程序,主线程主要负责 Activity/Service 等组件的生命周期以及UI相关操作都运行在这个线程; 另外,每个 App进程 中至少会有两个 binder 线程 ApplicationThread(简称AT) 和 ActivityManagerProxy(简称AMP),除了图中画的线程,其中还有很多线程

Binder

Binder 用于不同进程之间通信,由一个进程的 Binder 客户端向另一个进程的服务端发送事务,比如图中 线程2 向 线程4 发送事务;而 handler 用于同一个进程中不同线程的通信,比如图中 线程4 向主线程发送消息。



结合图说说 Activity 生命周期,比如暂停 Activity,流程如下:

线程1 的 AMS 中调用 线程2 的 ATP;(由于同一个进程的线程间资源共享,可以相互直接调用,但需要注意多线程并发问题)

线程2 通过 binder 传输到App进程的 线程4;

线程4 通过 handler 消息机制,将暂停 Activity 的消息发送给主线程;

主线程在 looper.loop() 中循环遍历消息,当收到 暂停Activity 的消息时,便将消息分发给 

 ActivityThread.H.handleMessage() 方法,再经过方法的调用,最后便会调用到 Activity.onPause(),当 onPause() 处理完后,继续循环 loop 下去。

补充:

ActivityThread 的 main 方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。
从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响UI线程的刷新速率,造成卡顿的现象。

最后通过《Android开发艺术探索》的一段话总结 :

ActivityThread 通过 ApplicationThread 和 AMS 进行进程间通讯,AMS 以进程间通信的方式完成 ActivityThread 的请求后会回调 ApplicationThread 中的 Binder 方法,然后 ApplicationThread 会向 H 发送消息,H 收到消息后会将
ApplicationThread 中的逻辑切换到 ActivityThread 中去执行,即切换到主线程中去执行,这个过程就是。主线程的消息循环模型

另外,ActivityThread 实际上并非线程,不像 HandlerThread类,ActivityThread 并没有真正继承 Thread类

那么问题又来了,既然 ActivityThread 不是一个线程,那么 ActivityThread 中 Looper 绑定的是哪个 Thread,也可以说它的动力是什么?(深入源码解析Handler结尾图中所说的“动力”。)

3. ActivityThread 的动力是什么?

进程
每个app运行时前首先创建一个进程,该进程是由 Zygote fork 出来的,用于承载App上运行的各种 Activity/Service 等组件。进程对于上层应用来说是完全透明的,这也是google有意为之,让App程序都是运行在 Android Runtime。大多数情况一个App就运行在一个进程中,除非在 AndroidManifest.xml
中配置 Android:process 属性,或通过 native 代码 fork 进程。
线程
线程对应用来说非常常见,比如每次 new Thread().start 都会创建一个新的线程。该线程与App所在进程之间资源共享,从 Linux 角度来说进程与线程除了是否共享资源外,并没有本质的区别,都是一个
task_struct 结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU 采用 CFS调度算法,保证每个 task 都尽可能公平的享有 CPU 时间片。

其实承载 ActivityThread 的主线程就是由 Zygote fork 而创建的进程。

4. Handler 是如何能够线程切换

其实看完上面我们大致也清楚,线程间是共享资源的。所以 Handler 处理不同线程问题就只要注意异步情况即可。
这里再引申出 Handler 的一些小知识点。
Handler 创建的时候会采用当前线程的 Looper 来构造消息循环系统,Looper 在哪个线程创建,就跟哪个线程绑定,并且
Handler 是在他关联的 Looper 对应的线程中处理消息的。(敲黑板)
那么 Handler 内部如何获取到当前线程的 Looper 呢—–ThreadLocal。ThreadLocal 可以在不同的线程中互不干扰的存储并提供数据,通过 ThreadLocal
可以轻松获取每个线程的 Looper。当然需要注意的是 ①线程是默认没有 Looper 的,如果需要使用 Handler,就必须为线程创建 Looper。我们经常提到的主线程,也叫UI线程,它就是 ActivityThread,②ActivityThread
被创建时就会初始化 Looper,这也是在主线程中默认可以使用 Handler 的原因

系统为什么不允许在子线程中访问UI?(摘自《Android开发艺术探索》)
这是因为 Android 的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:
①首先加上锁机制会让UI访问的逻辑变得复杂
②锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
所以最简单且高效的方法就是采用单线程模型来处理UI操作。
那么问题又来了,子线程一定不能更新UI
鸿洋曾曰(yue)过爱哥:数百头母驴为何半夜惨叫?小卖部安全套为何屡遭黑手?女生宿舍内裤为何频频失窃?连环强奸母猪案,究竟是何人所为?老尼姑的门夜夜被敲,究竟是人是鬼?数百只小母狗意外身亡的背后又隐藏着什么?这一切的背后, 是人性的扭曲还是道德的沦丧?是性的爆发还是饥渴的无奈?敬请关注今晚焦点访谈:

爱哥那些不得不说的故事 http://blog.csdn.net/aigestudio/article/details/43449123
看到这里,又留下两个知识点等待下篇详解:View的绘制机制 与 Android Window内部机制。

5. 子线程有哪些更新UI的方法

主线程中定义 Handler,子线程通过 mHandler 发送消息,主线程 Handler 的 handleMessage 更新UI。

用 Activity 对象的 runOnUiThread 方法。

创建 Handler,传入 getMainLooper。

View.post(Runnable r) 。

runOnUiThread

第一种咱们就不分析了,我们来看看第二种比较常用的写法。
先重新温习一下上面说的

Looper在哪个线程创建,就跟哪个线程绑定,并且 Handler是在他关联的Looper对应的线程中处理消息的。(敲黑板)

new Thread(new Runnable() {
   @Override
   public void run() {
       runOnUiThread(new Runnable() {
           @Override
           public void run() {
               //DO UI method
           }
       });
   }
}).start();

//Activity
final Handler mHandler = new Handler();

public final void runOnUiThread(Runnable action) {
   if (Thread.currentThread() != mUiThread) {
       mHandler.post(action);//子线程(非UI线程)
   } else {
       action.run();
   }
}

进入 Activity类 里面,可以看到如果是在子线程中,通过 mHandler 发送的更新UI消息。
而这个 Handler 是在 Activity 中创建的,也就是说在主线程中创建,所以便和我们在主线程中使用 Handler 更新UI没有差别。因为这个 Looper,就是 ActivityThread 中创建的Looper(Looper.prepareMainLooper())。

创建 Handler,传入 getMainLooper

那么同理,我们在子线程中,是否也可以创建一个 Handler,并获取 MainLooper,从而在子线程中更新UI呢?
首先我们看到,在 Looper类 中有静态对象 sMainLooper,并且这个 sMainLooper 就是在 ActivityThread 中创建的 MainLooper。
private static Looper sMainLooper;  // guarded by Looper.class

public static void prepareMainLooper() {
   prepare(false);
   synchronized (Looper.class) {
       if (sMainLooper != null) {
           throw new IllegalStateException("The main Looper has already been prepared.");
       }
       sMainLooper = myLooper();
   }
}

所以不用多说,我们就可以通过这个 sMainLooper 来进行更新UI操作。
new Thread(new Runnable() {
   @Override
   public void run() {
       Log.e("qdx", "step 1 "+Thread.currentThread().getName());
       Handler handler=new Handler(getMainLooper());
       handler.post(new Runnable() {
           @Override
           public void run() {
               //Do Ui method
               Log.e("qdx", "step 2 "+Thread.currentThread().getName());
           }
       });

   }
}).start();




View.post(Runnable r)

老样子,我们点入源码
//View
/**
* <p>Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.</p>
*
* @param action The Runnable that will be executed.
*
* @return Returns true if the Runnable was successfully placed in to the
*         message queue.  Returns false on failure, usually because the
*         looper processing the message queue is exiting.
*
*/
public boolean post(Runnable action) {
   final AttachInfo attachInfo = mAttachInfo;
   if (attachInfo != null) {
       return attachInfo.mHandler.post(action); //一般情况走这里
   }

   // Postpone the runnable until we know on which thread it needs to run.
   // Assume that the runnable will be successfully placed after attach.
   getRunQueue().post(action);
   return true;
}

/**
* A Handler supplied by a view's {@link android.view.ViewRootImpl}. This
* handler can be used to pump events in the UI events queue.
*/
final Handler mHandler;

居然也是 Handler 从中作祟,根据 Handler 的注释,也可以清楚该 Handler 可以处理UI事件,也就是说它的 Looper 也是主线程的 sMainLooper。这就是说我们常用的更新UI都是通过 Handler 实现的。
另外更新UI也可以通过 AsyncTask 来实现,难道这个 AsyncTask 的线程切换也是通过 Handler 吗? 没错,也是通过 Handler…Handler实在是:



6. 子线程中Toast,showDialog 的方法

可能有些人看到这个问题,就会想: 子线程本来就不可以更新UI的啊。而且上面也说了更新UI的方法



兄台且慢,且听我把话写完
new Thread(new Runnable() {
   @Override
   public void run() {
       //崩溃无疑
       Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
   }
}).start();




看到这个崩溃日志,是否有些疑惑,因为一般如果子线程不能更新UI控件是会报如下错误的(子线程不能更新UI) 



所以子线程不能更新 Toast 的原因就和 Handler 有关了,据我们了解,每一个 Handler 都要有对应的 Looper 对象,那么,满足你。
new Thread(new Runnable() {
   @Override
   public void run() {
       Looper.prepare();
       Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
       Looper.loop();
   }
}).start();

这样便能在子线程中 Toast,不是说子线程…?老样子,我们追根到底看一下 Toast 内部执行方式。
//Toast
/**
* Show the view for the specified duration.
*/
public void show() {
   ...

   INotificationManager service = getService();//从SMgr中获取名为notification的服务
   String pkg = mContext.getOpPackageName();
   TN tn = mTN;
   tn.mNextView = mNextView;

   try {
       service.enqueueToast(pkg, tn, mDuration);//enqueue? 难不成和Handler的队列有关?
   } catch (RemoteException e) {
       // Empty
   }
}

在 show 方法中,我们看到 Toast 的 show 方法和普通UI控件不太一样,并且也是通过 Binder 进程间通讯方法执行 Toast 绘制。这其中的过程就不在多讨论了,有兴趣的可以在 NotificationManagerService类 中分析。
现在把目光放在 TN 这个类上(难道越重要的类命名就越简洁,如 H类),通过 TN类,可以了解到它是 Binder 的本地类。在 Toast 的 show 方法中,将这个 TN 对象传给NotificationManagerService 就是为了通讯!并且我们也在 TN 中发现了它的 show 方法。
private static class TN extends ITransientNotification.Stub {//Binder服务端的具体实现类

   /**
    * schedule handleShow into the right thread
    */
   @Override
   public void show(IBinder windowToken) {
       mHandler.obtainMessage(0, windowToken).sendToTarget();
   }

   final Handler mHandler = new Handler() {
       @Override
       public void handleMessage(Message msg) {
           IBinder token = (IBinder) msg.obj;
           handleShow(token);
       }
   };
}

看完上面代码,就知道子线程中 Toast 报错的原因,因为在 TN 中使用 Handler,所以需要创建 Looper 对象。
那么既然用 Handler 来发送消息,就可以在 handleMessage 中找到更新 Toast 的方法。在 handleMessage 看到由 handleShow 处理。
//Toast的TN类
public void handleShow(IBinder windowToken) {
       ...
       mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

       mParams.x = mX;
       mParams.y = mY;
       mParams.verticalMargin = mVerticalMargin;
       mParams.horizontalMargin = mHorizontalMargin;
       mParams.packageName = packageName;
       mParams.hideTimeoutMilliseconds = mDuration ==
           Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
       mParams.token = windowToken;
       if (mView.getParent() != null) {
           mWM.removeView(mView);
       }
       mWM.addView(mView, mParams);//使用WindowManager的addView方法
       trySendAccessibilityEvent();
   }
}

看到这里就可以总结一下:
Toast 本质是通过 window 显示和绘制的(操作的是 window),而主线程不能更新UI 是因为 ViewRootImpl 的 checkThread方法 在 Activity 维护的 View树 的行为。
Toast 中 TN类 使用 Handler 是为了用队列和时间控制排队显示 Toast,所以为了防止在创建 TN 时抛出异常,需要在子线程中使用 Looper.prepare(); 和 Looper.loop();(但是不建议这么做,因为它会使线程无法执行结束,导致内存泄露)
Dialog 亦是如此。同时我们又多了一个知识点要去研究:Android 中 Window 是什么,它内部有什么机制?

7.  如何处理Handler 使用不当导致的内存泄露?

首先上文在子线程中为了节目效果,使用如下方式创建 Looper
Looper.prepare();
...
Looper.loop();

实际上这是非常危险的一种做法

在子线程中,如果手动为其创建 Looper,那么在所有的事情完成以后应该调用 quit 方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出 Looper 以后,这个线程就会立刻终止,因此建议不需要的时候终止 Looper。(【 Looper.myLooper().quit(); 】)

那么,如果在 Handler 的 handleMessage方法 中(或者是 run方法)处理消息,如果这个是一个延时消息,会一直保存在主线程的消息队列里,并且会影响系统对 Activity 的回收,造成内存泄露。
具体可以参考

Handler内存泄漏分析及解决
http://www.jianshu.com/p/cb9b4b71a820
总结一下,解决Handler内存泄露主要2点

有延时消息,要在 Activity 销毁的时候移除 Messages

匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者 Activity 使用弱引用。

总结



想不到 Handler 居然可以腾出这么多浪花,与此同时感谢前辈的摸索。
另外 Handler 还有许多不为人知的秘密,等待大家探索,下面我再简单的介绍两分钟

HandlerThread

IdleHandler

HandlerThread

HandlerThread 继承 Thread,它是一种可以使用 Handler 的 Thread,它的实现也很简单,在 run方法 中也是通过 Looper.prepare() 来创建消息队列,并通过 Looper.loop() 来开启消息循环(与我们手动创建方法基本一致),这样在实际的使用中就允许在 HandlerThread
中创建 Handler 了。
由于 HandlerThread 的 run方法 是一个无限循环,因此当不需要使用的时候通过 quit 或者 quitSafely方法 来终止线程的执行。

HandlerThread 的本质也是线程,所以切记关联的 Handler 中处理消息的 handleMessage为子线程。
IdleHandler
/**
* Callback interface for discovering when a thread is going to block
* waiting for more messages.
*/
public static interface IdleHandler {
   /**
    * Called when the message queue has run out of messages and will now
    * wait for more.  Return true to keep your idle handler active, false
    * to have it removed.  This may be called if there are still messages
    * pending in the queue, but they are all scheduled to be dispatched
    * after the current time.
    */
   boolean queueIdle();
}

根据注释可以了解到,这个接口方法是在消息队列全部处理完成后或者是在阻塞的过程中等待更多的消息的时候调用的,返回值 false 表示只回调一次,true 表示可以接收多次回调。
具体使用如下代码
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
   @Override
   public boolean queueIdle() {
       return false;
   }
});

另外提供一个小技巧:在 HandlerThread 中获取 Looper 的 MessageQueue 方法之反射。因为:

Looper.myQueue() 如果在主线程调用就会使用主线程 looper

使用 handlerThread.getLooper().getQueue() 最低版本需要23

//HandlerThread中获取MessageQueue
Field field = Looper.class.getDeclaredField("mQueue");
field.setAccessible(true);
MessageQueue queue = (MessageQueue) field.get(handlerThread.getLooper())

那么 Android 的消息循环机制是通过 Handler,是否可以通过 IdleHandler 来判断 Activity 的加载和绘制情况( measure,layout,draw 等)呢?并且 IdleHandler 是否也隐藏着不为人知的特殊功能?
同时我们仍需要研究

Java GC机制和内存泄露

Android 中Window是什么,它内部有什么机制?

Android View的绘制机制

欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: