科百科
当前位置: 首页 范文大全

okhttp框架怎么使用(OkHttp用了那么久你竟然不知道它的原理)

时间:2023-07-06 作者: 小编 阅读量: 1 栏目名: 范文大全

最近,很多朋友反映大厂的面试喜欢挖底层知识,像OkHttp这些都是必问的问题。OkHttp是一个高效的HTTP库:支持HTTP/2,允许对同一主机的所有请求共享一个套接字通过连接池可减少请求延迟支持GZIP压缩减少数据流量响应缓存可以完全避免网络重复请求静默恢复处理常见的连接问题…本文就以请求使用为入口,来深入学习下OkHttp。=null...}}OkhttpClient可以通过构造者配置参数来构建,也可以直接实例化,直接实例化其实也是内部调用构造者,只是传入的是默认builder。

最近,很多朋友反映大厂的面试喜欢挖底层知识,像OkHttp这些都是必问的问题。这里就给大家分享一篇非常有帮助的技术文吧。

OkHttp是一个高效的HTTP库:

  • 支持HTTP / 2,允许对同一主机的所有请求共享一个套接字
  • 通过连接池可减少请求延迟(如果HTTP / 2不可用)
  • 支持GZIP压缩减少数据流量
  • 响应缓存可以完全避免网络重复请求
  • 静默恢复处理常见的连接问题

本文就以请求使用为入口,来深入学习下OkHttp。

一、请求流程分析1. 同步请求

Okhttp同步GET请求使用:

// 新建一个Okhttp客户端(也可以通过OkHttpClient.Builder来构造)OkHttpClient client = new OkHttpClient();// 构造一个请求对象Request request = new Request.Builder().url(url).build();// 执行同步请求,返回响应Response response = client.newCall(request).execute();// 从响应体中获取数据String str = response.body().string();

先来瞧瞧构建OkhttpClient的源码:

open class OkHttpClient internal constructor(builder: Builder) : Cloneable, Call.Factory, WebSocket.Factory {//若直接实例化OkHttpClient,则调用主构造函数以默认Builder作为参数constructor() : this(Builder())// 通过builder中的值赋值@get:JvmName("dispatcher") val dispatcher: Dispatcher = builder.dispatcher@get:JvmName("connectionPool") val connectionPool: ConnectionPool = builder.connectionPool...class Builder constructor() {//分发器internal var dispatcher: Dispatcher = Dispatcher()//连接池internal var connectionPool: ConnectionPool = ConnectionPool()//应用拦截器集合internal val interceptors: MutableList<Interceptor> = mutableListOf()//网络拦截器集合internal val networkInterceptors: MutableList<Interceptor> = mutableListOf()//事件监听工厂internal var eventListenerFactory: EventListener.Factory = EventListener.NONE.asFactory()//连接失败是否重试internal var retryOnConnectionFailure = trueinternal var authenticator: Authenticator = Authenticator.NONE//是否跟随重定向internal var followRedirects = trueinternal var followSslRedirects = true//cookieinternal var cookieJar: CookieJar = CookieJar.NO_COOKIES//磁盘缓存internal var cache: Cache? = null//dnsinternal var dns: Dns = Dns.SYSTEM//代理设置internal var proxy: Proxy? = null...}}

OkhttpClient可以通过构造者配置参数来构建,也可以直接实例化,直接实例化其实也是内部调用构造者,只是传入的是默认builder。

再来看看OkhttpClient的newCall方法

Override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)

发现返回的是RealCall,接下来去RealCall中看后续的execute执行方法

override fun execute(): Response {//确认call没有执行过并置executed为true,否则抛出异常check(executed.compareAndSet(false, true)) { "Already Executed" }timeout.enter()callStart()try {//标记执行中client.dispatcher.executed(this)//通过拦截器链获取网络响应return getResponseWithInterceptorChain()} finally {//标记执行结束client.dispatcher.finished(this)}}

看起来比较精简,通过拦截器链获取网络响应,然后返回响应(拦截器链路在后续拦截器分析)。

2. 异步请求

Okhttp异步GET请求使用:

OkHttpClient client = new OkHttpClient();Request request = new Request.Builder().url(url).build();// 执行异步请求,通过回调返回响应client.newCall(request).enqueue(new Callback() {@Overridepublic void onFailure(@NotNull Call call, @NotNull IOException e) {}@Overridepublic void onResponse(@NotNull Call call, @NotNull Response response) Throws IOException {// 从回调中通过响应体获取数据String str = response.body().string();}});

异步请求流程大致和同步请求相似,但是最后的执行方法是enqueue,并传入回调对象。

我们来看看源码:

override fun enqueue(responseCallback: Callback) {//确认call没有执行过并置executed为true,否则抛出异常check(executed.compareAndSet(false, true)) { "Already Executed" }//监听回调callStart()//调用Dispatcher的enqueue方法,并传入一个AsyncCall对象client.dispatcher.enqueue(AsyncCall(responseCallback))}

内部调用了客户端的分发器的enqueue方法,并把AsyncCall(responseCallback)作为参数传入,AsyncCall是继承自Runnable,且是RealCall的内部类,我们先看Dispatcher.enqueue()方法

class Dispatcher constructor() {/** '异步准备执行'队列 */private val readyAsyncCalls = ArrayDeque<AsyncCall>()/** '异步正在执行'队列,包括取消但至今还没结束的 */private val runningAsyncCalls = ArrayDeque<AsyncCall>()/** ‘同步正在执行’队列*/private val runningSyncCalls = ArrayDeque<RealCall>()...internal fun enqueue(call: AsyncCall) {synchronized(this) {//加入准备执行队列readyAsyncCalls.add(call)...}// 执行promoteAndExecute()}private fun promoteAndExecute(): Boolean {this.assertThreadDoesntHoldLock()val executableCalls = mutableListOf<AsyncCall>()val isRunning: Booleansynchronized(this) {val i = readyAsyncCalls.iterator()while (i.hasNext()) {val asyncCall = i.next()//检查请求是否超过最大请求数if (runningAsyncCalls.size >= this.maxRequests) break//检查请求是否超过一个Host对应的最大请求数if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continuei.remove()asyncCall.callsPerHost.incrementAndGet()executableCalls.add(asyncCall)runningAsyncCalls.add(asyncCall)}isRunning = runningCallsCount() > 0}for (i in 0 until executableCalls.size) {val asyncCall = executableCalls[i]//调用AsyncCall的executeOn()方法asyncCall.executeOn(executorService)}return isRunning}}

可以从上面代码看出,就是将符合条件的调用从readyAsyncCalls队列提升到runningAsyncCalls,并调用 AsyncCall的executeOn() 方法,把线程池传入。

我们来看看AsyncCall:

inner class AsyncCall(private val responseCallback: Callback) : Runnable {...fun executeOn(executorService: ExecutorService) {client.dispatcher.assertThreadDoesntHoldLock()var success = falsetry {//使用线程池执行自己的run()方法executorService.execute(this)success = true} catch (e: RejectedExecutionException) {...//失败回调responseCallback.onFailure(this@RealCall, ioException)} finally {if (!success) {//标记结束client.dispatcher.finished(this) // This call is no longer running!}}}override fun run() {threadName("OkHttp ${redactedUrl()}") {var signalledCallback = falsetimeout.enter()try {//和同步请求一样,通过拦截器链获取网络响应val response = getResponseWithInterceptorChain()signalledCallback = true//回调成功responseCallback.onResponse(this@RealCall, response)} catch (e: IOException) {if (signalledCallback) {...} else {//回调失败responseCallback.onFailure(this@RealCall, e)}} catch (t: Throwable) {cancel()if (!signalledCallback) {...//回调失败responseCallback.onFailure(this@RealCall, canceledException)}throw t} finally {//标记结束client.dispatcher.finished(this)}}}}

使用线程池来执行自己,接下来就看run()方法,发现和同步请求一样,通过拦截器链获取网络响应,再调用回调对象的回调方法返回响应。

二、拦截器分析

请求大致流程知道了,我们来看看重头戏,拦截器链里面做了什么操作。

@Throws(IOException::class)internal fun getResponseWithInterceptorChain(): Response {// 建立一个拦截器列表val interceptors = mutableListOf<Interceptor>()// 用户设置的所有应用拦截器interceptors= client.interceptors// 处理错误恢复和重定向的拦截器interceptors= RetryAndFollowUpInterceptor(client)// 桥接拦截器,桥接应用层和网络层代码interceptors= BridgeInterceptor(client.cookieJar)// 缓存拦截器interceptors= CacheInterceptor(client.cache)// 服务器连接拦截器interceptors= ConnectInterceptorif (!forWebSocket) {// 用户设置的所有网络拦截器interceptors= client.networkInterceptors}// 服务器请求拦截器interceptors= CallServerInterceptor(forWebSocket)val chain = RealInterceptorChain(call = this,interceptors = interceptors,index = 0,exchange = null,request = originalRequest,connectTimeoutMillis = client.connectTimeoutMillis,readTimeoutMillis = client.readTimeoutMillis,writeTimeoutMillis = client.writeTimeoutMillis)var calledNoMoreExchanges = falsetry {//使用责任链模式开启链式调用val response = chain.proceed(originalRequest)if (isCanceled()) {response.closeQuietly()throw IOException("Canceled")}//返回响应return response} catch (e: IOException) {calledNoMoreExchanges = truethrow noMoreExchanges(e) as Throwable} finally {if (!calledNoMoreExchanges) {noMoreExchanges(null)}}}

我们在看下RealInterceptorChain的proceed方法:

@Throws(IOException::class)override fun proceed(request: Request): Response {...// 复制一个RealInterceptorChain,用于调用链中的下一个拦截器val next = copy(index = index1, request = request)val interceptor = interceptors[index]@Suppress("USELESS_ELVIS")// 调用下一个拦截器的intercept方法,获取response返回给上一个拦截器val response = interceptor.intercept(next) ?: throw NullPointerException("interceptor $interceptor returned null")...return response}

接下来我们来具体看看各个拦截器的作用

1. RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor 处理错误恢复和重定向,它会判断错误是否满足条件进行重试,还有根据返回的响应判断是否需要重定向请求。

class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {val realChain = chain as RealInterceptorChainvar request = chain.requestval call = realChain.callvar followUpCount = 0var priorResponse: Response? = nullvar newExchangeFinder = truevar recoveredFailures = listOf<IOException>()while (true) {// 初始化ExchangeFinder(后续ConnectInterceptor会用到ExchangeFinder来查找连接)call.enterNetworkInterceptorExchange(request, newExchangeFinder)var response: Responsevar closeActiveExchange = truetry {if (call.isCanceled()) {throw IOException("Canceled")}try {//执行下一个拦截器,获取响应response = realChain.proceed(request)newExchangeFinder = true} catch (e: RouteException) {...// 满足条件则重试continue} catch (e: IOException) {...// 满足条件则重试continue}// 赋上重定向之前的响应(响应体置空)if (priorResponse != null) {response = response.newBuilder().priorResponse(priorResponse.newBuilder().body(null).build()).build()}val exchange = call.interceptorScopedExchange//判断是否需重定向,若需则返回重定向请求val followUp = followUpRequest(response, exchange)//不需要重定向则直接返回responseif (followUp == null) {if (exchange != null && exchange.isDuplex) {call.timeoutEarlyExit()}closeActiveExchange = falsereturn response}val followUpBody = followUp.body// 若该请求只可传输一次,则返回响应if (followUpBody != null && followUpBody.isOneShot()) {closeActiveExchange = falsereturn response}response.body?.closeQuietly()//超过重定向最大次数则抛出异常if (followUpCount > MAX_FOLLOW_UPS) {throw ProtocolException("Too many follow-up requests: $followUpCount")}//将请求重新赋值为重定向的请求,继续循环,再次发送request = followUppriorResponse = response} finally {call.exitNetworkInterceptorExchange(closeActiveExchange)}}}...}

2. BridgeInterceptor

BridgeInterceptor 桥接应用层和网络层的代码,对用户的请求进行加工(如对请求头进行设置添加),也对网络响应做相应的处理(如解压服务端返回的 gzip 压缩数据)。

class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {// 获取用户请求val userRequest = chain.request()// 真正发送的网络请求的构建者val requestBuilder = userRequest.newBuilder()// 用户请求的请求体val body = userRequest.body// 对请求头的设置...var transparentGzip = falseif (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {transparentGzip = truerequestBuilder.header("Accept-Encoding", "gzip")}val cookies = cookieJar.loadForRequest(userRequest.url)if (cookies.isNotEmpty()) {requestBuilder.header("Cookie", cookieHeader(cookies))}if (userRequest.header("User-Agent") == null) {requestBuilder.header("User-Agent", userAgent)}// 执行下一个拦截器,获取网络响应val networkResponse = chain.proceed(requestBuilder.build())cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)val responseBuilder = networkResponse.newBuilder().request(userRequest)// 若因配置问题,服务端返回gzip压缩的数据,则做相应的解压缩if (transparentGzip &&"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&networkResponse.promisesBody()) {val responseBody = networkResponse.bodyif (responseBody != null) {// GzipSource对象,用于解压val gzipSource = GzipSource(responseBody.source())val strippedHeaders = networkResponse.headers.newBuilder().removeAll("Content-Encoding").removeAll("Content-Length").build()responseBuilder.headers(strippedHeaders)val contentType = networkResponse.header("Content-Type")responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))}}return responseBuilder.build()}}

3. CacheInterceptor

CacheInterceptor 承担着缓存的查找与保存的职责。根据策略判断是使用缓存还是走网络请求,对于返回的响应,满足条件则进行缓存。

class CacheInterceptor(internal val cache: Cache?) : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {val call = chain.call()val cacheCandidate = cache?.get(chain.request())val now = System.currentTimeMillis()// 检查缓存策略val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()// 若还需发送网络请求,则networkRequest不为空val networkRequest = strategy.networkRequest// 若存在可用缓存,则cacheResponse不为空val cacheResponse = strategy.cacheResponse...// 如果我们被禁止使用网络,并且无可用缓存,则返回失败if (networkRequest == null && cacheResponse == null) {return Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1).code(HTTP_GATEWAY_TIMEOUT).message("Unsatisfiable Request (only-if-cached)").body(EMPTY_RESPONSE).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build().also {listener.satisfactionFailure(call, it)}}// 如果不需要网络请求,缓存可用,则返回缓存if (networkRequest == null) {return cacheResponse!!.newBuilder().cacheResponse(stripBody(cacheResponse)).build().also {listener.cacheHit(call, it)}}...var networkResponse: Response? = nulltry {// 若无缓存可用,则执行下一个拦截器,获取响应networkResponse = chain.proceed(networkRequest)} finally {if (networkResponse == null && cacheCandidate != null) {cacheCandidate.body?.closeQuietly()}}// 如果我们还有缓存响应,且网络响应code为304,则更新缓存响应,并返回if (cacheResponse != null) {if (networkResponse?.code == HTTP_NOT_MODIFIED) {// 合并响应头、更新为网络请求时间和网络响应时间等val response = cacheResponse.newBuilder().headers(combine(cacheResponse.headers, networkResponse.headers)).sentRequestAtMillis(networkResponse.sentRequestAtMillis).receivedResponseAtMillis(networkResponse.receivedResponseAtMillis).cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build()networkResponse.body!!.close()cache!!.trackConditionalCacheHit()// 更新缓存cache.update(cacheResponse, response)return response.also {listener.cacheHit(call, it)}} else {cacheResponse.body?.closeQuietly()}}// 包装网络响应val response = networkResponse!!.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build()// 若用户配置了缓存if (cache != null) {// 判断是否满足缓存条件if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {// 将网络响应写入缓存,并返回val cacheRequest = cache.put(response)return cacheWritingResponse(cacheRequest, response).also {if (cacheResponse != null) {listener.cacheMiss(call)}}}// 根据请求方法判断是否为无效请求,是则从缓存移除相对应响应if (HttpMethod.invalidatesCache(networkRequest.method)) {try {cache.remove(networkRequest)} catch (_: IOException) {// cache无法被写}}}return response}}

4. ConnectInterceptor

ConnectInterceptor 主要是给网络请求提供一个连接,并交给下一个拦截器处理,这里还没有发送请求到服务器获取响应。在获取连接对象的时候,使用了连接池ConnectionPool来复用连接。

object ConnectInterceptor : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {val realChain = chain as RealInterceptorChain// 查找新连接或池里的连接以承载即将到来的请求和响应val exchange = realChain.call.initExchange(chain)val connectedChain = realChain.copy(exchange = exchange)return connectedChain.proceed(realChain.request)}}

ConnectInterceptor 看似代码很少,其实代码都在深处,看下initExchange方法

internal fun initExchange(chain: RealInterceptorChain): Exchange {...// codec(ExchangeCodec) 是一个连接所用的编码解码器,用于编码HTTP请求和解码HTTP响应val codec = exchangeFinder.find(client, chain)// result(Exchange)是封装这个编码解码器的一个工具类,用于管理ExchangeCodec,处理实际的 I/Oval result = Exchange(this, eventListener, exchangeFinder, codec)...return result}

ExchangeCodec持有连接,可通过其编码请求到服务端和获取服务端的响应并解码,我们依方法进入到最深处,看看是连接是如何获取的(代码已做简化处理)。

private fun findConnection(): RealConnection {// 1、复用当前连接val callConnection = call.connectionif (callConnection != null) {//检查这个连接是否可用和可复用if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {toClose = call.releaseConnectionNoEvents()}return callConnection} //2、从连接池中获取可用连接if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {val result = call.connection!!eventListener.connectionAcquired(call, result)return result}//3、从连接池中获取可用连接,通过一组路由routes(涉及知识点Http2多路复用)if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {val result = call.connection!!return result}route = localRouteSelection.next()// 4、创建新连接,进行tcp连接val newConnection = RealConnection(connectionPool, route)newConnection.connect// 5、再获取一次连接,在新建连接过程中可能有其他竞争连接被创建了,如可用防止浪费if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {val result = call.connection!!// 关闭刚刚创建的新连接newConnection.socket().closeQuietly()return result}//6、还是要使用创建的新连接,放入连接池,并返回connectionPool.put(newConnection)return newConnection}

5. CallServerInterceptor

CallServerInterceptor 是真正向服务器发起请求并获取响应的,它是拦责任链的最后一个拦截器,拿到响应后返回给上一个拦截器。代码已做简化(省略了很多条件判断和处理)。

class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {val realChain = chain as RealInterceptorChain// ConnectInterceptor获取到的,持有编码解码器val exchange = realChain.exchange!!val request = realChain.requestval requestBody = request.bodyval sentRequestMillis = System.currentTimeMillis()var responseBuilder: Response.Builder? = nulltry {// 写入请求头exchange.writeRequestHeaders(request)if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {if (...) {// 写入请求体val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()requestBody.writeTo(bufferedRequestBody)bufferedRequestBody.close()} else {...}} else {// 无请求体exchange.noRequestBody()}} catch (e: IOException) {...}try {if (responseBuilder == null) {// 读取响应头responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!if (invokeStartEvent) {exchange.responseHeadersStart()invokeStartEvent = false}}// 构建响应var response = responseBuilder.request(request).handshake(exchange.connection.handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build()var code = response.code// 读取响应体response = if (forWebSocket && code == 101) {response.newBuilder().body(EMPTY_RESPONSE).build()} else {response.newBuilder().body(exchange.openResponseBody(response)).build()}...return response} catch (e: IOException) {...}}}

总结

以上就是对OkHttp的源码解析,可以看出它是一个结构清晰的优质源码库,各个模块通过设计模式解耦。

总结下流程:首先通过OkHttpClient对象调用newCall方法得到RealCall实例,再通过调用RealCall的execute方法或enqueue方法,这两个方法最终都会调用到getResponseWithInterceptorChain方法,运用责任链模式,开始一层层传入各个拦截器,每个拦截器都有着自己的职责,最终在CallServerInterceptor发出请求并获取响应,然后层层返回响应。

    推荐阅读
  • 开题报告计划进度及内容(先要搞明白要求和标准)

    文献综述要求在叙述前人工作的同时,应有自己的看法和观点。文献综述的另一个作用就是为论证研究价值及其重要性提供了可比较的基础和参照。因此,在提交开题报告时,多尝试预估计是提高整个研究成功概率的不二法门。

  • 睡眠不足吃什么调理(哪些食物适合睡眠不好的人)

    睡眠不足吃什么调理睡前喝一杯牛奶能够有效的帮助睡眠,缓解失眠的症状,提高睡眠的质量。多吃一些桂圆,能够有效的养护心脏和脾脏,还能补血静心。对预防失眠、健忘有非常好的疗效。如果出现心情烦躁,多梦的现象,可以用莲子加盐煎煮后服用。睡不好的人可以补充一些糖水,因为糖水能在人的身体里变成血清素,能够有效的促进大脑平静,帮助睡眠。

  • 家庭装修等电位需要接什么地方(装修中怎么布置电位位置)

    家庭装修等电位需要接什么地方家庭装修正如谚语所说的:麻雀虽小,五脏俱全。如有帮助期望朋友们点赞、收藏。根据房屋格局情况,如果有过道区域,根据过道距离长短,建议在过道首尾布置双控开关,或布置过道灯电位。亦或顶部可以考虑人体感应灯。

  • 红薯和胡萝卜可以同食吗(胡萝卜的营养价值)

    胡萝卜的营养价值胡萝卜是一种质脆味美、营养丰富的家常蔬菜。胡萝卜中的主要营养素是β-胡萝卜素,它存在于胡萝卜的细胞壁中,而细胞壁是由纤维素构成,人体无法直接消化,唯有通过切碎、煮熟及咀嚼等方式,使其细胞壁破碎,β-胡萝卜素才能释放出来,为人体真正消化吸收利用。完好保持β-胡萝卜素等营养成分,与我们的食用与烹调方法有着极大的关系。

  • 宇宙第一速度是多少米每秒(第一宇宙速度介绍)

    以下内容希望对你有帮助!航天器沿地球表面作圆周运动时必须具备的发射速度,第一宇宙速度,也叫环绕速度,以下记为v1。按照力学理论可以计算出v1=7.9公里/秒。但在精确计算中,航天器在距离地面表面数百公里以上的高空运行,地球对航天器引力比在地面时要略小,故其速度也略小于v1。

  • 哄女朋友睡觉的小故事中篇(哄女友睡觉365个小故事)

    哄女朋友睡觉的小故事中篇《我的秘密只说给你听》小兔子在森林后面种了一大片的胡萝卜,胡萝卜地旁边还种着一小片的洋甘菊。终于,在胡萝卜成熟的时候,洋甘菊也盛开了。小兔子给森林的每个小动物都送了一份胡萝卜。不过送给小狐狸的这份胡萝卜,跟之前的都不一样。小兔子挑出了最大最红的胡萝卜,用上粉红色的精致礼盒,又给礼盒绑上了蓝色的蝴蝶结。路上,小兔子遇见了河马先生。

  • 大麦芽怎么炒(怎么炒大麦芽)

    大麦芽怎么炒将麦粒用水浸泡后,保持适宜温度、湿度。待幼芽长到0.5cm时,颜色为淡黄色,然后捞起干燥。将铁锅洗干净,擦干水,保证铁锅无油,将锅烧热后,加入生麦芽,翻炒均匀,待变色后起锅晾凉即可。麦芽是禾本科植物大麦的成熟果实经发芽干燥的炮制加工品。归脾、胃、肝经。麦芽的化学成分包括淀粉酶、大麦芽碱、磷脂、蛋白分解酶、麦黄酮、维生素B、葡萄糖、麦芽糖、维生素C等。其中,淀粉酶是主要发挥作用的成分。

  • 烟花三月下扬州烟花指的什么意思(烟花三月下扬州烟花含义)

    “烟花三月下扬州”中的“烟花”有三种意思第一种指的是“柳絮”,意思是柳絮如烟、繁花似锦;第二种指的是雾霭中的花,不特指某一种花;第三种指的是“琼花”,因为它洁白如玉,成片开起来也是白茫茫一片,所以被李白形容为烟花,今天小编就来聊一聊关于烟花三月下扬州烟花指的什么意思?接下来我们就一起去研究一下吧!烟花三月下扬州烟花出自《黄鹤楼送孟浩然之广陵》。

  • 该怎样培养孩子(从哪些方面入手呢)

    当孩子达到某个阶段时,他会表现出一些才能。培养孩子的外语能力。这是一种国际语言。考试需要进行测试。这要求我们每天观察孩子。环境因素非常重要。因此,有必要为孩子的道德成长,家长必须注意。监督孩子们锻炼身体。都说,身体是革命的本钱。拥有一个好身体是非常必要的。多运动,早上早点起床,和孩子一起跑步锻炼身体,养成早睡早起的好习惯。父母要作为孩子的榜样。孩子也会慢慢的跟着学,慢慢为良好的身体奠定基础。