Android 高级自定义Toast及源码解析

neusoft1 8年前
   <h2><strong>Toast概述</strong></h2>    <h2><strong>Toast的作用</strong></h2>    <p>不需要和用户交互的提示框。</p>    <h2><strong>Toast的简单使用</strong></h2>    <pre>  <code class="language-java">Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷学习,日渐消瘦",Toast.LENGTH_SHORT).show()</code></pre>    <h2><strong>自定义Toast</strong></h2>    <pre>  <code class="language-java">Toast customToast = new Toast(MainActivity.this.getApplicationContext());      View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null);      ImageView img = (ImageView) customView.findViewById(R.id.img);      TextView tv = (TextView) customView.findViewById(R.id.tv);      img.setBackgroundResource(R.drawable.daima);      tv.setText("沉迷学习,日渐消瘦");      customToast.setView(customView);      customToast.setDuration(Toast.LENGTH_SHORT);      customToast.setGravity(Gravity.CENTER,0,0);      customToast.show();</code></pre>    <p>布局文件中根元素为 LinearLayout ,垂直放入一个 ImageView 和一个 TextView 。代码就不贴了。</p>    <h2><strong>高级自定义Toast</strong></h2>    <p>产品狗的需求:点击一个 Button ,网络请求失败的情况下使用 Toast 的方式提醒用户。<br> 程序猿:ok~大笔一挥。</p>    <pre>  <code class="language-java">Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷学习,日渐消瘦",Toast.LENGTH_SHORT).show()</code></pre>    <p>测试:你这程序写的有问题。每次点击就弹出了气泡,连续点击20次,居然花了一分多钟才显示完。改!<br> 程序猿:系统自带的就这样。爱要不要。<br> 测试:那我用单元测试模拟点击50次之后,它就不显示了,这个怎么说。<br> 程序猿:…<br> 这个时候,高级自定义 Toast 就要出场了~</p>    <p>activity_main.xml —->上下两个按钮,略。</p>    <p>MainActivity.java</p>    <pre>  <code class="language-java">public class MainActivity extends AppCompatActivity implements View.OnClickListener{        public static final String TAG = "MainActivity";      private Button customToastBtn;      private Button singleToastBtn;      private static int num;        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          initView();          initClick();          performClick(100);        }        private void initView() {          customToastBtn = (Button) findViewById(R.id.customToastBtn);          singleToastBtn = (Button) findViewById(R.id.singleToastBtn);      }        private void initClick() {          customToastBtn.setOnClickListener(this);          singleToastBtn.setOnClickListener(this);      }        /**       * 点击singleToastBtn按钮       * @param clickFrequency 点击的次数       */      private void performClick(int clickFrequency) {          for (int i = 0; i < clickFrequency; i++){              singleToastBtn.performClick();          }      }        @Override      public void onClick(View view) {          switch (view.getId()){              case R.id.customToastBtn:                  showCustomToast();                  break;              case R.id.singleToastBtn:                  showSingleToast();                  break;              default:break;          }      }        private void showCustomToast() {          Toast customToast = new Toast(MainActivity.this.getApplicationContext());          View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null);          ImageView img = (ImageView) customView.findViewById(R.id.img);          TextView tv = (TextView) customView.findViewById(R.id.tv);          img.setBackgroundResource(R.drawable.daima);          tv.setText("沉迷学习,日渐消瘦");          customToast.setView(customView);          customToast.setDuration(Toast.LENGTH_SHORT);          customToast.setGravity(Gravity.CENTER,0,0);          customToast.show();      }        private void showSingleToast() {          Toast singleToast = SingleToast.getInstance(MainActivity.this.getApplicationContext());          View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null);          ImageView img = (ImageView) customView.findViewById(R.id.img);          TextView tv = (TextView) customView.findViewById(R.id.tv);          img.setBackgroundResource(R.drawable.daima);          tv.setText("沉迷学习,日渐消瘦 第"+num+++"遍 toast="+singleToast);          singleToast.setView(customView);          singleToast.setDuration(Toast.LENGTH_SHORT);          singleToast.setGravity(Gravity.CENTER,0,0);          singleToast.show();      }  }</code></pre>    <p>SingleToast.java</p>    <pre>  <code class="language-java">public class SingleToast {        private static Toast mToast;        /**双重锁定,使用同一个Toast实例*/      public static Toast getInstance(Context context){          if (mToast == null){              synchronized (SingleToast.class){                  if (mToast == null){                      mToast = new Toast(context);                  }              }          }          return mToast;      }  }</code></pre>    <p>那么有的同学会问了:你这样不就是加了个单例吗,好像也没有什么区别。区别大了。仅仅一个单例,既实现了产品狗的需求,又不会有单元测试快速点击50次的之后不显示的问题。为什么?Read The Fucking Source Code。</p>    <h2><strong>Toast源码解析</strong></h2>    <p>这里以 Toast.makeText().show 为例,一步步追寻这个过程中源码所做的工作。自定义 Toast 相当于自己做了 makeText() 方法的工作,道理是一样一样的,这里就不再分别讲述了~</p>    <p>源码位置:frameworks/base/core/java/android/widght/Toast.java<br> Toast#makeText()</p>    <pre>  <code class="language-java">public static Toast makeText(Context context, CharSequence text, @Duration int duration) {          // 获取Toast对象          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);          // 设置View和duration属性          result.mNextView = v;          result.mDuration = duration;          return result;      }</code></pre>    <p>这里填充的布局 transient_notification.xml 位于frameworks/base/core/res/res/layout/transient_notification.xml。加分项,对于XML布局文件解析不太了解的同学可以看下这篇博客。</p>    <pre>  <code class="language-java"><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></code></pre>    <p>可以发现,里面只有一个 TextView ,平日设置的文本内容就是在这里展示。接下来只有一个 show() 方法,似乎我们的源码解析到这里就快结束了。不,这只是个开始</p>    <pre>  <code class="language-java">public void show() {          if (mNextView == null) {              throw new RuntimeException("setView must have been called");          }          INotificationManager service = getService();          String pkg = mContext.getOpPackageName();          TN tn = mTN;          tn.mNextView = mNextView;          try {              service.enqueueToast(pkg, tn, mDuration);          } catch (RemoteException e) {              // Empty          }      }</code></pre>    <p>这里有三个问题。<br> 1. 通过 getService() 怎么就获得一个 INotificationManager 对象?<br> 2. TN 类是个什么鬼?<br> 3. 方法最后只有一个 service.enqueueToast() ,显示和隐藏在哪里?</p>    <p>Toast 的精华就在这三个问题里,接下来的内容全部围绕上述三个问题,尤其是第三个。已经全部了解的同学可以去看别的博客了~</p>    <h2><strong>1. 通过 getService() 怎么就获得一个 INotificationManager 对象?</strong></h2>    <pre>  <code class="language-java">static private INotificationManager getService() {          if (sService != null) {              return sService;          }          sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));          return sService;      }</code></pre>    <p>对 Binder 机制了解的同学看见 XXX.Stub.asInterface 肯定会很熟悉,这不就是 AIDL 中获取 client 嘛!确实是这样。</p>    <p>tips: 本着追本溯源的精神,先看下 ServiceManager.getService("notification") 。startOtherServices() 涉及到 NotificationManagerService 的启动,代码如下,这里不再赘述。</p>    <pre>  <code class="language-java">mSystemServiceManager.startService(NotificationManagerService.class);</code></pre>    <p>Toast 中 AIDL 对应文件的位置。</p>    <p>源码位置:frameworks/base/core/java/android/app/INotificationManager.aidl</p>    <p>Server 端: NotificationManagerService.java<br> 源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java</p>    <p>篇幅有限,这里不可能将 AIDL 文件完整的叙述一遍,不了解的同学可以理解为:经过进程间通信( AIDL 方式),最后调用 NotificationManagerService#enqueueToast() 。具体可以看下这篇博客。</p>    <h2><strong>2. TN 类是个什么鬼?</strong></h2>    <p>在 Toast#makeText() 中第一行就获取了一个 Toast 对象</p>    <pre>  <code class="language-java">public Toast(Context context) {          mContext = context;          mTN = new TN();          mTN.mY = context.getResources().getDimensionPixelSize(                  com.android.internal.R.dimen.toast_y_offset);          mTN.mGravity = context.getResources().getInteger(                  com.android.internal.R.integer.config_toastDefaultGravity);      }</code></pre>    <p>源码位置:frameworks/base/core/java/android/widght/Toast$TN.java</p>    <pre>  <code class="language-java">private static class TN extends ITransientNotification.Stub {          ...          TN() {              final WindowManager.LayoutParams params = mParams;              params.height = WindowManager.LayoutParams.WRAP_CONTENT;              params.width = WindowManager.LayoutParams.WRAP_CONTENT;              ...          }          ...      }</code></pre>    <p>源码中的进程间通信实在太多了,我不想说这方面的内容啊啊啊~。有时间专门再写一片博客。这里提前剧透下 TN 类除了设置参数的作用之外,更大的作用是 Toast 显示与隐藏的回调。 TN 类在这里作为 Server 端。 NotificationManagerService$NotificationListeners 类作为 client 端。这个暂且按下不提,下文会详细讲述。</p>    <h2><strong>3. show() 方法最后只有一个 service.enqueueToast() ,显示和隐藏在哪里?</strong></h2>    <p>源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java</p>    <pre>  <code class="language-java">private final IBinder mService = new INotificationManager.Stub() {            @Override          public void enqueueToast(String pkg, ITransientNotification callback, int duration)          {              if (pkg == null || callback == null) {                  Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);                  return ;              }              final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));              ...              synchronized (mToastQueue) {                  int callingPid = Binder.getCallingPid();                  long callingId = Binder.clearCallingIdentity();                  try {                      ToastRecord record;                      int index = indexOfToastLocked(pkg, callback);                      if (index >= 0) {                          record = mToastQueue.get(index);                          record.update(duration);                      } else {                          if (!isSystemToast) {                              int count = 0;                              final int N = mToastQueue.size();                              for (int i=0; i<N; i++) {                                   final ToastRecord r = mToastQueue.get(i);                                   if (r.pkg.equals(pkg)) {                                       count++;                                       if (count >= MAX_PACKAGE_NOTIFICATIONS) {                                           Slog.e(TAG, "Package has already posted " + count                                                  + " toasts. Not showing more. Package=" + pkg);                                           return;                                       }                                   }                              }                          }                            record = new ToastRecord(callingPid, pkg, callback, duration);                          mToastQueue.add(record);                          index = mToastQueue.size() - 1;                          // 将Toast所在的进程设置为前台进程                          keepProcessAliveLocked(callingPid);                      }                      if (index == 0) {                          showNextToastLocked();                      }                  } finally {                      Binder.restoreCallingIdentity(callingId);                  }              }          }          ...      }</code></pre>    <p>在 Toast#show() 最终会进入到这个方法。首先通过 indexOfToastLocked() 方法获取应用程序对应的 ToastRecord 在 mToastQueue 中的位置, Toast 消失后返回-1,否则返回对应的位置。 mToastQueue 明明是个 ArratList 对象,却命名 Queue ,猜测后面会遵循“后进先出”的原则移除对应的 ToastRecord 对象~。这里先以返回 index=-1 查看,也就是进入到 else 分支。如果不是系统程序,也就是应用程序。那么同一个应用程序 <strong>瞬时</strong> 在 mToastQueue 中存在的消息不能超过50条( Toast 对象不能超过50个)。否则直接 return 。这也是上文中为什么快速点击50次之后无法继续显示的原因。既然 <strong>瞬时</strong> Toast 不能超过50个,那么运用单例模式使用同一个 Toast 对象不就可以了嘛?答案是:可行。消息用完了就移除, <strong>瞬时</strong> 存在50个以上的 Toast 对象相信在正常的程序中也用不上。而且注释中也说这样做是为了放置DOS攻击和防止泄露。其实从这里也可以看出:为了防止内存泄露,创建 Toast 最好使用 getApplicationContext ,不建议使用 Activity 、 Service 等。</p>    <p>回归主题。接下来创建了一个 ToastRecord 对象并添加进 mToastQueue 。接下来调用 showNextToastLocked() 方法显示一个 Toast 。</p>    <p>源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java<br> NotificationManagerService#showNextToastLocked()</p>    <pre>  <code class="language-java">void showNextToastLocked() {          ToastRecord record = mToastQueue.get(0);          while (record != null) {              if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);              try {                  record.callback.show();                  scheduleTimeoutLocked(record);                  return;              } catch (RemoteException e) {                  int index = mToastQueue.indexOf(record);                  if (index >= 0) {                      mToastQueue.remove(index);                  }                  keepProcessAliveLocked(record.pid);                  if (mToastQueue.size() > 0) {                      record = mToastQueue.get(0);                  } else {                      record = null;                  }              }          }      }</code></pre>    <p>这里首先调用 record.callback.show() ,这里的 record.callback 其实就是 TN 类。接下来调用 scheduleTimeoutLocked() 方法,我们知道 Toast 显示一段时间后会自己消失,所以这个方法肯定是定时让 Toast 消失。跟进。</p>    <pre>  <code class="language-java">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);      }</code></pre>    <p>果然如此。重点在于使用 mHandler.sendMessageDelayed(m, delay) 延迟发送消息。这里的 delay 只有两种值,要么等于 LENGTH_LONG ,其余统统的等于 SHORT_DELAY , setDuration 为其他值用正常手段是没有用的(可以反射,不在重点范围内)。<br> handler 收到 MESSAGE_TIMEOUT 消息后会调用 handleTimeout((ToastRecord)msg.obj) 。跟进。</p>    <pre>  <code class="language-java">private void handleTimeout(ToastRecord record)      {          if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);          synchronized (mToastQueue) {              int index = indexOfToastLocked(record.pkg, record.callback);              if (index >= 0) {                  cancelToastLocked(index);              }          }      }</code></pre>    <p>啥也不说了,跟进吧~</p>    <pre>  <code class="language-java">void cancelToastLocked(int index) {          ToastRecord record = mToastQueue.get(index);          try {              record.callback.hide();          } catch (RemoteException e) {              ...          }          mToastQueue.remove(index);          keepProcessAliveLocked(record.pid);          if (mToastQueue.size() > 0) {              showNextToastLocked();          }      }</code></pre>    <p>延迟调用 record.callback.hide() 隐藏 Toast ,前文也提到过: record.callback 就是 TN 对象。到这,第三个问题已经解决一半了,至少我们已经直到 Toast 的显示和隐藏在哪里被调用了,至于怎么显示怎么隐藏的,客观您接着往下看。</p>    <p>源码位置:frameworks/base/core/java/android/widght/Toast . j a v a o a s t</p>    <p>TN#show()</p>    <pre>  <code class="language-java">final Handler mHandler = new Handler();             @Override          public void show() {              if (localLOGV) Log.v(TAG, "SHOW: " + this);              mHandler.post(mShow);          }            final Runnable mShow = new Runnable() {              @Override              public void run() {                  handleShow();              }          };</code></pre>    <p>注意下这里直接使用 new Handler 获取 Handler 对象,这也是为什么在子线程中不用 Looper 弹出Toast会出错的原因。跟进 handleShow() 。</p>    <pre>  <code class="language-java">public void handleShow() {              if (mView != mNextView) {                  // remove the old view if necessary                  handleHide();                  mView = mNextView;                  Context context = mView.getContext().getApplicationContext();                  String packageName = mView.getContext().getOpPackageName();                  if (context == null) {                      context = mView.getContext();                  }                  mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);                  ...                  mParams.packageName = packageName;                  if (mView.getParent() != null) {                      mWM.removeView(mView);                  }                  mWM.addView(mView, mParams);                  trySendAccessibilityEvent();              }          }</code></pre>    <p>原来 addView 到 WindowManager 。这样就完成了 Toast 的显示。至于隐藏就更简单了。</p>    <pre>  <code class="language-java">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;              }          }</code></pre>    <p>直接 remove 掉。</p>    <p> </p>    <p> </p>    <p>来自:http://blog.csdn.net/qq_17250009/article/details/52753929</p>    <p> </p>