本文转载于:http://www.cnblogs.com/net168/p/4058193.html               

前言

  Toast相信大家都不会陌生吧,如果对于Toast不甚了解,可以参考我的上一篇博客《Android:谈一谈安卓应用中的Toast情节》,里面有关于Toast基础比较详细的介绍。但是如果你想要看的是最原汁原味的Toast攻略,我非常建议你:出门右转,谷歌官网,据说是一个非常给力的地儿,一般人我还不告诉他呢。但是!如果官网的开发者指南都满足不了你的胃口的话,那你还是得准备点西瓜瓜子回来吧,搬个板凳坐前排来一起分析一下Toast的源码设计。

Toast的源代码世界

  这个故事要从哪里说起呢?话说很久很久以前,程序员菜鸟小明不小心搜索到了Toast这个java文件,顿时小明心跳加速、脸红耳赤的:“这可不是我经常用到的Toast吗?”。怀揣着程序员固有的好奇心的小明点进了这个代码文件,发现了这么一个函数

public static Toast makeText(Context context, CharSequence text, int duration) {Toast result = new Toast(context);LayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);tv.setText(text);result.mNextView = v;result.mDuration = duration;return result;}

好眼熟,貌似昨天还刚刚跟它在代码上打过招呼呢。小明顿时有一种很高大上的感觉,这就是传说中的android源代码!

小明瞄了几眼代码,马上总结出两个信息:1、android源码真简单!2、Toast显示的布局文件是transient_notification.xml!

怀揣这洋洋得意的心思,小明在源代码中开始搜索transient_notification.xml,一顿卡死,终于在快放弃的时候给出了结果。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  android:layout_width="match_parent"  android:layout_height="match_parent"  android:orientation="vertical"  android:background="?android:attr/toastFrameBackground">  <TextView  android:id="@android:id/message"  android:layout_width="wrap_content"  android:layout_height="wrap_content"  android:layout_weight="1"  android:layout_gravity="center_horizontal"  android:textAppearance="@style/TextAppearance.Toast"  android:textColor="@color/bright_foreground_dark"  android:shadowColor="#BB000000"  android:shadowRadius="2.75"  />  </LinearLayout>  

这简单的不像话了!!小明愤怒了。但是愤怒归愤怒,小明还是继续往下看了,接下来看什么呢,肯定是show()方法了。

小明边念念叨叨的:“作为一个二十一世纪的优秀攻城狮,我们需要的是一种探索源代码的情怀。。。。。。”,一边定位到了show()的代码。

public void show() {if (mNextView == null) {throw new RuntimeException("setView must have been called");}INotificationManager service = getService();String pkg = mContext.getPackageName();TN tn = mTN;tn.mNextView = mNextView;try {service.enqueueToast(pkg, tn, mDuration);} catch (RemoteException e) {// Empty}
}

  这里好像是要先获取一个服务:INotificationManager,然后调用service.enqueueToast(pkg, tn, mDuration)好像是将Toast放到一个队列里面显示吧;小明这么底气不足的理解着。这个TN是个啥子玩意呢?没见过?那就来个第一次约会咯。代码搜索出炉:

private static class TN extends ITransientNotification.Stub {  final Runnable mShow = new Runnable() {  @Override  public void run() {  handleShow();  }  };  final Runnable mHide = new Runnable() {  @Override  public void run() {  handleHide();  // Don't do this in handleHide() because it is also invoked by handleShow()  mNextView = null;  }  };  private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();  final Handler mHandler = new Handler();      int mGravity;  int mX, mY;  float mHorizontalMargin;  float mVerticalMargin;  View mView;  View mNextView;  WindowManager mWM;  TN() {  // XXX This should be changed to use a Dialog, with a Theme.Toast  // defined that sets up the layout params appropriately.  final WindowManager.LayoutParams params = mParams;  params.height = WindowManager.LayoutParams.WRAP_CONTENT;  params.width = WindowManager.LayoutParams.WRAP_CONTENT;  params.format = PixelFormat.TRANSLUCENT;  params.windowAnimations = com.android.internal.R.style.Animation_Toast;  params.type = WindowManager.LayoutParams.TYPE_TOAST;  params.setTitle("Toast");  params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON  | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE  | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;  }  /** * schedule handleShow into the right thread */  @Override  public void show() {  if (localLOGV) Log.v(TAG, "SHOW: " + this);  mHandler.post(mShow);  }  /** * schedule handleHide into the right thread */  @Override  public void hide() {  if (localLOGV) Log.v(TAG, "HIDE: " + this);  mHandler.post(mHide);  }  public void handleShow() {  if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView  + " mNextView=" + mNextView);  if (mView != mNextView) {  // remove the old view if necessary
                handleHide();  mView = mNextView;  Context context = mView.getContext().getApplicationContext();  if (context == null) {  context = mView.getContext();  }  mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);  // We can resolve the Gravity here by using the Locale for getting  // the layout direction  final Configuration config = mView.getContext().getResources().getConfiguration();  final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());  mParams.gravity = gravity;  if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {  mParams.horizontalWeight = 1.0f;  }  if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {  mParams.verticalWeight = 1.0f;  }  mParams.x = mX;  mParams.y = mY;  mParams.verticalMargin = mVerticalMargin;  mParams.horizontalMargin = mHorizontalMargin;  if (mView.getParent() != null) {  if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);  mWM.removeView(mView);  }  if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);  mWM.addView(mView, mParams);  trySendAccessibilityEvent();  }  }  private void trySendAccessibilityEvent() {  AccessibilityManager accessibilityManager =  AccessibilityManager.getInstance(mView.getContext());  if (!accessibilityManager.isEnabled()) {  return;  }  // treat toasts as notifications since they are used to  // announce a transient piece of information to the user  AccessibilityEvent event = AccessibilityEvent.obtain(  AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);  event.setClassName(getClass().getName());  event.setPackageName(mView.getContext().getPackageName());  mView.dispatchPopulateAccessibilityEvent(event);  accessibilityManager.sendAccessibilityEvent(event);  }          public void handleHide() {  if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);  if (mView != null) {  // note: checking parent() just to make sure the view has  // been added...  i have seen cases where we get here when  // the view isn't yet added, so let's try not to crash.  if (mView.getParent() != null) {  if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);  mWM.removeView(mView);  }  mView = null;  }  }  } 

乍一看,把小明给虚的,急忙找来大牛程序员帮忙讲解一下。大牛认真过了几眼,咦~其实也不是那么复杂的。这时大牛注意到了这个TN继承了ITransientNotification.Stub,这个类的形式不知道大家还熟悉吗?连小明好像在博客园里面介绍AIDL的文章时懵懵懂懂看到过这种形式的类,可是没等小明反应过来,大牛顺手就在源代码中搜索了一下:ITransientNotification

“果断是AIDL!!”小明惊叹。果然大神跟菜鸟就是不一样,大牛这时打开ITransientNotification瞄一瞄,发现了show()和hide()这两个方法。

package android.app;  /** @hide */
oneway interface ITransientNotification {  void show();  void hide();
} 

“那么应该回去TN看看他的实现了”,大牛跟小明说。

@Override
public void show() {  if (localLOGV) Log.v(TAG, "SHOW: " + this);  mHandler.post(mShow);
}  @Override
public void hide() {  if (localLOGV) Log.v(TAG, "HIDE: " + this);  mHandler.post(mHide);
}  

原来是使用handler机制,分别post一个nShow和一个mHide,再接再厉,追踪源码

final Runnable mShow = new Runnable() {  @Override  public void run() {  handleShow();  }
};  final Runnable mHide = new Runnable() {  @Override  public void run() {  handleHide();  mNextView = null;  }
};

小明这次学聪明了,毕竟跟大牛学习比小明整天啃得那些《七天精通Android编程》之类的坑爹书靠谱多了,所以小明跟大牛说,我们应该看看handleShow()的实现,正解!

public void handleShow() {  if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView  + " mNextView=" + mNextView);  if (mView != mNextView) {  // remove the old view if necessary  handleHide();  mView = mNextView;  Context context = mView.getContext().getApplicationContext();  if (context == null) {  context = mView.getContext();  }  mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);  // We can resolve the Gravity here by using the Locale for getting  // the layout direction  final Configuration config = mView.getContext().getResources().getConfiguration();  final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());  mParams.gravity = gravity;  if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {  mParams.horizontalWeight = 1.0f;  }  if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {  mParams.verticalWeight = 1.0f;  }  mParams.x = mX;  mParams.y = mY;  mParams.verticalMargin = mVerticalMargin;  mParams.horizontalMargin = mHorizontalMargin;  if (mView.getParent() != null) {  if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);  mWM.removeView(mView);  }  if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);  mWM.addView(mView, mParams);  trySendAccessibilityEvent();  }
} 

原来是Toast的视图是通过WindowManager的addView来加载的,小明突然感觉自己向高级程序员迈进了一大步-----“怎么说哥现在也是了解实现原理的人了!”

他们接下来又把邪恶的目光定位在TN()这个构造方法上面

TN() {  final WindowManager.LayoutParams params = mParams;  params.height = WindowManager.LayoutParams.WRAP_CONTENT;  params.width = WindowManager.LayoutParams.WRAP_CONTENT;  params.format = PixelFormat.TRANSLUCENT;  params.windowAnimations = com.android.internal.R.style.Animation_Toast;  params.type = WindowManager.LayoutParams.TYPE_TOAST;  params.setTitle("Toast");  params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON  | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE  | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
}  

这就是设置Toast中的View的各种位置参数params。

但是小明还是有点不明白,大牛看到小明神游的样子,就给他解释道:

  其实Toast的原理是这样的,先通过makeText()实例化出一个Toast,然后调用toast.Show()方法,这时并不会马上显示Toast,而是会实例化一个TN变量,然后通过service.enqueueToast()将其加到服务队列里面去等待显示。在TN中进行调控Toast的显示格式以及里面的hide()、show()方法来控制Toast的出现以及消失,强调一下的是这个队列是系统维护的,我们并不能干涉。

小明若有所思的点点头。。。。。。

 自由控制Toast的显示时间

  时间就像水,干着干着就干了,撸着撸着就没了,吸着吸着就瘪了。两三天又过去了,突然有一天头儿给小明吩咐了一个活:给应用设置一个较长时间的Toast。这还不简单,小明偷偷在工位上打着瞌睡揉揉眼睛,Toast.setDuration()不就解决了嘛~要几秒就设几秒咯,这还是事儿?但是,谷歌又一次坑了他:因为小明不管怎么设置,Toast只能有显示2s和3.5s这两个情况,这时为啥呢?小明突然想起前些天翻了翻Toast的源码,赶紧去里面找答案

private void scheduleTimeoutLocked(ToastRecord r)  {  mHandler.removeCallbacksAndMessages(r);  Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);  long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;  mHandler.sendMessageDelayed(m, delay);
}
private static final int LONG_DELAY = 3500; // 3.5 seconds
private static final int SHORT_DELAY = 2000; // 2 seconds  

  我们呢看到这里是使用了handler中的延迟发信息来显示toast的,这里我们也看到了,延迟时间是duration,但是只有两个值:2s和3.5s这两个值,所以我们在之前说过我们设置toast的显示时间是没有任何效果的,所以小明又得去请教大牛了,果然活都不会是那么简单的。。。。。。。

大牛早有研究,他分析道:你还记得我们前些天分析的Toast源代码吗?Toast的显示是首先借助TN类,所有的显示逻辑在这个类中的show方法中,然后再实例一个TN类变量,将传递到一个队列中进行显示,所以我们要向解决这个显示的时间问题,那就从入队列这部给截断,说白了就两点:

1、不让Toast进入队列

2、调用TN类中的hide和show的方法自己控制Toast

但是第一点好实现,第二点让人抓狂了,因为我们看到TN这个类是私有的,所以我们也不能实例化他的对象,但是toast类中有一个实例化对象:tn

final TN mTN;  

竟然是包访问权限,大牛一脸淫笑的说,咱们得借助无比强大的反射技术,我们只需要反射出这个变量,然后强暴她一次即可,得到这个变量我们可以得到这个TN类对象了,然后再使用反射获取他的show和hide方法即可,代码如下:

方法一:

public class ToastReflect {private Toast mToast;private Field field;private Object obj;private Method showMethod, hideMethod;private double time;private ToastReflect(Context context, String text, double time){this.time = time;mToast = Toast.makeText(context, text, Toast.LENGTH_LONG);reflectionTN();}private void reflectionTN() {try{field = mToast.getClass().getDeclaredField("mTN");field.setAccessible(true);obj = field.get(mToast);showMethod = obj.getClass().getDeclaredMethod("show", null);hideMethod = obj.getClass().getDeclaredMethod("hide", null);}catch(Exception e){e.printStackTrace();}}public static ToastReflect makeText(Context context, String text, double time){ToastReflect toastReflect = new ToastReflect(context, text, time);return toastReflect;}private void showToast(){try{showMethod.invoke(obj, null);}catch(Exception e){e.printStackTrace();}}private void hideToast(){try{hideMethod.invoke(obj, null);}catch(Exception e){e.printStackTrace();}}public void show(){showToast();new Timer().schedule(new TimerTask() {@Overridepublic void run() {hideToast();}}, (long)(time * 1000));}
}

ps:利用反射来控制Toast的显示时间在高版本会有bug,Android 2.2实测实可以用的,Android 4.0则无法使用。具体原因大牛还在分析。。。。。。

方法二:

  但是作为一个通用性软件,对于任何版本都需要支持,所以小明还是只能采取其他办法,说实话,还真发现了一个比较傻瓜的实现。

就是可以利用handler.post结合timer来实现效果,兼容性较好。。利用定时重复show一个Toast就能达到根据特定时间来显示的功能。

public class ToastSimple {private double time;private static Handler handler;private Timer showTimer;private Timer cancelTimer;private Toast toast;private ToastSimple(){showTimer = new Timer();cancelTimer = new Timer();}public void setTime(double time) {this.time = time;}public void setToast(Toast toast){this.toast = toast;}public static ToastSimple makeText(Context context, String text, double time){ToastSimple toast1= new ToastSimple();toast1.setTime(time);toast1.setToast(Toast.makeText(context, text, Toast.LENGTH_SHORT));handler = new Handler(context.getMainLooper());return toast1;}public void show(){toast.show();if(time > 2){showTimer.schedule(new TimerTask() {@Overridepublic void run() {handler.post(new ShowRunnable());}}, 0, 1900);}cancelTimer.schedule(new TimerTask() {@Overridepublic void run() {handler.post(new CancelRunnable());}}, (long)(time * 1000));}private class CancelRunnable implements Runnable{@Overridepublic void run() {showTimer.cancel();toast.cancel();}}private class ShowRunnable implements Runnable{@Overridepublic void run() {toast.show();}}
}

方法三:  

这时,大牛也琢磨出一个办法,因为Toast是基于windowManager来显示的,所以完全可以自己写一个自定义的Toast,代码如下

package com.net168.toast;import java.util.Timer;
import java.util.TimerTask;import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;public class ToastCustom {private WindowManager wdm;private double time;private View mView;private WindowManager.LayoutParams params;private Timer timer;private ToastCustom(Context context, String text, double time){wdm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);timer = new Timer();Toast toast = Toast.makeText(context, text, Toast.LENGTH_SHORT);mView = toast.getView();params = new WindowManager.LayoutParams();params.height = WindowManager.LayoutParams.WRAP_CONTENT;  params.width = WindowManager.LayoutParams.WRAP_CONTENT;  params.format = PixelFormat.TRANSLUCENT;  params.windowAnimations = toast.getView().getAnimation().INFINITE;  params.type = WindowManager.LayoutParams.TYPE_TOAST;  params.setTitle("Toast");  params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON  | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE  | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;params.y = -30;this.time = time;}public static ToastCustom makeText(Context context, String text, double time){ToastCustom toastCustom = new ToastCustom(context, text, time);return toastCustom;}public void show(){wdm.addView(mView, params);timer.schedule(new TimerTask() {@Overridepublic void run() {wdm.removeView(mView);}}, (long)(time * 1000));}public void cancel(){wdm.removeView(mView);timer.cancel();}}

PS:上面自定义Toast代码只实现了基本功能,其余功能由于时间关系没有全部实现。

测试代码如下:

public class MainActivity extends ActionBarActivity implements View.OnClickListener{private EditText edt_duration;private Button btn_toast_simple;private Button btn_toast_reflect;private Button btn_toast_custom;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);edt_duration = (EditText) findViewById(R.id.edt_duration);btn_toast_simple = (Button) findViewById(R.id.btn_toast_simple);btn_toast_reflect = (Button) findViewById(R.id.btn_toast_reflect);btn_toast_custom = (Button) findViewById(R.id.btn_toast_custom);btn_toast_simple.setOnClickListener(this);btn_toast_reflect.setOnClickListener(this);btn_toast_custom.setOnClickListener(this);}@Overridepublic void onClick(View v) {double time = Double.parseDouble((edt_duration.getText().toString()));switch (v.getId()){case R.id.btn_toast_simple:ToastSimple.makeText(MainActivity.this, "简单Toast,执行时间为:" + time, time).show();break;case R.id.btn_toast_reflect:ToastReflect.makeText(MainActivity.this, "反射Toast,执行时间为" + time, time).show();break;case R.id.btn_toast_custom:ToastCustom.makeText(MainActivity.this, "反射Toast,执行时间为" + time, time).show();break;}}
}

限于篇幅,也就懒得讲解了。。。。。。

作者:enjoy风铃
出处:http://www.cnblogs.com/net168/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则下次不给你转载了。

Android:剖析源码,随心所欲控制Toast显示相关推荐

  1. 【Android SDM660源码分析】- 03 - UEFI XBL GraphicsOutput BMP图片显示流程

    [Android SDM660源码分析]- 03 - UEFI XBL GraphicsOutput BMP图片显示流程 1. GraphicsOutput.h 2. 显示驱动初化 DisplayDx ...

  2. android简单录音机波形,android 录音机源码(带波形显示)

    [实例简介] android 录音机源码, 可以显示波形 [实例截图] [核心代码] d78dbf10-09a0-4149-b851-d49cef36ac33 └── MediaRecorder ├─ ...

  3. Android项目源码分享

    ├─android web应用 │      jqmDemo_static.zip │      jqmMobileDemo-master.zip │      jqmMobileDemo1_1-ma ...

  4. [Android精品源码] Android 仿美团网,探索ListView的A-Z字母排序功能实现选择城市

    Material Design中文版Code4APPPHP100UI4APP 开启辅助访问设为首页收藏本站快捷导航切换到宽版切换风格 石刚 | |我的 |签到打卡 |设置 |消息 |提醒(2) |退出 ...

  5. android系统源码7.1.2_r8下载,编译,运行到nexus5X上,修改源码并编译SDK进行测试

    一,学习android系统源码下载,编译的作用 1,可以自己 DIY 自己的rom系统,从系统层面,宏观的加深理解 android系统 2,编译自己的 userdebug(原生root权限) rom, ...

  6. Android FrameWork学习(二)Android系统源码调试

    点击打开链接 通过上一篇 Android FrameWork学习(一)Android 7.0系统源码下载\编译 我们了解了如何进行系统源码的下载和编译工作. 为了更进一步地学习跟研究 Android ...

  7. 【Android 10 源码】healthd 模块 HAL 2.0 分析

    Android 9 引入了从 health@1.0 HAL 升级的主要版本 android.hardware.health HAL 2.0.这一新 HAL 具有以下优势: 框架代码和供应商代码之间的区 ...

  8. Android FrameWork 学习之Android 系统源码调试

    这是很久以前访问掘金的时候 无意间看到的一个关于Android的文章,作者更细心,分阶段的将学习步骤记录在自己博客中,我觉得很有用,想作为分享同时也是留下自己知识的一些欠缺收藏起来,今后做项目的时候会 ...

  9. Android系统源码目录及功能介绍

    Android的移植按如下流程:     1.android linux 内核的普通驱动移植,让内核可以在目标平台上运行起来.     2.正确挂载文件系统,确保内核启动参数和 android 源代码 ...

最新文章

  1. Object-C 打开工程,选择模拟起时,提示no scheme
  2. linux常用的makefile模版编写
  3. Day 1 用户交互
  4. Ribbon-3使用配置文件自定义Ribbon Client
  5. 在JDK 12精简数字格式中使用最小分数数字
  6. Ollydbg使用教程学习总结(三)
  7. Eclipse是否必需要安装jdk,jre
  8. C++类型转换实现不同类型相加【复数与double类型相加】
  9. kafka如何创建topic
  10. xss.haozi.me靶场详解
  11. jsoup爬虫发送get、post请求、解析html、获取json
  12. kubernetes 亲和、反亲和、污点、容忍
  13. iOS 14 Beta 5 来了,修复游戏闪退问题了,还有着几个变化!
  14. android studio 显示view树_Android手势分发和嵌套滚动机制
  15. prometheus+grafana简介与linux下的安装
  16. 华中科大三个大学生创新团队的成长启示
  17. TensorFlow2 手把手教你实现自定义层
  18. ADZS-HPUSB-ICE以及ADI的DSP仿真器故事,也许你不知道,我来讲给你听
  19. pyqt5 tablewidget 隐藏表头,设置表头
  20. 基于C语⾔的中间代码⽣成

热门文章

  1. Windows文件系统中文件的储存原理、文件粉碎机的原理、数据恢复的原理
  2. 繁简转换的实现方式。
  3. A股api交易接口文档怎么使用?
  4. 股票交易接口api自动交易-Java继承
  5. 电子劳动合同,为企业用工护航
  6. ddos攻击是什么 怎么防ddos攻击教程
  7. 自学量化投资之旅-计算股票的复权价
  8. 迅为iMX6UL开发板多路串口开发平台接口详解
  9. 外设citrix xendesktop 手写笔
  10. 国外一些有价值的docker相关的文章