Android 9.0 Toast源码改变引发的问题 (2024)

Android 9.0 Toast源码改变引发的问题 (1)

问题描述

在Android开发中,Toast的重复显示问题很早就有人提出了解决方案,具体做法就是全局使用一个Toast对象,就像下面这样:

private static Toast mToast = null;/** * 显示一个Toast提示 * * @param context context 上下文对象 * @param text toast字符串 * @param duration toast显示时间 */public static void showToast(Context context, String text, int duration) { if (mToast == null) { mToast = Toast.makeText(context, text, duration); } else { mToast.setText(text); mToast.setDuration(duration); } mToast.show();}

相信大多数人的项目中都会有一个类似的工具类,但其实这个方法存在一个问题,在Android 9.0的手机上(我不确定是否都会有这个问题,有的手机厂商可能会定制自己的Toast),当前Toast还未消失时弹出下一个Toast,会导致当前Toast消失,并且下一个Toast也不会显示,之后短时间内弹出的Toast也不会显示,如下图所示:

Android 9.0 Toast源码改变引发的问题 (2)

Toast消失

问题产生的原因

想要弄清楚这个问题产生的原因就要从源码入手了,我们首先来了解一下Toast的显示原理,下文的分析基于Android 9.0(API Level 28)的源码。

1.Toast的显示原理

我们先来看Toast的makeText()方法,makeText()有三个重载方法,最终调用的都是下面的方法:

public static Toast makeText(@NonNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Duration int duration) { // 创建Toast对象 Toast result = new Toast(context, looper); // 加载Toast布局 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); // 给Toast对象的mNextView和mDuration赋值 result.mNextView = v; result.mDuration = duration; return result;}

方法内部首先创建了一个Toast对象,之后加载Toast的布局,将其赋值给Toast的mNextView。接下来我们看一下Toast的构造方法:

public Toast(@NonNull Context context, @Nullable Looper looper) { mContext = context; // 创建TN对象 mTN = new TN(context.getPackageName(), looper); 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);}

Toast的构造方法内部创建了一个TN类型的对象,将其赋值给Toast中的成员变量mTN,我们来看一下这个TN是什么。

private static class TN extends ITransientNotification.Stub { // ... TN(String packageName, @Nullable Looper looper) { 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; mPackageName = packageName; if (looper == null) { looper = Looper.myLooper(); if (looper == null) { // Toast不能创建在没有Looper的线程中 throw new RuntimeException( "Can't toast on a thread that has not called Looper.prepare()"); } } mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { case SHOW: { IBinder token = (IBinder) msg.obj; handleShow(token); break; } case HIDE: { handleHide(); mNextView = null; break; } case CANCEL: { handleHide(); mNextView = null; try { getService().cancelToast(mPackageName, TN.this); } catch (RemoteException e) { } break; } } } }; // ...}

TN继承自ITransientNotification.Stub,是一个Binder类型,作用肯定就是用于跨进程了。在TN的构造方法中首先设置了Toast窗口的一些属性,包括宽高等等,然后创建了一个Handler对象,将其赋值给TN内部的成员变量mHandler,在创建Handler对象的时候传入了Looper对象,根据上面的判断不难看出Toast不能创建在没有Looper的线程中。关于这个Handler的作用后面会分析,这里先跳过。
到这里Toast对象的创建过程就完成了,用一张图总结一下:

Android 9.0 Toast源码改变引发的问题 (3)

Toast对象创建完成

创建出Toast对象后调用show()方法Toast就会显示出来了,接下来我们来看Toast的show()方法。

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

show()方法内部的逻辑还是比较简单的,将Toast的mNextView对象赋值给mTN内部的同名成员变量mNextView,然后通过getService()方法获取到NotificationManagerService,调用它的enqueueToast()方法,参数传入了包名、mTN和Toast显示时长。NotificationManagerService和ActivityManagerService类似,是系统的通知服务,这就解释了为什么在有的手机上关掉应用的通知权限会导致Toast不显示,由于这个问题不是本文要研究的重点,网上也有一些相关的文章,这里就不介绍了。
接下来我们来看NotificationManagerService的enqueueToast()方法:

@Overridepublic void enqueueToast(String pkg, ITransientNotification callback, int duration) { // ... // 是否为系统级应用 final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg)); // ... ToastRecord record; int index; if (!isSystemToast) { // 不是系统级应用的Toast index = indexOfToastPackageLocked(pkg); } else { index = indexOfToastLocked(pkg, callback); } if (index >= 0) { // 应用已经显示了Toast record = mToastQueue.get(index); record.update(duration); try { record.callback.hide(); } catch (RemoteException e) { } record.update(callback); } else { // 应用未显示Toast Binder token = new Binder(); mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY); record = new ToastRecord(callingPid, pkg, callback, duration, token); mToastQueue.add(record); index = mToastQueue.size() - 1; } // ... if (index == 0) { showNextToastLocked(); } // ...}

NotificationManagerService会将每一个Toast封装为ToastRecord对象,并添加到mToastQueue中,mToastQueue的类型是ArrayList。在enqueueToast()方法中首先会调用indexOfToastPackageLocked()方法根据传入的包名获取mToastQueue中相应ToastRecord的索引,将返回值赋值给index。

int indexOfToastPackageLocked(String pkg) { ArrayList<ToastRecord> list = mToastQueue; int len = list.size(); for (int i = 0; i < len; i++) { ToastRecord r = list.get(i); if (r.pkg.equals(pkg)) { return i; } } return -1;}

如果index大于或等于0,说明当前应用已经显示了Toast,这里我们先不管这种情况,后面会具体分析。我们直接来看index小于0(等于-1)的情况,这种情况说明应用未显示Toast,此时mToastQueue的size为0,会创建出ToastRecord对象,添加到mToastQueue中,此时mToastQueue的size变为1,因此index被赋值为0,进而调用showNextToastLocked()方法。

void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record != null) { // ... record.callback.show(record.token); scheduleDurationReachedLocked(record); return; }}

showNextToastLocked()方法取出mToastQueue中的第一个元素,对应着要显示的Toast,接下来进入了一个while循环,循环体内部首先执行了record.callback.show(),结合之前创建ToastRecord对象的代码不难看出这里的record.callback其实就是enqueueToast()的callback参数,也就是Toast对象的mTN,因此这里调用的就是TN的show()方法,我们回到TN类中来看一下这个方法:

@Overridepublic void show(IBinder windowToken) { // ... mHandler.obtainMessage(SHOW, windowToken).sendToTarget();}mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { case SHOW: { IBinder token = (IBinder) msg.obj; handleShow(token); break; } // ... } }};

show()方法内部其实就是使用TN内部的mHandler发送了一条消息,我们找到mHandler对消息的处理,发现接着调用了handleShow()方法。

public void handleShow(IBinder windowToken) { // ... if (mView != mNextView) { // ... mView = mNextView; // ... mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); // ... mWM.addView(mView, mParams); // ... }}

handleShow()方法内部会调用WindowManager的addView()方法将Toast对应的View添加到Window中,Toast也就显示出来了。
到这里还没完,我们知道Toast一段时间后就会消失,那么Toast的消失是如何控制的呢,我们回到NotificationManagerService的showNextToastLocked()方法,在调用TN的show()方法显示出Toast后又调用了scheduleDurationReachedLocked()方法,我们来看一下这个方法做了什么。

private void scheduleDurationReachedLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r); long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; mHandler.sendMessageDelayed(m, delay); }

scheduleDurationReachedLocked()方法内部也是使用Handler发送了一条延时消息,延时的时间由Toast的显示时长决定,Toast.LENGTH_LONG对应的延时时间为LONG_DELAY,为3.5秒;Toast.LENGTH_SHORT对应的延时时间为SHORT_DELAY,为2秒,我们接着来看一下消息的处理:

@Overridepublic void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_DURATION_REACHED: handleDurationReached((ToastRecord) msg.obj); break; // ... }}private void handleDurationReached(ToastRecord record) { // ... int index = indexOfToastLocked(record.pkg, record.callback); if (index >= 0) { cancelToastLocked(index); } // ...}

接着调用了handleDurationReached()方法,方法内部首先获取当前Toast的索引,然后调用cancelToastLocked()方法,从方法名上我们也能猜到这个方法就是为了隐藏Toast,结合上面的延时消息,其实差不多就能清楚Toast是如何消失的了,我们来具体看一下cancelToastLocked()方法。

void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); // ... record.callback.hide(); // ... ToastRecord lastToast = mToastQueue.remove(index); // ... if (mToastQueue.size() > 0) { showNextToastLocked(); }}

cancelToastLocked()方法内部执行了record.callback.hide(),和此前的show()方法类似,这里同样是调用了TN的hide()方法。

@Overridepublic void hide() { // ... mHandler.obtainMessage(HIDE).sendToTarget();}mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { // ... case HIDE: { handleHide(); mNextView = null; break; } // ... } }};

接下来的步骤其实也和Toast的显示类似,最终会调用到handleHide()方法。

public void handleHide() { // ... if (mView != null) { // ... mWM.removeViewImmediate(mView); // ... mView = null; }}

handleHide()方法调用WindowManager的removeViewImmediate()方法将Toast对应的View从Window移除,Toast也就消失了。回到NotificationManagerService的cancelToastLocked()方法,在Toast隐藏后会将Toast对应的ToastRecord从mToastQueue中移除,如果此时mToastQueue的size大于0,则接着调用showNextToastLocked()方法显示下一个Toast。
到这里我们基本上就清楚了Toast的显示原理,不难看出Toast内部的TN对象扮演着重要的作用,Toast的显示和隐藏都是通过TN中的对应方法实现的。

Toast消失的原因

上文已经简单分析了Toast的显示和隐藏过程,下面我们就要回到文章开头提出的问题上,分析一下为什么Toast会消失。在上文enqueueToast()方法的分析中,我们只分析了index小于0也就是应用未显示Toast的情况,接下来我们来看一下index大于或等于0,对应应用已经显示Toast的情况。

if (index >= 0) { record = mToastQueue.get(index); record.update(duration); try { record.callback.hide(); } catch (RemoteException e) { } record.update(callback);}

首先获取到ToastRecord对象,这个对象就是对应着应用此时显示的Toast,更新它的duration和callback属性,注意这里在更新callback属性之前执行了record.callback.hide(),根据此前的分析,之后会隐藏当前显示的Toast。之后的过程是一样的,调用showNextToastLocked()显示Toast。
回到具体场景中,如果全局使用的是一个Toast对象,那么当然TN对象也是共用的,当第一个Toast还未消失时再次调用Toast的show()方法显示下一个Toast,由于此时Toast对应的ToastRecord对象还未从mToastQueue中移除,因此indexOfToastPackageLocked()方法获得的index等于0(这里不考虑多个Toast排队等待执行的情况,认为mToastQueue中只有一个ToastRecord对象),会调用到TN的hide()方法:

mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { // ... case HIDE: { handleHide(); // 将mNextView置为null mNextView = null; break; } // ... } }};public void handleHide() { // ... if (mView != null) { // ... // 将mView置为null mView = null; }}

执行到这里会导致第一个Toast消失,之后调用showNextToastLocked()方法显示第二个Toast,最终调用到TN的handleShow()方法:

public void handleShow(IBinder windowToken) { // ... if (mView != mNextView) { // ... mView = mNextView; // ... mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); // ... mWM.addView(mView, mParams); // ... }}

由于所有的Toast都对应一个TN对象,因此此时mView和mNextView均为null,不会执行mWM.addView(),Toast也就不会显示。
根据此前的分析,当Toast显示后会发送一条延时消息,根据makeText()方法传入的时长在一段时间间隔后隐藏Toast同时将ToastRecord从mToastQueue中移除,因此如果弹出第二个Toast时第一个Toast已经消失了,那么是可以正常显示的。
看到这里我们基本上就清楚了Toast无法显示的原因,不过想想全局使用一个Toast的方案很早就提出了,有很多人在用,如果一直都存在上述问题,不是早就应该有人提出了吗,我们不妨与其他版本的Toast源码对比一下。以Android 8.0(API Level 28)为例,我们来看一下NotificationManagerService的enqueueToast()方法:

@Overridepublic void enqueueToast(String pkg, ITransientNotification callback, int duration) { // ... final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg)); // ... ToastRecord record; int index = indexOfToastLocked(pkg, callback); if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } else { if (!isSystemToast) { // 限制一个应用弹出的Toast上限 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(); 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 (index == 0) { showNextToastLocked(); }}

对比Android 9.0中的enqueueToast()方法,最大的区别就是当应用已经显示Toast(对应index>=0)时,只调用了ToastRecord的update()方法,没有调用TN的hide()方法,因此就不会有第二个Toast不显示的问题。

如何解决问题

综合上面的所有分析,在Android 9.0中,当前应用在已经显示了Toast的情况下会调用TN的hide()方法,因此我们不需要全局使用一个Toast对象,每次直接执行Toast.makeText().show()就可以了,由于每一个Toast对象对应不同的TN对象,这样就不会因为mView==mNextView而导致Toast不显示。同时也正是由于调用了hide()方法,当显示下一个Toast时会隐藏当前正在显示的Toast,因此我们不必再自己处理Toast的重复显示问题,算是官方的优化吧。在Android 9.0以下版本,依然可以采用全局一个Toast的方案解决重复显示问题,不会造成Toast消失的问题。
但是还没完,正好前些天我的手机升级到了Android 10版本,我发现上文分析的Toast消失问题又不存在了,于是点开源码看了看,这不看不知道,一看吓一跳,NotificationManagerService中的enqueueToast()方法竟然又改回去了:

Android 9.0 Toast源码改变引发的问题 (4)

可以看出Android 10.0中又去掉了record.callback.hide()这行代码,因此表现上和Android 9.0以下版本一致,我们依然需要使用一个全局Toast来解决重复弹出的问题。我不得不吐槽一下,Android 9.0明明已经优化了Toast的重复弹出问题,为什么Android 10.0又给改回去了,这不是开历史倒车吗,而且这样改来改去对于开发者来说也是非常不友好。当然官方可能也有自己的考虑,由于我能力的不足而没有理解到,如果大家有自己的见解欢迎提出。
好了,最终完善后的示例代码如下:

private static Toast mToast = null;/** * 显示一个toast提示 * * @param context context 上下文对象 * @param text toast字符串 * @param duration toast显示时间 */public static void showToast(Context context, String text, int duration) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { Toast.makeText(context, text, duration).show(); } else { if (mToast == null) { mToast = Toast.makeText(context, text, duration); } else { mToast.setText(text); mToast.setDuration(duration); } mToast.show(); }}

我们来看一下运行效果:

OK,的确解决了Toast消失的问题。

总结

Toast在很多人看来可能都是再简单不过的了,每个项目中都会用,使用起来也很简单,这次算是我第一次了解Toast的原理,通过查阅相关文章,了解到Toast在使用时还存在一些需要注意问题,比如关闭通知权限导致Toast不显示、Android 7.1上Toast抛出BadTokenException异常等等问题,这里就不介绍了,可以自行了解一下。
限于个人水平的原因。关于Toast的运行原理分析得不是很详细,可能有些地方分析得不是很准确,如果有不对的地方欢迎大家提出。

最后编辑于

©

著作权归作者所有,转载或内容合作请联系作者

  • 人面猴

    序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...

    沈念sama阅读 174,493评论 5赞 416

  • 死咒

    序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...

    沈念sama阅读 73,611评论 2赞 334

  • 救了他两次的神仙让他今天三更去死

    文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...

    开封第一讲书人阅读 123,360评论 0赞 288

  • 道士缉凶录:失踪的卖姜人

    文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...

    开封第一讲书人阅读 47,406评论 0赞 245

  • 港岛之恋(遗憾婚礼)

    正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...

    茶点故事阅读 56,176评论 3赞 325

  • 恶毒庶女顶嫁案:这布局不是一般人想出来的

    文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...

    开封第一讲书人阅读 42,743评论 1赞 241

  • 城市分裂传说

    那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...

    沈念sama阅读 33,832评论 3赞 355

  • 双鸳鸯连环套:你想象不到人心有多黑

    文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...

    开封第一讲书人阅读 32,406评论 0赞 228

  • 万荣杀人案实录

    序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...

    沈念sama阅读 36,439评论 1赞 269

  • 护林员之死

    正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...

    茶点故事阅读 32,120评论 2赞 275

  • 白月光启示录

    正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...

    茶点故事阅读 33,773评论 1赞 288

  • 活死人

    序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...

    沈念sama阅读 29,889评论 3赞 284

  • 日本核电站爆炸内幕

    正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...

    茶点故事阅读 35,050评论 3赞 279

  • 男人毒药:我在死后第九天来索命

    文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...

    开封第一讲书人阅读 27,059评论 0赞 11

  • 一桩弑父案,背后竟有这般阴谋

    文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...

    开封第一讲书人阅读 28,223评论 1赞 232

  • 情欲美人皮

    我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...

    沈念sama阅读 38,151评论 2赞 309

  • 代替公主和亲

    正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...

    茶点故事阅读 37,849评论 2赞 313

推荐阅读更多精彩内容

  • IPC在Toast中的应用

    1、Toast概念和问题的引出 Toast 中文名"土司",应该算是 Android 使用频率很高的一个 widg...

    未见哥哥阅读 758评论 0赞 5

  • 简单源码分析之小小的Toast

    前言:toast再常见不过,但是一个小小的toast居然内有乾坤,呵(w)呵(t)呵(f) 源码如下: publi...

    super超_9754阅读 1,234评论 0赞 0

  • android Toast 吐司 源码分析

    什么是土司(Toast)? Toast是Android系统提供的一种非常好的提示方式,在程序中可以使用它将一些短小...

    一航jason阅读 1,035评论 0赞 1

  • Window的创建

    前言 上篇文章中讲到, Android中所有视图都是通过Window来呈现的, 如Activity, Dialog...

    海之韵Baby阅读 433评论 0赞 1

  • 由禅意想到

    禅意的生活是什么,一直以来总是觉得那应该是空灵的,脱离人间烟火气的,但其实那就是在生活中各种细节中与物与环境的一种...

    007玩设计的灰太狼阅读 528评论 0赞 0

Android 9.0 Toast源码改变引发的问题 (2024)
Top Articles
Latest Posts
Article information

Author: Van Hayes

Last Updated:

Views: 5771

Rating: 4.6 / 5 (66 voted)

Reviews: 81% of readers found this page helpful

Author information

Name: Van Hayes

Birthday: 1994-06-07

Address: 2004 Kling Rapid, New Destiny, MT 64658-2367

Phone: +512425013758

Job: National Farming Director

Hobby: Reading, Polo, Genealogy, amateur radio, Scouting, Stand-up comedy, Cryptography

Introduction: My name is Van Hayes, I am a thankful, friendly, smiling, calm, powerful, fine, enthusiastic person who loves writing and wants to share my knowledge and understanding with you.