同學,你的系統吐司可能需要修復一下

Java和Android架構發表於2018-12-28

640?wx_fmt=gif

熱文導讀 | 點選標題閱讀

金九銀十跳槽季如何進階找到合適滿意的工作?

35歲老程式設計師因身體原因沒加班,老闆:不想幹就滾蛋

劉強東翻臉!忍不住向兄弟們下手,真相令人震撼!

歡迎fork,專案地址:

https://github.com/Dovar66/DToast

先看看使用系統吐司存在的問題:

1.當通知許可權被關閉時在華為等手機上Toast不顯示;

2.Toast的佇列機制在不同手機上可能會不相同;

3.ToastBadTokenException問題;

當發現系統吐司存在問題時,不少同學都會採用自定義的TYPE_TOAST彈窗來實現相同效果雖然大部分情況下效果都是
OK的,但其實TYPE_TOAST彈窗依然存在相容問題:

4.Android8.0之後的token null is not valid問題(實測部分機型問題);

5.Android7.1之後,不允許同時展示兩個TYPE_TOAST彈窗(實測部分機型問題)。

那麼,DToast使用的解決方案是:

1.通知許可權未被關閉時,使用SystemToast(修復了問題2和問題3的系統Toast);
2.通知許可權被關閉時,使用DovaToast(自定義的TYPE_TOAST彈窗);
3.當使用DovaToast出現token null is not valid時,嘗試使用ActivityToast(自定義的TYPE_APPLICATION_ATTACHED_DIALOG
彈窗,只有當傳入Context為Activity時,才會啟用ActivityToast).

相信不少同學舊專案中封裝的ToastUtil都是直接使用的ApplicationContext的作為上下文,然後在需要彈窗的時候直接就是ToastUtil.show(STR),這樣的使用方式對於我們來說是最方便的啦。

當然,使用DToast你也依然可以沿用這種封裝方式,但這種方式在下面這個場景中可能會無法成功展示出彈窗(該場景下原生吐司也一樣無法彈出),
不過請放心不會導致應用崩潰,而且這個場景出現的概率較小,有以下三個必要條件:1。通知欄許可權被關閉(通知欄許可權預設都是開啟的)
2。非MIUI手機3.Android8.0以上的部分手機(我最近測試中的幾部8.0+裝置都不存在該問題)。

不過,如果想要保證在所有場景下都能正常展示彈窗,還是建議在DToast.make(上下文)時傳入活動作為上下文,這樣在該場景下DToast會啟用ActivityToast展示出彈窗。

接下來再詳細分析下上面提到的五個問題:

問題一:關閉通知許可權時吐司不顯示

看下方Toast原始碼中的show()方法,通過AIDL獲取到INotificationManager,並將接下來的顯示流程控制權
交給NotificationManagerService。
NMS中會對Toast進行許可權校驗,當通知許可權校驗不通過時,Toast將不做展示。
當然不同ROM中NMS可能會有不同,比如MIUI就對這部分內容進行了修改,所以小米手機關閉通知許可權不會導致Toast不顯示。

  /**
     * Show the view for the specified duration.
     */

    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
        }
    }

如何解決這個問題?只要能夠繞過NotificationManagerService即可。

DovaToast通過使用TYPE_TOAST實現全域性彈窗功能,不使用系統Toast,也沒有使用NMS服務,因此不受通知許可權限制。

問題二:系統吐司的佇列機制在不同手機上可能會不相同

 我找了四臺裝置,建立兩個Gravity不同的Toast並呼叫show()方法,結果出現了四種展示效果:

        * 榮耀5C-android7.0(只看到展示第一個Toast
        * 小米8-MIUI10(只看到展示第二個Toast,即新的Toast.show會中止當前Toast的展示)
        * 紅米6pro-MIUI9(兩個Toast同時展示)
        * 榮耀5C-android6.0(第一個TOAST展示完成後,第二個才開始展示)

造成這個問題的原因應該是各大廠商ROM中NMS維護吐司佇列的邏輯有差異。
同樣的,DToast內部也維護著自己的佇列邏輯,保證在所有手機上使用DToast的效果相同。

 DToast中多個彈窗連續出現時:

        1.相同優先順序時,會終止上一個,直接展示後一個;
        2.不同優先順序時,如果後一個的優先順序更高則會終止上一個,直接展示後一個。

問題三:系統吐司的BadTokenException問題

  • Toast有個內部類TN(擴充套件ITransientNotification.Stub),呼叫Toast.show()時會將TN傳遞給NMS;

    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
        }
    }
  • 在NMS中會生成一個windowToken,並將windowToken給到WindowManagerService,WMS會暫時儲存該令牌並用於之後的校驗;

    NotificationManagerService.java #enqueueToast原始碼:

        synchronized (mToastQueue) {
            int callingPid = Binder.getCallingPid();
            long callingId = Binder.clearCallingIdentity();
            try {
                ToastRecord record;
                int index = indexOfToastLocked(pkg, callback);
                // If it's already in the queue, we update it in place, we don't
                // move it to the end of the queue.
                if (index >= 0) {
                    record = mToastQueue.get(index);
                    record.update(duration);
                } else {
                    // Limit the number of toasts that any given package except the android
                    // package can enqueue.  Prevents DOS attacks and deals with leaks.
                    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;
                                 }
                             }
                        }
                    }
                Binder token = new Binder();//生成一個token
                mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                record = new ToastRecord(callingPid, pkg, callback, duration, token);
                mToastQueue.add(record);
                index = mToastQueue.size() - 1;
                keepProcessAliveIfNeededLocked(callingPid);
            }
            // If it's at index 0, it's the current toast.  It doesn't matter if it's
            // new or just been updated.  Call back and tell it to show itself.
            // If the callback fails, this will remove it from the list, so don't
            // assume that it's valid after this.
            if (index == 0) {
                showNextToastLocked();
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
  • 然後NMS通過呼叫TN.show(windowToken)回傳令牌給TN;

        /**
         * schedule handleShow into the right thread
         */

        @Override
        public void show(IBinder windowToken) 
    {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }
  • TN使用該令牌嘗試向WindowManager中新增Toast檢視(mParams.token = windowToken);

    在API25的原始碼中,吐司的WindowManager.LayoutParams引數新增了一個標記屬性,用於對新增的視窗進行校驗。

640?wx_fmt=png

  • 當param.token為空時,WindowManagerImpl會為其設定DefaultToken;

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params
    {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

    private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params{
        // Only use the default token if we don't have a parent window.
        if (mDefaultToken != null && mParentWindow == null) {
            if (!(params instanceof WindowManager.LayoutParams)) {
                throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
            }
            // Only use the default token if we don't already have a token.
            final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
            if (wparams.token == null) {
                wparams.token = mDefaultToken;
            }
        }
    }
  • 當WindowManager收到addView請求後會檢查mParams.token是否有效,若有效則新增視窗展示,否則丟擲BadTokenException異常。

              switch (res) {
                  case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                  case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                      throw new WindowManager.BadTokenException(
                              "Unable to add window -- token " + attrs.token
                              + " is not valid; is your activity running?");
                  case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                      throw new WindowManager.BadTokenException(
                              "Unable to add window -- token " + attrs.token
                              + " is not for an application");
                  case WindowManagerGlobal.ADD_APP_EXITING:
                      throw new WindowManager.BadTokenException(
                              "Unable to add window -- app for token " + attrs.token
                              + " is exiting");
                  case WindowManagerGlobal.ADD_DUPLICATE_ADD:
                      throw new WindowManager.BadTokenException(
                              "Unable to add window -- window " + mWindow
                              + " has already been added");
                  case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
                      // Silently ignore -- we would have just removed it
                      // right away, anyway.
                      return;
                  case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
                      throw new WindowManager.BadTokenException("Unable to add window "
                              + mWindow + " -- another window of type "
                              + mWindowAttributes.type + " already exists");
                  case WindowManagerGlobal.ADD_PERMISSION_DENIED:
                      throw new WindowManager.BadTokenException("Unable to add window "
                              + mWindow + " -- permission denied for window type "
                              + mWindowAttributes.type);
                  case WindowManagerGlobal.ADD_INVALID_DISPLAY:
                      throw new WindowManager.InvalidDisplayException("Unable to add window "
                              + mWindow + " -- the specified display can not be found");
                  case WindowManagerGlobal.ADD_INVALID_TYPE:
                      throw new WindowManager.InvalidDisplayException("Unable to add window "
                              + mWindow + " -- the specified window type "
                              + mWindowAttributes.type + " is not valid");
              }

什麼情況下windowToken會失效?

UI執行緒發生阻塞,導致TN.show()沒有及時執行,當NotificationManager的檢測超時後便會刪除WMS中的該token,即造成token失效。

如何解決?

Google在API26中修復了這個問題,即增加了try-catch:

            // Since the notification manager service cancels the token right
            // after it notifies us to cancel the toast there is an inherent
            // race and we may attempt to add a window after the token has been
            // invalidated. Let us hedge against that.
            try {
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            } catch (WindowManager.BadTokenException e) {
                /* ignore */
            }

因此對於8.0之前的我們也需要做相同的處理.DToast是通過反射完成這個動作,具體看下方實現:

  //捕獲8.0之前Toast的BadTokenException,Google在Android 8.0的程式碼提交中修復了這個問題
     private void hook(Toast toast) {
         try {
             Field sField_TN = Toast.class.getDeclaredField("mTN");
             sField_TN.setAccessible(true);
             Field sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler");
             sField_TN_Handler.setAccessible(true);

             Object tn = sField_TN.get(toast);
             Handler preHandler = (Handler) sField_TN_Handler.get(tn);
             sField_TN_Handler.set(tn, new SafelyHandlerWrapper(preHandler));
         } catch (Exception e) {
             e.printStackTrace();
         }
     }


     public class SafelyHandlerWrapper extends Handler {
         private Handler impl;

         public SafelyHandlerWrapper(Handler impl) {
             this.impl = impl;
         }

         @Override
         public void dispatchMessage(Message msg) {
             try {
                 impl.dispatchMessage(msg);
             } catch (Exception e) {
             }
         }

         @Override
         public void handleMessage(Message msg) {
             impl.handleMessage(msg);//需要委託給原Handler執行
         }
     }

問題四:Android8.0之後的令牌null無效問題

Android8.0後對視窗管理做了限制和修改,特別是TYPE_TOAST型別的視窗,必須要傳遞一個令牌用於校驗。

API25:(PhoneWindowManager.java原始碼)

public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
    int type = attrs.type;

    outAppOp[0] = AppOpsManager.OP_NONE;

    if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)
            || (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)
            || (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {
        return WindowManagerGlobal.ADD_INVALID_TYPE;
    }

    if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
        // Window manager will make sure these are okay.
        return WindowManagerGlobal.ADD_OKAY;
    }
    String permission = null;
    switch (type) {
        case TYPE_TOAST:
            // XXX right now the app process has complete control over
            // this...  should introduce a token to let the system
            // monitor/control what they are doing.
            outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW;
            break;
        case TYPE_DREAM:
        case TYPE_INPUT_METHOD:
        case TYPE_WALLPAPER:
        case TYPE_PRIVATE_PRESENTATION:
        case TYPE_VOICE_INTERACTION:
        case TYPE_ACCESSIBILITY_OVERLAY:
        case TYPE_QS_DIALOG:
            // The window manager will check these.
            break;
        case TYPE_PHONE:
        case TYPE_PRIORITY_PHONE:
        case TYPE_SYSTEM_ALERT:
        case TYPE_SYSTEM_ERROR:
        case TYPE_SYSTEM_OVERLAY:
            permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
            outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
            break;
        default:
            permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
    }
    if (permission != null) {
        ...
    }
    return WindowManagerGlobal.ADD_OKAY;
}

API26:(PhoneWindowManager.java原始碼)

public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
    int type = attrs.type;

    outAppOp[0] = AppOpsManager.OP_NONE;

    if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)
            || (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)
            || (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {
        return WindowManagerGlobal.ADD_INVALID_TYPE;
    }

    if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
        // Window manager will make sure these are okay.
        return ADD_OKAY;
    }

    if (!isSystemAlertWindowType(type)) {
        switch (type) {
            case TYPE_TOAST:
                // Only apps that target older than O SDK can add window without a token, after
                // that we require a token so apps cannot add toasts directly as the token is
                // added by the notification system.
                // Window manager does the checking for this.
                outAppOp[0] = OP_TOAST_WINDOW;
                return ADD_OKAY;
            case TYPE_DREAM:
            case TYPE_INPUT_METHOD:
            case TYPE_WALLPAPER:
            case TYPE_PRESENTATION:
            case TYPE_PRIVATE_PRESENTATION:
            case TYPE_VOICE_INTERACTION:
            case TYPE_ACCESSIBILITY_OVERLAY:
            case TYPE_QS_DIALOG:
                // The window manager will check these.
                return ADD_OKAY;
        }
        return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
                == PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;
    }
}

為了解決問題一,DovaToast不得不選擇繞過NotificationManagerService的控制,但由於windowToken是NMS生成的,
繞過NMS就無法獲取到有效的windowToken,於是作為TYPE_TOAST的DovaToast就可能陷入第四個問題。因此,DToast選擇在DovaToast出現
該問題時引入ActivityToast,在DovaToast無法正常展示時建立一個依附於活性的彈窗展示出來,不過ActivityToast只會展示在當前活動,不具有跨頁面功能。
如果說有更好的方案,那肯定是去獲取懸浮窗許可權然後改用TYPE_PHONE等型別,但懸浮窗許可權往往不容易獲取,目前來看恐怕除了微信其他APP都不能保證拿得到使用者的懸浮窗許可權

問題五:Android7.1之後,不允許同時展示兩個TYPE_TOAST彈窗

DToast的彈窗策略就是同一時間最多隻展示一個彈窗,邏輯上就避免了此問題。因此僅捕獲該異常。

TODO清單:

  • 增加適配應用已獲取到懸浮窗許可權的情況

  • 考慮是否需要支援同時展示多個彈窗

其他建議

  • 新專案做應用架構的時候可以考慮把整個應用(除閃屏頁等特殊介面外)做成只有一個活動,其他全是片段,這樣就不存在懸浮窗的問題啦。

  • 如果能夠接受敬酒不跨介面的話,建議使用小吃吧

想進阿里嗎?快加入我們的知識星球吧,如下:

640?wx_fmt=gif

如有收穫,歡迎分享 640?wx_fmt=jpeg

「點贊640?「評論 640?wx_fmt=jpeg

 媽媽常教導我,讓我養成良好習慣。這樣長大才能成為一個有用的人。良好的習慣是尊敬師長這樣長大才能成為一個有用的人。良好的習慣是尊敬師長,愛護同學,對人有禮貌;是不粗心,做事情不拖拉;還是愛護公物,不浪費糧食。為什麼呢?因為擁有良好習慣,做一個品德高尚的人,懂得尊重別人,才會得到別人的尊重。我要努力地做到這些。我有一些壞習慣,有時候學習很粗心,把一些會做的題做錯。在生活上,也很粗心,有一次早上起床居然穿反了衣服。我吃飯很慢,有的時候還剩飯。我還起床磨蹭,本來應該迅速地穿好衣服,但是,我總是磨磨蹭蹭地,速度很慢。我打算在這學期裡,改掉這些壞習慣。早上起來,迅速地穿好衣服,不拖拉。學習不粗心,仔細完成每一道題。吃飯的時候,要很快的把飯吃完,不剩飯。我要從一點一滴做起,逐漸養成良好習慣。我相信自己一定能成為一名品學兼優的好學生!我打算在這學期裡,改掉這些壞習慣。早上起來,迅速地穿好衣服,不拖拉。學習不粗心,仔細完成每一道題。吃飯的時候,要很快的把飯吃完,不剩飯。我要從一點一滴做起,逐漸養成良好習慣。我相信自己一定能成為一名品學兼優的好學生!  在上幼兒園以前,我什麼也不會幹,就連穿衣服也是媽媽給我穿好,就要上幼兒園了,這樣可不行,媽媽鍛鍊我要學會自己穿衣服。   有一天,媽媽把衣服擺在我面前,開始讓我自己穿。一開始。我又哭又叫就是不穿,還把衣服扔的滿地都是,然後坐在地上開始大哭,等了好長時間,媽媽還是不理我,我只好自己乖乖的把衣服穿好, 一出了房間門,媽媽就笑了起來,再看看我的衣服,毛衣和褲子都穿反了,我趕緊回房間又重新穿了一遍,這次穿好了,拿起外套,可是外套的扣子又扣不上了,釦子可調皮了,好像故意和我作對,我把釦子往釦眼——人類邪惡的根源;愛情——幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話:幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話親愛的!擦乾你的眼淚,至高無上的愛情已經開啟了我們的眼界,使我們成了它的崇拜者。是它,





 媽媽常教導我,讓我養成良好習慣。這樣長大才能成為一個有用的人。良好的習慣是尊敬師長這樣長大才能成為一個有用的人。良好的習慣是尊敬師長,愛護同學,對人有禮貌;是不粗心,做事情不拖拉;還是愛護公物,不浪費糧食。為什麼呢?因為擁有良好習慣,做一個品德高尚的人,懂得尊重別人,才會得到別人的尊重。我要努力地做到這些。我有一些壞習慣,有時候學習很粗心,把一些會做的題做錯。在生活上,也很粗心,有一次早上起床居然穿反了衣服。我吃飯很慢,有的時候還剩飯。我還起床磨蹭,本來應該迅速地穿好衣服,但是,我總是磨磨蹭蹭地,速度很慢。我打算在這學期裡,改掉這些壞習慣。早上起來,迅速地穿好衣服,不拖拉。學習不粗心,仔細完成每一道題。吃飯的時候,要很快的把飯吃完,不剩飯。我要從一點一滴做起,逐漸養成良好習慣。我相信自己一定能成為一名品學兼優的好學生!我打算在這學期裡,改掉這些壞習慣。早上起來,迅速地穿好衣服,不拖拉。學習不粗心,仔細完成每一道題。吃飯的時候,要很快的把飯吃完,不剩飯。我要從一點一滴做起,逐漸養成良好習慣。我相信自己一定能成為一名品學兼優的好學生!  在上幼兒園以前,我什麼也不會幹,就連穿衣服也是媽媽給我穿好,就要上幼兒園了,這樣可不行,媽媽鍛鍊我要學會自己穿衣服。   有一天,媽媽把衣服擺在我面前,開始讓我自己穿。一開始。我又哭又叫就是不穿,還把衣服扔的滿地都是,然後坐在地上開始大哭,等了好長時間,媽媽還是不理我,我只好自己乖乖的把衣服穿好, 一出了房間門,媽媽就笑了起來,再看看我的衣服,毛衣和褲子都穿反了,我趕緊回房間又重新穿了一遍,這次穿好了,拿起外套,可是外套的扣子又扣不上了,釦子可調皮了,好像故意和我作對,我把釦子往釦眼——人類邪惡的根源;愛情——幸福和光明的源泉。我一直在這些思想的舞臺上徘徊。突然我發現兩個身影從我面前經過,坐在不遠的草地上。這是一對從農田那邊走過來的青年男女。農田那邊有農民的茅舍。在一陣令人傷心的沉默之後,隨著一聲長嘆,我聽見從一個肺癆病人的嘴裡說出了這樣的話:親愛的!擦乾你的眼淚,至高無上的愛情已經開啟了我們的眼界,使我們成了它的崇拜者。是它,

你有好的文章想和大家分享歡迎投稿,直接向我投遞文章連結即可


最後,國慶福利來了,我們的知識星球已達到1000人了,之前說過到達1000人時將大大幅漲價到169元,為了反饋大家對我們的關注和厚愛,特此維持現價99元最後一天,今天后(今晚 00:00)後將漲到169元,歡迎大家加入我們的知識星球,更多星球資訊參見:

如何進階成為Java和Android架構師?

金九銀十跳槽季如何進階找到合適滿意的工作?

說兩件事

640?wx_fmt=jpeg

微信掃描或者點選上方二維碼領取Android\Python\AI\Java等高階進階資源

更多學習資料點選下面的“閱讀原文”獲取

640?wx_fmt=gif

相關文章