1. suspendCoroutine作用及用法

suspendCoroutine 是 Kotlin 中用于挂起协程并与外部世界进行交互的函数之一。它的主要作用是允许你在协程中将异步非挂起的操作转化为挂起操作,从而可以方便地与协程上下文集成。具体来说,suspendCoroutine 的主要作用包括以下几点:

  1. 将回调式的异步操作转化为挂起操作:当你需要在协程中执行某些异步操作,例如调用回调函数,处理回调式 API,或等待某个事件发生时,suspendCoroutine 允许你将这些异步操作封装为挂起操作,使得你可以像调用普通的挂起函数一样调用它们,从而简化了异步编程的代码。

  2. 挂起当前协程:suspendCoroutine 在执行时会挂起当前协程,这意味着协程的执行会暂停,直到回调操作完成。这有助于防止阻塞线程,并允许其他协程在执行。

  3. 传递回调函数:suspendCoroutine 接受一个 Lambda 表达式,该 Lambda 表达式需要传递一个函数参数,通常是一个回调函数,该回调函数将在异步操作完成时被调用。在 Lambda 表达式内部,你可以使用 resume 来恢复协程的执行并传递结果,或者使用 resumeWithException 来传递异常,以便协程可以处理结果或错误。

suspendCoroutine案例

|

1
suspend fun main() {  	withContext(Dispatchers.IO) {  		val result = async { getResult() }.await()  		println("result=$result")  	}  }    suspend fun getResult() = suspendCoroutine<Boolean> {  	val callback = object : Callback {  		override fun onSuccess() {  			it.resume(true)  		}  	  		override fun onFail() {  			it.resumeWithException(Exception("fail"))  		}  	  		override fun onCancel() {  			it.resume(false)  		}  	}  	handleCallback(callback)  }    fun handleCallback(callback: Callback) {  	callback.onFail()  }    interface Callback {  	fun onSuccess()  	fun onFail()  	fun onCancel()  }

|

suspendCancellableCoroutine作用和用法

suspendCancellableCoroutine 是 Kotlin 中的一个函数,它允许你在一个协程中创建一个可取消的挂起操作,通常用于实现自定义的挂起函数或处理异步操作。基本功能和suspendCoroutine一样,只是多了一个支持监听外部协程取消和取消内部协程执行的能力。

suspendCancellableCoroutine案例

|

1
// 自定义的可取消挂起函数  suspend fun customCancelableOperation(): String {  	return suspendCancellableCoroutine { continuation ->  		val job = GlobalScope.launch {  			try {  				// 模拟异步操作  				delay(1000)  				// 如果操作成功,恢复挂起操作并传递结果  				continuation.resume("Operation completed successfully")  			} catch (e: CancellationException) {  				// 如果操作被取消,处理取消操作  				continuation.resumeWithException(e)  			} catch (e: Exception) {  				// 如果操作失败,传递异常  				continuation.resumeWithException(e)  			}  		}  		  		// 当外部协程取消时,取消内部协程  		continuation.invokeOnCancellation {  		job.cancel()  		}  	}  }    fun main() = runBlocking {  	val job = launch {  		val result = customCancelableOperation()  		println(result)  	}  	  	delay(500) // 等待一段时间  	job.cancel() // 取消外部协程  	job.join()  }

|

处理resumeWithException的通用方案

除了通过try catch的方式来捕获异常,也可以在launch协程的context中添加exceptionHandler来统一处理异常。 例如:

|

1
fun main() = runBlocking {  	val exceptionHandler = CoroutineExceptionHandler { _, exception ->  		println("Caught exception: $exception")  	}  	  	val job = GlobalScope.launch(exceptionHandler) {  		println(data)  	}  	  	job.join()  }

|

其他

在日常开发中,在协程上下文将异步非挂起的操作转化为挂起操作时遇到过多次回调完成的情况,导致一个continuation被多次resume,从而出现异常java.lang.IllegalStateException: Already resumed...

这时,可以通过使用suspendCancellableCoroutine并通过判断其状态决定是否需要真正resume来避免重复多次resume。

|

1
fun <T> Continuation<T>.safeResume(value: T) {  	if (this is CancellableContinuation) {  		if (isActive) {  			resume(value)  		} else {  			CapaLog.d("Continuation", "continuation is not active")  		}  	} else throw Exception("Must use suspendCancellableCoroutine instead of suspendCoroutine")  }

|

本文记录自定义ViewGroup实践中提炼出来的经验。

ViewGroup是啥?

ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity),当然还有margin等;于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ;决定childView的位置;为什么只是建议的宽和高,而不是直接确定呢,别忘了childView宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。

2、View的职责是啥?
View的职责,根据测量模式和ViewGroup给出的建议的宽和高,计算出自己的宽和高;同时还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形态。

3、ViewGroup和LayoutParams之间的关系?
大家可以回忆一下,当在LinearLayout中写childView的时候,可以写layout_gravity,layout_weight属性;在RelativeLayout中的childView有layout_centerInParent属性,却没有layout_gravity,layout_weight,这是为什么呢?这是因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等。如果大家去看LinearLayout的源码,会发现其内部定义了LinearLayout.LayoutParams,在此类中,你可以发现weight和gravity的身影。

2、View的3种测量模式
上面提到了ViewGroup会为childView指定测量模式,下面简单介绍下三种测量模式:

EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY;

AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST;

UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见。

注:上面的每一行都有一个一般,意思上述不是绝对的,对于childView的mode的设置还会和ViewGroup的测量mode有一定的关系;当然了,这是第一篇自定义ViewGroup,而且绝大部分情况都是上面的规则,所以为了通俗易懂,暂不深入讨论其他内容。

3、从API角度进行浅析
上面叙述了ViewGroup和View的职责,下面从API角度进行浅析。

View的根据ViewGroup传人的测量值和模式,对自己宽高进行确定(onMeasure中完成),然后在onDraw中完成对自己的绘制。

ViewGroup需要给View传入view的测量值和模式(onMeasure中完成),而且对于此ViewGroup的父布局,自己也需要在onMeasure中完成对自己宽和高的确定。此外,需要在onLayout中完成对其childView的位置的指定。

摘自《认知觉醒:开启自我改变的原动力》荐读笔记

读完《认知觉醒》2年,来交一份答卷。2021年9月9日,我在微信读书遇到《认知觉醒》,和大部分人一样,翻开书就被吸引了。
那时的我,32岁,大厂工作,但不太顺利。公司内斗,行业衰退,应该算是北上广的“中年危机”吧。站在那个时间点上,我很迷茫,很焦虑,好像被溺到深水里,拼命挣扎,渴望能探出水面,但又十分徒劳。
在人生的某些时刻,我曾有过同样的感受,比如大学毕业找不到工作,从小城市来北京后遭遇失业,只是前两次危机的顺利解决,让我很快松懈下来,并没有思考问题背后的本质。
以至于30岁的这次危机再次如期而至。当时读书很少,对人生、工作、生活的理解都很浅薄,所以也不知道应该怎么解决,只能闷着头走路,探索。
恰好遇到这本书,周岭老师在书中提到,「早读冥写跑」人生五件套,是每个人最基础的成长之道。
这给我一些启发,既然没什么方向,索性就从人生五件套开始改变自己吧,没想到竟然一路坚持下来,还慢慢养成了7个好习惯,形成了我自己的人生七件套。
所以,站在两年的这个节点,我想分享一下,读完这本书后自己的改变。
一来是对本书的反馈,我认为这是一种很好的习惯,书籍也好,文章也罢,绝大部分作者都渴望收到读者的回音。说到底,作者和读者是一种互相“驯化”的关系,如果没有回馈,这种关系就不会牢固,总会觉得缺点什么。
二来,是最近进入了新的平台期,感觉没什么动力。所以,想做个总结,看看这一路坚持而积累的成果,希望能激励自己,再出发。当然,如果你能看到这篇书评,并有幸也能从中获得一些力量,那这些文字就又多了一层意义。
我的人生七件套,「早读冥写跑复记」。
早:
这两年,我在坚持早起,每天早起1.5小时,累计早起516天,粗略估算一下,这让我多出了750+小时。我每天早起后会读书,几乎大部分时候都会进入心流状态,每次还没读过瘾,1个小时就过去了。如果按10个小时读完一本书的话,我大概利用早起的时间读了75本书。
开始早起前,我每天会睡到8点多,晚上熬夜,所以每次起床都很挣扎,起床后为了避免迟到,又急匆匆的挤地铁上班,一天下来迷迷糊糊,没有精气神。开始早起后,睡的也早了,我可以从容的起床、洗漱、吃早饭,还有1个多小时的读书学习时间,这种感觉让我每天都动力满满。
很多人觉得早起痛苦,但其实只需要一个转念。
你想,在每个清晨,阳光刚透过窗子,大部分人还在睡懒觉的时候,你已经早起洗漱完毕,倒上一杯热茶或咖啡,坐在书桌前,开始了一天的学习,这是一件多有意义的事情!
读:
这两年,我几乎每天都在阅读,累计阅读668天,读完了140+本,听完了100+本。一开始读的很慢,但渐渐越读越快,2021读了20多本,2022读了50本,今年的目标是102本,目前已经读了一多半。这些书都化成了我的养料,以各种各样的方式,支撑我前行。
毫不夸张的说,是阅读重塑了我,正是有了阅读,我才会打开认知,才会有了这两年持续的改变。
冥:
最近1年,我也开始学着冥想,累计冥想249天。冥想不是神秘的宗教仪式,而是一种锻炼专注能力、元认知能力的方法。练习冥想前,我的觉知能力很差,很容易生气,很容易愤怒,很容易沉浸在负面情绪里。
但练习冥想后,我的觉察能力大幅提升,现在一有负面情绪,就能很快意识到,并开始分析,这个情绪哪儿来的?
写:
这两年,我开始坚持写作,累计写作487天,写了100万+字,2021写了几万字,2022写了60万字,今年已经写了40多万字。每读完一本书就会写一篇书评,已经写了82篇。
一开始写的很烂、很慢,一篇2000字的文章要写4个小时,但慢慢就越写越快、越写越好了,现在一篇2000字的文章1个多小时就能写完。
而且,我明显感觉到,我的学习能力、思考能力、分析能力、表达能力都大幅提高。
更重要的是,我发现我很喜欢写作的感觉,虽然也有写不下去的痛苦,但更多时候会进入心流状态,内心被激情和愉悦填满。
周岭老师说,我们要拥有自己的作品,带着作品和别人社交,这是一种更高级的方式。你无需考虑如何说话,如何取悦他人,只要你持续写出有价值的作品,就会不断有人被你吸引,和你交朋友。
我开了公号,今年有了1万+粉丝,微读也有了1.3万关注,这就是持续写作的魅力。
跑:
这两年,也开始坚持运动了,累计运动472天。每天晚上我会做10分钟力量锻炼,俯卧撑、仰卧起坐、蹲起等等,没想到竟然练出了腹肌、胸肌和肱二头肌,虽然不是爆炸性的,但我感觉很好,再小的事情,只要坚持做,就会回报你结果,这也让我越来越有耐心。
我也开始跑步,2022跑了200公里,2023计划跑500公里,目前已完成350公里。一开始跑3公里我就会很喘,现在跑10公里都不觉得很累,身体素质明显变强了。更重要的是,感觉意志力也变强了,以前做事耐性很差,现在越来越能坚持了。
复:
这两年,也开始学会做每日复盘,已经累计复盘532天。2021复盘了81天,2022复盘了279天,今年已经坚持复盘200多天。
每天总结一下工作情况,写成功日记和感恩日记,在重要事件上进行深入反思。复盘给我带来最大的改变是,以前总喜欢抱怨别人,总觉得自己没问题,但现在一遇到事情,就会下意识思考,自己能做点什么。
人其实没办法改变别人,只能改变自己,向外求是不会有结果的,最好的办法是反求诸己。思考自己最终想要的结果是什么,然后承担责任,解决问题。
记:
这两年,也在坚持做时间记录,已经累计记录392天。每天记录时间开销,什么时间,做了什么事情,到晚上再做一个复盘总结。今天的时间都去哪儿了?有没有浪费时间?有没有使用不合理的地方?明天需要怎么调整。
这倒不是让自己成为一个工作狂魔,时间怎么分配由我自己决定,但不要让时间在自己毫不知情的情况下,被娱乐、短视频、资讯吸走。
做时间记录前,我对时间花到哪儿了,完全没有概念,但开始记录后,我对时间就敏感了很多,即使我被短视频吸走了,也能很快觉醒出来,而不会沉浸在里面一两个小时。或者说,是多了一种紧迫感,知道有更重要的事情等着我去做。
以上,就是我的人生七件套。现在回想,能有这些的改变,多亏了有阅读。
如果没有读《认知觉醒》,我就不会知道,阅读量<思考量<行动量<改变量,就不会通过阅读、思考和行动去改变自己,而仍旧沉浸在读完一本接一本书的快感里。
如果没有读《早起的奇迹》《把时间当作朋友》,我就不会知道,那些厉害的人都在早起学习,也就不会通过早起修炼自己,而是每天依旧在睡懒觉。
如果没有读《学会写作》《写作法宝》,我就不会知道,普通人改变自己命运最好的方式是写作,也就不会通过写作深度思考,而是每天仍旧过得浑浑噩噩。
如果没有读《运动改造大脑》《跑步治愈》,我就会不知道,坚持跑步有那么多意义,也就不会通过跑步锻炼身体,而是每天继续消耗自己。
如果没有读《十分钟冥想》《冥想》,我就不会知道,冥想能够培养人的觉察能力,也就不会通过冥想锻炼自己的大脑,而是继续被负面情绪控制。
如果没有读《复盘》《好好学习》我就不会知道,复盘能让人在犯错中成长,也就不会通过复盘反求诸己,而是仍旧在怨天尤人……
当然,这一切的起点都源于《认知觉醒》,在我最困难,最无助,最彷徨的时候,是这本书指引我走上改变之路。
所以,如果你想改变自己,不妨从阅读开始,从读《认知觉醒》开始,从“早读冥写跑复记”开始。
最后,假如你也和曾经的我一样,此时此刻刚好陷入困境,正饱受折磨,那么我想告诉你:“尽管眼下十分艰难,可日后这段经历说不定就会开花结果。”
这个世界有不少类似“缝隙”的地方,只要走运,找到适合自己的“缝隙”,就好歹能生存下去。
以前,我总想试图改变自己的命运,但这实在异常艰难。后来我想,一个人如果能找到自己的“缝隙”,以自己喜欢的方式生活,就已经是上天巨大的恩赐了。
因此,无论如何,请奋力前行。

Handler机制组成元素之间的关系

Handler机制主要有Handler、MessageQueue、Message、Looper几个元素构成。
它们之间的关系是:

  • 一个线程只有一个Looper实例

  • Looper中持有队列mQueue

  • Handler持有队列mQueue和Looper对象。在构造Handler实例时如果没有Looper入参,那就默认使用当前线程的Looper,ThreadLocal<Looper>.get()

  • Message中持有handler和next message

  • MessageQueue中持有当前message

  • 生产消息:Handler发送的信息通过MessageQueue.enqueueMessage将消息入队

  • 消费消息:Looper.loopOnce中将MessageQueue的消息取出,调用Message.target.dispatchMessage,target属性就标记了消息最终交给哪个Handler处理,所以这里的含义是在生产Msg的Handler中执行处理逻辑;如果MessageQueue信息为空,就会执行被挂起的IdleHandler。

dispatchMessage方法分析

dispatchMessage方法关系到消息队列中消息所对应的处理逻辑最终在哪如何被处理
title:android.os.Handle.dispatchMessage
|

1
/**  * Handle system messages here.  */  public void dispatchMessage(@NonNull Message msg) {  	if (msg.callback != null) {  		handleCallback(msg);  	} else {  		if (mCallback != null) {  			if (mCallback.handleMessage(msg)) {  				return;  			}  		}  		handleMessage(msg);  	}  }

|

方法有三个逻辑分支,都是处理MessageQueue抛出的Msg:

  1. msg.callback不为空
    message.callback.run() Message的callback成员是一个Runnable对象

  2. Handler.mCallback不为空
    由Handler.Callback的接口实现来处理

  3. msg.callback和Handler.mCallback都为空
    由Handler.handleMessage方法处理,子类没重写则默认不处理

Handler处理并发实现

title:android.os.MessageQueue.enqueueMessage消息入队
|

1
boolean enqueueMessage(Message msg, long when) {  	if (msg.target == null) {  		throw new IllegalArgumentException("Message must have a target.");  	}  	// 因为可能在任意对象操作入队,而只会在looper所绑定的线程出队,所以这里加对象锁,保证入队出队操作是线程安全的	synchronized (this) {  		if (msg.isInUse()) {  			throw new IllegalStateException(msg + " This message is already in use.");  		}  		  		if (mQuitting) {  			IllegalStateException e = new IllegalStateException(  			msg.target + " sending message to a Handler on a dead thread");  			Log.w(TAG, e.getMessage(), e);  			msg.recycle();  			return false;  		}  		  		msg.markInUse();  		msg.when = when;  		Message p = mMessages;  		boolean needWake;  		if (p == null || when == 0 || when < p.when) {  			// New head, wake up the event queue if blocked.			// 当前队列为空  			msg.next = p;  			mMessages = msg;  			needWake = mBlocked;		} else {  			// Inserted within the middle of the queue. Usually we don't have to wake  			// up the event queue unless there is a barrier at the head of the queue  			// and the message is the earliest asynchronous message in the queue.  			needWake = mBlocked && p.target == null && msg.isAsynchronous();  			Message prev; 			// 按调度时间调整队列位置 			for (;;) {  				prev = p;  				p = p.next;  				if (p == null || when < p.when) {  					break;  				}  				if (needWake && p.isAsynchronous()) {  					needWake = false;  				}  			}  			msg.next = p; // invariant: p == prev.next  			prev.next = msg;  		}  			  		// We can assume mPtr != 0 because mQuitting is false.  		if (needWake) {  			nativeWake(mPtr);  		}  	}  	return true;  }

|

title:android.os.MessageQueue.next消息出队
|

1
Message next() {  	// Return here if the message loop has already quit and been disposed.  	// This can happen if the application tries to restart a looper after quit  	// which is not supported.  	final long ptr = mPtr;  	if (ptr == 0) {  		return null;  	}  	  	int pendingIdleHandlerCount = -1; // -1 only during first iteration  	int nextPollTimeoutMillis = 0;  	for (;;) {  		if (nextPollTimeoutMillis != 0) {  		Binder.flushPendingCommands();  	}  	  	nativePollOnce(ptr, nextPollTimeoutMillis);  // 用来检查消息队列中是否有新的消息要处理,当队列为空时,`nativePollOnce` 会使线程等待直到:1. 有新消息到达。2. 被唤醒去处理其他任务(例如,定时事件、输入事件等)。3. 明确使用 `wakeUp()` 方法唤醒。	  	synchronized (this) {  		// Try to retrieve the next message. Return if found.  		final long now = SystemClock.uptimeMillis();  		Message prevMsg = null;  		Message msg = mMessages;  		if (msg != null && msg.target == null) {  			// Stalled by a barrier. Find the next asynchronous message in the queue.  			do {  				prevMsg = msg;  				msg = msg.next;  			} while (msg != null && !msg.isAsynchronous());  		}  		if (msg != null) {  			if (now < msg.when) {  				// Next message is not ready. Set a timeout to wake up when it is ready.  				nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);  			} else {  				// Got a message.  				mBlocked = false;  				if (prevMsg != null) {  					prevMsg.next = msg.next;  				} else {  					mMessages = msg.next;  				}  				msg.next = null;  				if (DEBUG) Log.v(TAG, "Returning message: " + msg);  				msg.markInUse();  				return msg;  			}  		} else {  			// No more messages.  			nextPollTimeoutMillis = -1;  		}  		  		// Process the quit message now that all pending messages have been handled.  		if (mQuitting) {  			dispose();  			return null;  		}  		  		// If first time idle, then get the number of idlers to run.  		// Idle handles only run if the queue is empty or if the first message  		// in the queue (possibly a barrier) is due to be handled in the future.  		if (pendingIdleHandlerCount < 0  			&& (mMessages == null || now < mMessages.when)) {  			pendingIdleHandlerCount = mIdleHandlers.size();  		}  		if (pendingIdleHandlerCount <= 0) {  			// No idle handlers to run. Loop and wait some more.  			mBlocked = true;  // 没有消息,休眠			continue;  		}  		  		if (mPendingIdleHandlers == null) {  			mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];  		}  		mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);  		}  		  		// Run the idle handlers.  		// We only ever reach this code block during the first iteration.  		for (int i = 0; i < pendingIdleHandlerCount; i++) {  			final IdleHandler idler = mPendingIdleHandlers[i];  			mPendingIdleHandlers[i] = null; // release the reference to the handler  			  			boolean keep = false;  			try {  			keep = idler.queueIdle();  			} catch (Throwable t) {  			Log.wtf(TAG, "IdleHandler threw exception", t);  			}  			  			if (!keep) {  			synchronized (this) {  			mIdleHandlers.remove(idler);  			}  		}  		}  		  		// Reset the idle handler count to 0 so we do not run them again.  		pendingIdleHandlerCount = 0;  		  		// While calling an idle handler, a new message could have been delivered  		// so go back and look again for a pending message without waiting.  		nextPollTimeoutMillis = 0;  	}  }

|

title:android.os.Looper
|

1
private static boolean loopOnce(final Looper me,  final long ident, final int thresholdOverride) {  	Message msg = me.mQueue.next(); // might block  	if (msg == null) {  		// No message indicates that the message queue is quitting.  		return false;  	}    	// This must be in a local variable, in case a UI event sets the logger  	final Printer logging = me.mLogging;  	if (logging != null) {  		logging.println(">>>>> Dispatching to " + msg.target + " "  		+ msg.callback + ": " + msg.what);  	}  	// Make sure the observer won't change while processing a transaction.  	final Observer observer = sObserver;  	  	final long traceTag = me.mTraceTag;  	long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;  	long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;  	if (thresholdOverride > 0) {  		slowDispatchThresholdMs = thresholdOverride;  		slowDeliveryThresholdMs = thresholdOverride;  	}  	final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);  	final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);  	  	final boolean needStartTime = logSlowDelivery || logSlowDispatch;  	final boolean needEndTime = logSlowDispatch;  		  	if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {  		Trace.traceBegin(traceTag, msg.target.getTraceName(msg));  	}  	  	final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;  	final long dispatchEnd;  	Object token = null;  	if (observer != null) {  		token = observer.messageDispatchStarting();  	}  	long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);  	try {  		msg.target.dispatchMessage(msg);  		if (observer != null) {  			observer.messageDispatched(token, msg);  		}  		dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;  	} catch (Exception exception) {  		if (observer != null) {  			observer.dispatchingThrewException(token, msg, exception);  		}  		throw exception;  	} finally {  		ThreadLocalWorkSource.restore(origWorkSource);  		if (traceTag != 0) {  			Trace.traceEnd(traceTag);  		}  	}  	if (logSlowDelivery) {  		if (me.mSlowDeliveryDetected) {  			if ((dispatchStart - msg.when) <= 10) {  				Slog.w(TAG, "Drained");  				me.mSlowDeliveryDetected = false;  			}  		} else {  			if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",  			msg)) {  				// Once we write a slow delivery log, suppress until the queue drains.  				me.mSlowDeliveryDetected = true;  			}  		}  	}  	if (logSlowDispatch) {  		showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);  	}  	  	if (logging != null) {  		logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);  	}  	  	// Make sure that during the course of dispatching the  	// identity of the thread wasn't corrupted.  	final long newIdent = Binder.clearCallingIdentity();  	if (ident != newIdent) {  		Log.wtf(TAG, "Thread identity changed from 0x"  		+ Long.toHexString(ident) + " to 0x"  		+ Long.toHexString(newIdent) + " while dispatching to "  		+ msg.target.getClass().getName() + " "  		+ msg.callback + " what=" + msg.what);  	}  	  	msg.recycleUnchecked();  	  	return true;  }    /**  * Run the message queue in this thread. Be sure to call  * {@link #quit()} to end the loop.  */  @SuppressWarnings("AndroidFrameworkBinderIdentity")  public static void loop() {  	final Looper me = myLooper();  	if (me == null) {  		throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");  	}  	if (me.mInLoop) {  		Slog.w(TAG, "Loop again would have the queued messages be executed"  		+ " before this one completed.");  	}  	  	me.mInLoop = true;  	  	// Make sure the identity of this thread is that of the local process,  	// and keep track of what that identity token actually is.  	Binder.clearCallingIdentity();  	final long ident = Binder.clearCallingIdentity();  	  	// Allow overriding a threshold with a system prop. e.g.  	// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'  	final int thresholdOverride =  	SystemProperties.getInt("log.looper."  	+ Process.myUid() + "."  	+ Thread.currentThread().getName()  	+ ".slow", 0);  	  	me.mSlowDeliveryDetected = false;  	  	for (;;) {  		if (!loopOnce(me, ident, thresholdOverride)) {  			return;  		}  	}  }

|

Handler的并发处理其实就是消息入队和出队被处理的过程

  1. 可以在任意线程将消息入队,具体线程由Handlder.sendMsg的方法栈决定

  2. 只会在Looper.loop方法中执行出队,而Looper.loop只会在指定的一个线程中执行的,也就是消息最终被处理的线程

  3. 可以看出入队和出队可能是在不同的线程中执行的,在MessageQueue中通过对象锁来保证线程安全

Hanlder与ANR的关系

消息阻塞机制

当主线程阻塞超过5s之后,就会触发ANR;前面我们知道,在Looper开启死循环取消息的时候,如果消息队列中没有消息的时候,就可能会被block,调用了nativePollOnce,那么为什么没有阻塞主线程呢?

其实应该把这分为两件事来看,looper.loop是用来处理消息,当没有消息的时候,主线程就休息了,不需要干任何事;像input事件,其实就是一个Message,当它加入到消息队列的时候,会调用nativeWake唤醒主线程,主线程来处理这个消息,只有处理这个消息超时,才会发生ANR,而不是死循环会导致ANR。

案例分析

title:”线程休眠”
|

1
"main" prio=5 tid=1 Native  | group="main" sCount=1 dsCount=0 flags=1 obj=0x7185b6a8 self=0xb400007375b4bbe0  | sysTid=3433 nice=0 cgrp=default sched=0/0 handle=0x749c9844f8  | state=S schedstat=( 800801640 66783841 881 ) utm=60 stm=19 core=0 HZ=100  | stack=0x7fc20cb000-0x7fc20cd000 stackSize=8192KB  | held mutexes=  native: #00 pc 000000000009ca68  /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)  native: #01 pc 0000000000019d88  /system/lib64/libutils.so (android::Looper::pollInner(int)+184)  native: #02 pc 0000000000019c68  /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+112)  native: #03 pc 0000000000112194  /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)  at android.os.MessageQueue.nativePollOnce(Native method)  at android.os.MessageQueue.next(MessageQueue.java:335)  at android.os.Looper.loop(Looper.java:183)  at android.app.ActivityThread.main(ActivityThread.java:7723)  at java.lang.reflect.Method.invoke(Native method)  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:997)

|

在我们分析ANR日志时,经常会看到这样表现,结合上面我们对于Handler的了解,这个时候其实就是没有消息了,我们看已经调用了nativePollOnce方法,此时主线程就休眠了,等待下一个消息到来。
title:”ANR堆栈”
|

1
"main" prio=5 tid=1 Blocked  | group="main" sCount=1 dsCount=0 flags=1 obj=0x7185b6a8 self=0xb400007375b4bbe0  | sysTid=3906 nice=-10 cgrp=default sched=0/0 handle=0x749c9844f8  | state=S schedstat=( 2591708189 61276010 2414 ) utm=220 stm=38 core=5 HZ=100  | stack=0x7fc20cb000-0x7fc20cd000 stackSize=8192KB  | held mutexes=  // ......   - waiting to lock <0x0167ghe6d> (a java.lang.Object) held by thread 5  // ...... 方法调用  at android.os.Handler.handleCallback(Handler.java:938)  at android.os.Handler.dispatchMessage(Handler.java:99)  at android.os.Looper.loop(Looper.java:223)  at android.app.ActivityThread.main(ActivityThread.java:7723)  at java.lang.reflect.Method.invoke(Native method)  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:997)

|

在这段堆栈中,我们看到主线程已经是出问题了,处于Blocked的状态,那么在Handler调用dispatchMessage方法的时候,是调用了handleCallback,说明此时是调用了post方法,在post方法中,主线程一直想要获取其他线程持有的一把锁,导致了超时产生了ANR。

日常开发中很多地方都见到了LayoutInflater.from().inflate()方法去将一个布局文件的内容填充为一个View,特别是inflate()这个方法,这个方法的参数有布局文件id,root和attachToRoot,那么这个root和attachToRoot参数有什么作用呢?

|

1
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {  	synchronized (mConstructorArgs) {  	View result = root;  	  	try {  		advanceToRootNode(parser);  		final String name = parser.getName();  		  		if (TAG_MERGE.equals(name)) {		// merge标签处理  			if (root == null || !attachToRoot) {  				throw new InflateException("<merge /> can be used only with a valid "  			+ "ViewGroup root and attachToRoot=true");  			}  		  			rInflate(parser, root, inflaterContext, attrs, false);  		} else {  			// Temp is the root view that was found in the xml  			final View temp = createViewFromTag(root, name, inflaterContext, attrs);  			  			ViewGroup.LayoutParams params = null;  			  			if (root != null) {  				// Create layout params that match root, if supplied  				params = root.generateLayoutParams(attrs);  				if (!attachToRoot) {  					// Set the layout params for temp if we are not  					// attaching. (If we are, we use addView, below)  					temp.setLayoutParams(params);  				}			} 			  			// Inflate all children under temp against its context.  			rInflateChildren(parser, temp, attrs, true);  			  			// We are supposed to attach all the views we found (int temp)  			// to root. Do that now.  			if (root != null && attachToRoot) {  				root.addView(temp, params);  			}  			  			// Decide whether to return the root that was passed in or the  			// top view found in xml.  			if (root == null || !attachToRoot) {  				result = temp;  			}  		}  	  	} catch (XmlPullParserException e) {  	} catch (Exception e) {  	} finally {  		// Don't retain static reference on context.  			}  	  	return result;  	}  }

|

分析root与attachToRoot的四种组合情况,可以得出以下结论:

|
root
| null
| null
| not null
| not null
|

|
attachToRoot
| true
| false
| true
| false
|

|

| 返回创建的不会带params的view,不作为root的子view
| 创建出来的View没有设置params就使用,所以被创建出来的布局,最外层设置的宽高是无效的,并最终返回了创建出来的View对象
| view带param添加到root中,返回root
| 返回创建的带params的view,不作为root的子view
|

  • root = null,无论attachToRoot取什么值都是一样的效果

Kotlin 的泛型和 Any 都是 Kotlin 语言中的重要概念,它们在处理类型和数据时有不同的用法和作用。下面详细说明它们的联系和区别:

联系

1.

类型参数的上界: 每个 Kotlin 的泛型类型参数都有一个默认的上界,即 Any?。这意味着如果你没有指定泛型类型参数的上界,默认情况下它可以是任何类型,包括 null

|

1
class Box<T>(val value: T)  // T 的默认上界是 Any?

|

2.

兼容性: 泛型类型参数可以指定上界为 Any,这意味着它只能接受非空类型。

|

1
class Box<T : Any>(val value: T)  // T 的上界是 Any,所以 T 不能是 null

|

区别

  1. 基本概念:

-

泛型(Generics):提供了类型参数化能力,使得类和函数可以处理多个不同的类型,而不需要在定义时指定具体的类型。

|

1
class Box<T>(val value: T)  // T 是一个泛型类型参数

|

-

AnyAny 是所有非空类型的根类型,相当于 Java 的 ObjectAny? 是所有类型(包括 null)的根类型。

  1. 类型安全和约束:
  • 泛型类型可以指定上界来约束它可以接受的类型。没有指定上界的泛型类型参数的上界默认是 Any?,即允许任何类型,包括 null

|

1
class Box<T : Number>(val value: T)  // T 的上界是 Number,所以只能接受数字类型

|

  • Any 只能接受非空类型,如果需要允许空值,应该使用 Any?

|

1
fun printValue(value: Any) {  // 只接受非空的任何值	println(value)}fun printValue(value: Any?) {  // 可以接受任何值,包括 null    println(value)}

|

  1. 类型擦除: Kotlin 的泛型在运行时类型信息会被擦除,类型参数信息只在编译期间有效。而 Any 是具体的类型,不会被类型擦除。

|

1
fun <T> isString(value: T): Boolean {   return value is String  // 会出现类型擦除的问题,此处 is String 仍然会编译通过但有警告} fun isString(value: Any): Boolean {   return value is String  // 不会被类型擦除,运行时仍然有效}

|

  1. 类型参数的使用: 泛型类型参数可以在类、接口和函数中使用,使得代码更加通用和灵活。

|

1
class Container<T>(val content: T) {	fun getContent(): T = content}fun <T> createContainer(item: T): Container<T> {	return Container(item)}

|

总结来说,Kotlin 的泛型提供了类型参数化的能力,使代码更灵活和可重用,而 Any 是所有非空类型的根类型,提供了对象类型的基本功能。泛型在编译期间处理,而 Any 在运行时也有效,两者在处理类型时有不同的侧重点和用法。

开闭原则 OCP(Open-Closed Principle)

对扩展开放,对修改关闭

根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。

扩展开放:某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
修改关闭:某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求模块是修改关闭的

开闭原则的实现方法
为了满足开闭原则的对修改关闭原则以及扩展开放原则,应该对软件系统中的不变的部分加以抽象,在面向对象的设计中

  • 可以把这些不变的部分加以抽象成不变的接口,这些不变的接口可以应对未来的扩展;

  • 接口的最小功能设计原则。根据这个原则,原有的接口要么可以应对未来的扩展;不足的部分可以通过定义新的接口来实现;

  • 模块之间的调用通过抽象接口进行,这样即使实现层发生变化,也无需修改调用方的代码。

接口可以被复用,但接口的实现却不一定能被复用。
接口是稳定的,关闭的,但接口的实现是可变的,开放的。
可以通过对接口的不同实现以及类的继承行为等为系统增加新的或改变系统原来的功能,实现软件系统的柔性扩展。

迪米特法则(最少握手) LoD(Law Of Demeter or Principle of Least Knowledge)

一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。

里氏替换原则 LSP(Liskov Substitution Principle)

任何超类出现的地方可以用任一派生类进行替换,实践中可用以校验继承是否有必要以及是否合法

只有满足以下2个条件的OO设计才可被认为是满足了LSP原则:

  • 不应该在代码中出现if/else之类对派生类类型进行判断的条件。

  • 派生类应当可以替换基类并出现在基类能够出现的任何地方,或者说如果我们把代码中使用基类的地方用它的派生类所代替,代码还能正常工作。

同时LSP体现了:

  • 类的继承原则:如果一个派生类的对象可能会在基类出现的地方出现运行错误,则该派生类不应该从该基类继承,或者说,应该重新设计它们之间的关系。

  • 动作正确性保证:从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入新的错误。

重构违反LSP的设计

如果两个具体的类A,B之间的关系违反了LSP 的设计,(假设是从B到A的继承关系),那么根据具体的情况可以在下面的两种重构方案中选择一种:

  • 创建一个新的抽象类C,作为两个具体类的基类,将A,B的共同行为移动到C中来解决问题。

  • 从B到A的继承关系改为关联关系。

单一职责原则  SRP(Single Resposibility Principle)

永远不要让一个类存在多个改变的理由。

要控制类的粒度大小,将对象解耦,提高其内聚性,即一个对象不要承担太多职责,一个方法尽量做好一件事,提高其原子性。

接口隔离原则 ISP(Interface Insolation Principle)

不能强迫用户去依赖那些他们不使用的接口。

  • 按分类设计接口,避免耦合

依赖倒置原则  DIP(Dependency Inversion Principle)

A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
B. 抽象不应该依赖于细节,细节应该依赖于抽象
C.针对接口编程,不要针对实现编程

符合DIP原则的操作:

  • 在高层模块与低层模块之间,引入一个抽象接口层

  • 避免依赖传递

组合/聚合复用原则

尽量使用组合/聚合,不要使用类继承

  • 厘清 is-a 与 has-a,区别继承与组合

  • 复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

在面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。

什么时候才应该使用继承
只有当以下的条件全部被满足时,才应当使用继承关系:

1)派生类是基类的一个特殊种类,而不是基类的一个角色,也就是区分”Has-A”和”Is-A”。只有”Is-A”关系才符合继承关系,”Has-A”关系应当用聚合来描述。
2)永远不会出现需要将派生类换成另外一个类的派生类的情况。如果不能肯定将来是否会变成另外一个派生类的话,就不要使用继承。
3)派生类具有扩展基类的责任,而不是具有置换掉(override)或注销掉(Nullify)基类的责任。如果一个派生类需要大量的置换掉基类的行为,那么这个类就不应该是这个基类的派生类。
4)只有在分类学角度上有意义时,才可以使用继承。

思考:如何培养code sense

  • 多写多想,从实践中摸索总结

  • 注重代码设计,做好设计再coding

  • 提升bad code识别能力,如重复代码的出现、hard code

  • 基于OOP原则,内化设计模式且与其常用场景结合消化,训练强化的结果

参考自:面向对象设计的七大设计原则详解

Kotlin 的泛型系统支持定义上界和下界约束,包括协变和逆变。这些特性使得 Kotlin 的泛型类型系统更加灵活和强大。下面是对 Kotlin 泛型上界和下界的详细说明:

泛型的上界 (Upper Bounds)

上界用于限制泛型类型参数的类型范围。默认情况下,泛型类型参数具有一个隐式的上界 Any?。你可以通过显式声明上界来进一步限制类型参数的类型范围。

示例

1.

默认隐式上界

|

1
class Box<T>(val value: T)  // T 的隐式上界是 Any?

|

2.

显式上界

|

1
class Box<T : Number>(val value: T)  // T 的上界是 Number,因此只能接受 Number 的子类型

|

3.

多重上界: 如果需要使用多个上界,可以使用 where 子句来进行声明。

|

1
fun <T> ensureAllNonNull(vararg args: T) where T : Any, T : Comparable<T> {    for (arg in args) {        checkNotNull(arg)        println(arg)    }}

|

泛型的下界 (Lower Bounds)

Kotlin 不直接提供显式声明下界的语法,但下界的概念可以通过协变和逆变来实现。协变和逆变是用于描述类型参数之间的子类型关系的概念,它们定义了类型参数在子类型化关系中的行为。

协变 (Covariance)

协变允许使用子类型来替代泛型类或接口的类型参数。在 Kotlin 中,协变使用关键字 out 来定义,表示泛型类型参数只能出现在输出位置(返回类型),不能出现在输入位置(函数参数)。

|

1
interface Source<out T> {    fun nextT(): T}fun demo(source: Source<String>) {    val anySource: Source<Any> = source // 协变,String 是 Any 的子类型,所以 Source<String> 可以赋值给 Source<Any>}

|

逆变 (Contravariance)

逆变允许使用超类型来替代泛型类或接口的类型参数。在 Kotlin 中,逆变使用关键字 in 来定义,表示泛型类型参数只能出现在输入位置(函数参数),不能出现在输出位置(返回类型)。

|

1
interface Consumer<in T> {    fun consume(item: T)}fun demo(consumer: Consumer<Number>) {    val intConsumer: Consumer<Int> = consumer // 逆变,Int 是 Number 的子类型,所以 Consumer<Number> 可以赋值给 Consumer<Int>}

|

使用示例

以下是一个综合示例如 Box 类和函数如何利用泛型的上界和协变逆变:

|

1
// 定义一个具有上界的泛型类class Box<T : Number>(val value: T)fun <T : Number> sum(value1: T, value2: T): Double {    return value1.toDouble() + value2.toDouble()}// 定义一个协变接口interface Source<out T> {    fun nextT(): T}// 定义一个逆变接口interface Consumer<in T> {    fun consume(item: T)}fun main() {    // 使用具有上界的泛型类    val intBox = Box(1)    val doubleBox = Box(1.0)    println(sum(1, 2))         // 输出: 3.0    println(sum(1.0, 2.0))     // 输出: 3.0    // 示例协变    val stringSource: Source<String> = object : Source<String> {        override fun nextT(): String {            return "Hello"        }    }    val anySource: Source<Any> = stringSource    println(anySource.nextT())  // 输出: Hello    // 示例逆变    val numberConsumer: Consumer<Number> = object : Consumer<Number> {        override fun consume(item: Number) {            println("Consumed $item")        }    }    val intConsumer: Consumer<Int> = numberConsumer    intConsumer.consume(10)  // 输出: Consumed 10}

|

总结

  • 上界:限制泛型类型参数的类型范围,默认是 Any?,可以显式地使用 :<类型> 来指定。

  • 下界:Kotlin 没有直接的下界语法,但可以通过协变(out)和逆变(in)来实现特定的行为。

  • 协变:使用 out 关键字表示,只能用于返回类型,允许将子类型泛型对象赋值给超类型的变量。

  • 逆变:使用 in 关键字表示,只能用于输入类型,允许将超类型泛型对象赋值给子类型的变量。

近期实现的一个功能中,由于后台需要的请求入参数据量过大,而req的载体有大小限制,所以采用了先使用pb文件存储数据上云,云文件索引作为req入参的方案。
本文记录其中使用pb与Java互转的过程。

前言

protobuf是谷歌推出的序列化协议,有比json所占字节少体积小、序列化传输快的特点。再加上其同样有平台无关的特性,protobuf得以广泛应用。

常见的使用场景:

  1. 前端与后端的通信数据载体

  2. 多端复用的数据结构,如草稿库

  3. 作为轻量级文件存储数据(本次的使用场景)

ProtoBuf转成Java过程记录

编写proto文件

|

1
syntax = "proto2";    package demo;    option java_multiple_files = true;  option java_package = "com.darrenyuen.demo.protos";  option java_outer_classname = "DemoProtos";    message Person {  optional string name = 1;  optional int32 id = 2;  optional string email = 3;    enum PhoneType {  	MOBILE = 0;  	HOME = 1;  	WORK = 2;  }    message PhoneNumber {  optional string number = 1;  optional PhoneType type = 2 [default = HOME];  }    repeated PhoneNumber phones = 4;  }    message AddressBook {  	repeated Person people = 1;  }

|

命令编译

proto语法是跨平台的,所以我们需要需要对应平台的编译器工具,编译成java文件,比如笔者在MacOs下,需要下载这个平台的编译工具。

|

1
protoc --java_out=输出目录 编译文件 -I=编译文件所在的文件夹# 编译.proto文件为Kotlin文件# protoc --proto_path=src/test/proto --kotlin_out=src/test/kotlin src/test/proto/addressbook.proto

|

  • protoc:Protobuf 编译器的命令行工具,用于编译 Protobuf 文件。

  • --java_out=输出目录:指定生成的 Java 代码的输出目录。编译器会将生成的代码放置在这个目录下。

  • 编译文件:Protobuf 文件的路径,这些文件包含了消息定义。

  • -I=编译文件所在的文件夹:指定 Protobuf 文件所在的目录。编译器会在这个目录下搜索指定的文件。
    执行编译后,便可拷贝生成java文件到工程中使用

集成插件编译

  1. 集成gradle插件
    |
1
// 根目录 build.gradle文件,引入插件buildscript {repositories {  	mavenCentral()  }    dependencies {  	classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18'  	}  }

|
``
|

1
// app目录 build.gradle 引入pb-java库implementation 'com.google.protobuf:protobuf-java:3.8.0'

|

|

1
// app目录 build.gradle 声明使用插件plugins {   	id 'com.google.protobuf'  }

|

  1. 编译配置
    |
1
// app目录 build.gradlesourceSets {  	main {   		proto {  			srcDir 'src/main/proto'  		}  	}  }protobuf {  	protoc {  		artifact = 'com.google.protobuf:protoc:3.18.1'  	}  	generateProtoTasks {  		all().each { task ->  			task.builtins {  				java {  					option "lite"  				}  				// Generates Python code  				// python { }  				// Generates Kotlin code 				// kotlin { }			}  		}  	}  }

|

  1. 使用结果文件
    编译后,结果文件会生成并存储在{PROJECT_ROOT}/app/build/generated/source/proto中,可拷贝到工程中使用

Java数据序列化成pb文件

|

1
val filePath = "dir/${System.currentTimeMillis()}.pb"  var outputStream: FileOutputStream? = nulltry {  	val demoParam = addressBookBuilder.build()  	FileUtils.createOrExistsFile(File(filePath))  	outputStream = FileOutputStream(filePath)  try {  	demoParam.writeTo(outputStream)  } catch (t: Throwable) {    }  } catch (t: Throwable) {   } finally {  	outputStream?.close()  }

|

参考文章:
如何在Android上从零使用Protobuf
适用于 Gradle 的 Protobuf 插件

0%