香菇蛋白質含量豐富!每天吃對身體有好處「脂肪低控制體重」教你如何挑選好香菇

3{icon} {views}

香菇味道鮮美,香氣沁人,營養豐富。香菇素有山珍之王之稱,是高蛋白,低脂肪的營養保健食品,香菇還含有多種維生素、礦物質、對促進人體新陳代謝,提高機體適應力有很大作用。香菇中還含有一種抗病毒的乾擾素誘發劑,能提高人體抗病能力,可預防流行性感冒等症。香菇具有和胃、健脾、補氣益腎的功效,還能促進食慾。所以家裡要經常吃些香菇。

新鮮香菇口感更嫩,有一股清香的味道,所以一般會用來清炒。而乾香菇由於含有一種鮮味物質5』-鳥苷酸,所以它的風味非常濃郁,一般會用來熬湯或者製作燉菜。

營養師發現,如今很多人吃東西都會把營養放在第一位,所以他們往往會更偏向於吃新鮮香菇。但實際上,他們都錯了,因為乾香菇的營養價值,和新鮮香菇是差不多的:


從蛋白質的角度來看,正常大小的一個乾香菇約為2克,而新鮮菇則為2.2克;從脂肪的角度來看,每個乾香菇約為0.12克,而新鮮香菇則為0.3克。

從碳水化合物的角度來說,每個乾香菇約為6.17克,而新鮮香菇為5.2克;從鐵元素的角度來看,每個乾香菇為11.3毫克,而新鮮香菇為10.5毫克。


所以總的來說,新鮮香菇和乾香菇營養價值差別不大。在某些方面乾香菇反而還會更好,比如說脂肪含量更低,這是很多控制體重人士非常在乎的一點。

當然啦,新鮮香菇的水分肯定更足,不過這算不上什麼「營養優勢」,因為想要補水的話,大家直接喝水就可以了。


因此,如果真要營養師在兩者中選一個,我們更推薦大家吃乾香菇。畢竟營養差不多,但一般來說,乾香菇的口感會更好,而且價格相對來說更便宜一點。

不過最近也有粉絲問我們,香菇泡水後顏色非常深,網上一查說可能是二氧化硫超標,這是真的嗎?


必須要承認,市面上確實出現過二氧化硫超標的香菇,但隨著監管越來越嚴,基本已經不存在了。而如果二氧化硫超標的話,會有一股刺鼻的味道,大家其實可以很輕易分辨出來。

至於香菇泡水的顏色很深,往往和二氧化硫沒有什麼關係。相信懂點化學的人也都知道,二氧化硫雖然可以溶於水,但是它是無色的,根本不會改變水的顏色。


實際上,香菇泡水變色,是一件十分正常的事情。因為各種香菇的生長條件是不一樣的,所以晾曬過程和加工時間也都不一樣,它們裡面含有的雜質和色素就會不同,就會導致香菇泡出不同深淺顏色的水。當雜質和色素的含量較高的時候,泡香菇水的顏色就會比較深。


下面給大家介紹一道鮮香菇的做法:

香菇這樣搭配做,又香又鮮,簡單幾步就做好,不錯的下飯菜

今天分享的這道香菇的做法,是和肉末和韭菜一起炒,韭菜又嫩又鮮,混合著香菇的香味,肉末的肉香味,吃著非常鮮美下飯,喜歡吃香菇的不要錯過這個做法,簡單又實用,做法也簡單,只需簡單幾步就能做好,是不錯的下飯菜,下面分享下做法:

【肉末香菇炒韭菜】

材料:鮮香菇300克、韭菜1把、豬肉100克、油鹽適量、薑1片、大蒜2瓣、醬油1湯匙、料酒1\2湯匙、

做法:

1.準備好所需材料,韭菜擇去老的葉子,投洗乾凈,香菇切去根部不要,投洗乾凈

2.把香菇切成薄厚均勻的片

3.韭菜切成寸段,豬肉切成末,薑切末,大蒜切片

4.鍋內加入油燒熱,下入薑末和蒜片爆香,再下入肉末小火煸炒,將肉末炒散開,炒至顏色變白

5.淋入料酒和醬油,將肉末翻炒上色,料酒有去除豬肉腥味的作用

6.下入香菇翻炒1-2分鐘,和肉末一起翻炒均勻

7.下入韭菜,鹽,快速翻炒均勻,炒至韭菜葉稍變軟,關火,盛出裝盤即可

小貼士:香菇炒之前,焯下水,可以去除雜質和異味,使香菇的味道更純正。韭菜很容易熟,不要炒過火,炒過火,失去了韭菜的鮮美的風味了。


via

本站聲明:網站內容來源http://www.look543.com,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

聽過「電子菸」嗎?想知道與一般傳統香菸有何不同嗎?

電子煙能幫助戒菸嗎?專家學者以健康觀點帶您來了解 !

新手該如何選擇電子菸口味及濃度呢?

秋冬季節吃柿子「脆柿子」和「軟柿子」有區別嗎?不管哪種「別吃生柿子 」

3{icon} {views}

秋天正是柿子豐收的季節,柿子的營養價值很高。柿子還可以做成柿子餅的一些農副產品,大多數人都是很喜歡吃柿子的。

「金桂飄香柿子紅,霜降採摘正秋風。」柿子正在市場上活躍,是秋冬季節的一抹亮色。

一般在市場上有兩種柿子,一種是軟柿子,成熟的軟柿子裡面果肉是柔軟的,味道比較甜,可以用勺子挖著吃,另一種是脆柿子,成熟之後果肉仍然比較硬,吃起來清脆爽口,甜而不膩,口感像蘋果,澀味也更淡一些。那麼買哪種柿子更好呢?


在運輸儲藏的過程中,脆柿子具有天然的優勢,更加容易保存,而軟柿子可能在採摘或者運輸的過程中出現擠爛的情況,沒法運輸到太遠的地方。

甚至因為軟柿子本身就是一個易熟的水果,要是放置的時間過長,就會導致腐爛,所以軟柿子要求更嚴格,導致市場更小,售價稍貴。

在成分上來說,這兩種柿子主要的區別是果膠的形態。

果膠是一種多糖,就像它的名字一樣,在植物中與纖維素、半纖維素等黏在一起,形成緊密的結構。當一般的軟柿子成熟時,果膠酶就會將果膠不斷地降解,果實就會慢慢變軟。


而市場上的硬柿子,一種是可以自動脫澀的品種,成熟了也不會變軟,另一種是需要人工脫澀,同時抑制了果膠酶的活動,讓果膠分解不了,柿子的果肉就會更緊實,因此吃起來是硬的。

其他營養成分來說差別不是很大,所以大家挑選柿子的時候,根據自己的喜好來就好,喜歡軟甜的,而且牙口不是很好,就吃軟柿子,喜歡脆嫩,吃著方便不臟手的,就選脆柿子。


還要提醒大家,不管吃什麼樣的柿子,最好等完全成熟了以後再食用,而且不要吃柿子皮,因為未成熟的柿子,尤其是柿子皮中含有大量的單寧、鞣酸等成分。


過多的鞣酸就會和蛋白質結合,形成鞣酸蛋白,在胃酸過多的情況下,就會與柿子的果膠、纖維等混在一起,形成硬塊。

如果這些硬塊不能通過幽門到達小腸,就會滯留在胃中形成結石,也叫胃柿石。胃柿石無法自然被排出的話,就會造成消化道梗阻的情況,出現上腹部劇烈疼痛、嘔吐、甚至嘔血等癥狀。


所以如果大家遇到特別澀的柿子,應該放一放或者扔掉,不要因為怕浪費就勉強繼續吃下去,小心對身體造成傷害。


吃柿子推薦一次一個,每日最多2~3個

柿子的含糖量是較高的,100g柿子就能提供17g左右的碳水化合物,和很多常見水果相比,含糖量都遙遙領先,所以柿子不適合多吃,特別對於有糖尿病、高血糖的糖友來說更應當控制攝入量。推薦一次就吃100g左右柿子即可,一天不要超過2~3個。水果雖然礦物質、膳食纖維、植物色素(抗氧化成分)豐富,但它們並不是多多益善的食物,膳食指南中也明確推薦了,水果每日的攝入量最好在200~350g,吃多了可能會造成糖分攝入過多,營養不均衡的問題。

自身消化能力弱、胃腸有嚴疾的群體不多吃柿子

傳統醫學上經常提到:柿子性寒,不能多吃,否則會引起腹瀉,孕婦吃多了還會流產,「宮寒」的人吃了會痛經等等。柿子還真是有點冤,其實柿子就是因為水分多,鞣酸含量偏高,對粘膜的刺激會更大一些,可能會更容易引起腸胃道的頻繁蠕動,導致排便反應,另外,這水果的「寒、溫」性質個人認為是因人而異的,可能部分人群吃了柿子的確會出現腸胃不適的情況,那麼柿子可能對他們來說是「涼性」,不過也有不少人吃了柿子沒什麼特殊情況的,那麼柿子可能對他們來說就是「平性」的,總之,不要過度受到傳統教唆,根據自身體質來判斷就好。

但是要注意如果自身消化能力弱,腸胃虛弱,已有胃潰瘍、胃出血、腸梗阻等問題的人群,如果過多吃柿子的話,可能對粘膜更有刺激,若胃部總有隱痛,那就別多吃柿子。

和高蛋白食物一起吃的話,不要過量

前文提到「胃柿石」的問題,因為這個問題,很多宣傳都不讓柿子和「富含高蛋白的食物」一同食用,其實胃柿石的說法的確是真實的,不過一般來說,鞣酸和蛋白質在胃中的反應程度是極小的,只要這些沉澱順利代謝,它們並不會堆積在腸胃道中。只是還是那句話,不怕一萬隻怕萬一,對於有特殊情況的中老年消化能力弱,或有胃腸疾病的患者來說柿子和高蛋白食物(如牛奶、雞蛋、海鮮)一同吃的時候,柿子不要多吃,可以間隔一段時間再吃,普通人群的話只要不過多頻繁地吃柿子,問題不大。對於每個人的消化能力、腸胃反應情況來說都各有不同,只要我們控制攝入量,引起胃柿石的幾率也不大。


via

本站聲明:網站內容來源http://www.look543.com,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※聽過「電子菸」嗎?想知道與一般傳統香菸有何不同嗎?

電子煙能幫助戒菸嗎?專家學者以健康觀點帶您來了解 !

※新手該如何選擇電子菸口味及濃度呢?

家裡大掃除「白牆髒了」怎麼辦?家政阿姨教我一招「不用水也能去掉各種痕迹」

2{icon} {views}

一放假,不少家庭已經開始著手家中內外的清潔,環顧四周若牆面髒兮兮的不僅自己看著不舒適。 
   

原本白凈的牆面染上灰塵之後實在很難清潔,若大肆粉刷一番不僅費用昂貴而且室內還會留下味道一時半會散不去。  

不要著急,教你個簡單的方法,不用水擦、也不需要粉刷,一會就能將髒兮兮的牆面變得乾凈如新。 
我們只需調製一碗具有清潔效果的溶液就可以了,拿一個容器,先在裡面擠入一些牙膏,接著倒入白醋,最後用筷子攪拌均勻就可以了。 
   

做好之後我們再將溶液倒在噴壺中。 

牙膏既能夠清潔還具有美白的效果,而白醋可殺菌去異味,將三者配合在一起可有效將牆面清潔乾凈。 

將噴壺對著牆面髒的地方噴一噴,稍後用抹布擦一擦,牆面的灰塵就被擦乾凈了,變得如同從前一樣潔白。 

這個方法簡單還實用,過年的時候助你輕鬆大掃除。


延伸閱讀:大掃除就用小蘇打!所有污漬都能輕鬆搞定


這幾天冷空氣殺過來了, 離過年也越來越近了,家裡又到一年一次的大掃除啦,今天給大家分享一個超級簡單的清潔方法,那就是用小蘇打粉!

小蘇打即碳酸氫鈉,人們通常只知道可以用它來發麵包、制糕點和做滅火劑,其實,它無毒、自然分解力強,能除垢除臭,是極好的家庭清潔劑。

【 廚 房 篇 】

廚房可以說是整個家裡最讓人頭疼的地方,每天都要和油煙、熱氣接觸,想要讓它重回潔凈其實也不難,只需達成以下幾個標準!

A:操作台、柜子表明摸起來不粘膩

櫥櫃在整個廚房中面積不小,可是「顏面擔當」,對重污染區域,我們可以給它們做一個”清潔面膜”。

小蘇打和清水按2:1的比例,配製成小蘇打膏,塗抹在櫥櫃表面,用百潔布輕輕擦拭即可。

B:廚房用具也要乾凈

做飯的過程中,不鏽鋼似乎很容易變黑變焦,讓人沒有了做飯的慾望,這些焦黑可逆嗎?

在一口大鍋內放入要拯救的不鏽鋼製品,加入足夠的水和大量小蘇打粉大火煮沸10-15min,撈起後用抹布用力擦拭,污漬肉眼可見的減少。

對於嚴重黑焦污漬,同樣可以用小蘇打搞定!不信?有圖有真相!

在污漬表面沾一些水,薄薄的塗抹上一層小蘇打粉,用保鮮膜將整個鍋子包裹起來,放置一個晚上。第二天拆開保鮮膜,用保潔布進行擦洗。

C:水槽、水龍頭光亮嗎?

水漬、油煙會讓廚房的水龍頭、水槽蒙上一層灰撲撲的污漬,讓廚房有種破舊感。

只需在污漬處撒一些小蘇打,再用百潔布擦拭,立刻還原水槽龍頭原有的金屬光澤感!

【 衛 生 間 篇 】

衛生間每天和水、沐浴露接觸,怎麼還會有問題呢?不信?對號入座看看~

A:淋蓬頭出水暢快嗎?

自來水中的礦物質和雜質會產生沉澱、水垢,堵住淋蓬頭的出水口,導致水流阻塞不暢快。

在塑料袋裝中裝白醋水,把淋浴頭泡進白醋中,再用小蘇打水清洗祛味,可充分溶解水垢。

B:浴簾底部有霉斑嗎?

浴簾每天沾水,又不會特地擦乾,長期以來,會產生黑色的霉斑。

對這種陳年污漬,可用牙刷,蘸小蘇打糊或者小蘇打牙膏刷洗。

如果覺得清潔力度不夠,可在牙膏中再加一份小蘇打。

當然,這種方法也可用來對付,衛生間和廚房內,瓷磚和瓷磚的縫隙。

C:清洗馬桶還在靠刷刷刷?

馬桶,可以說是整個衛生間最不乾凈的地方,最容易引起污漬和異味。

我們的秘訣是:在馬桶內撒小蘇打,配合刷子去污又去味。

小蘇打還可以用來洗碗洗水果

這樣健康無污染的大掃除方法,你GET了嗎?趕快把家裡的廚房衛生間衛生搞定了吧!

如果你也用過小蘇打,歡迎來分享評論哦~

via

本站聲明:網站內容來源http://www.look543.com,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※聽過「電子菸」嗎?想知道與一般傳統香菸有何不同嗎?

電子煙能幫助戒菸嗎?專家學者以健康觀點帶您來了解 !

※新手該如何選擇電子菸口味及濃度呢?

剝石榴最快最輕鬆的方法!石榴籽全部自動脫落,不費力不髒手

3{icon} {views}

石榴酸甜可口多汁,營養價值非常高,深受大家的喜愛,但是說到剝石榴,大家都非常頭疼,一粒粒小小的石榴果肉很難剝下來,那麼今天妙招姐就給大家帶來一個剝石榴的小妙招,方法既簡單又好用。

首先我們把石榴的頂蓋給切下來。

切下來後,再沿著石榴果肉裡面的白膜將外殼一刀刀的劃開。

全部劃開後,再把石榴這樣輕輕的掰一掰。

再拿一個小勺子在石榴外殼的側邊不斷的拍打。

這樣石榴果肉就非常輕鬆的全部跑到碗裡來了,我們再用勺子舀著吃就非常的方便了。

怎麼樣,今天這個剝石榴的小妙招很好用吧,趕快把這個妙招分享給你的家人和朋友哦,這樣可以幫助更多的人。

本站聲明:網站內容來源http://www.look543.com,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※聽過「電子菸」嗎?想知道與一般傳統香菸有何不同嗎?

電子煙能幫助戒菸嗎?專家學者以健康觀點帶您來了解 !

※新手該如何選擇電子菸口味及濃度呢?

冬天每晚「泡腳」時放一包「半個月體內寒濕氣全無」肚子平了,腰也細了

5{icon} {views}

 

進入冬季以後,由於天氣寒冷,人們不自然的就會減少運動,減少排汗,戶內外的溫差也很容易腰腿關節的疼痛,甚至於早上怎麼都睡不醒,白天哈欠連天,疲乏無力。

 

其實,這很可能是因為:到了冬天,你體內的濕氣更加嚴重了。在冬季,吃的多,運動的少,脾虛胃弱,平時有濕氣的人會進一步加重。

這個時候,不妨伸出舌頭觀察下,如果發現舌苔厚膩,舌苔胖大,邊緣有鋸齒,再觀察下自己的大便形態,如果大便溏稀不成型,那麼肯定說明你體內的濕氣更加嚴重了。

 

濕氣是中醫理論上六大致病因素之一,在風、寒、暑、濕、燥、火這「六淫邪氣」 中,濕氣對人傷害最大。

首先,濕氣會傷害脾胃,常出現肚子脹,消化不良,大便溏稀或者便秘等現象。如果濕氣長久不除,很快濕氣會傷肺,表現為痰多咳嗽,冬天易患呼吸道方面的疾病。

 

緊接著,濕氣會傷腎,常出現冬天畏風怕冷,女性黑眼圈,月經不調,男性陽痿早泄等癥狀。

 

於此同時,濕氣還會侵犯肝膽,常出現嘴唇乾燥,口乾口苦等癥狀。

一般到了冬天,這些濕氣的癥狀可能會更加嚴重,有些人會出現數以下幾種不適:

 

1.大便不成形,溏稀或者便秘;

2.舌苔厚膩,舌苔邊緣成鋸齒狀,俗稱裙邊舌;

3.疲倦無力,無精打采,特別睏乏嗜睡;

4.畏風怕冷,腰酸關節疼痛;

5.口乾,口苦,口臭;

6.痰多,咳嗽,嗓子不清爽;

7.頭髮油膩,脫髮,白髮;

8.胸口悶;

9.睡覺打呼嚕;

10.頭暈沒精神,特別疲勞;

11.臉上長斑,起痘,滿臉油光;

12.皮膚油膩,起濕疹等等。

如果有這樣的癥狀,不妨試試泡腳,每晚泡泡腳,是舒筋活血、暖身驅寒的好辦法,並且能去濕氣,讓我們睡得更好。泡腳還能刺激足部的穴位、反射區和經絡,增強機體的免疫力,防禦各種毒素對人體的侵害。泡腳還可以促進人體的血液循環。

艾葉泡腳,功非小補

很多人曉得用熱水泡腳對身體十分好,假如用艾葉泡腳,那更是勝吃補藥。

艾葉泡腳對祛濕有著很大的益處,那末除去這些,艾葉泡腳對人體另有甚麼影響呢?

1、祛虛火,寒火

艾葉泡腳具有祛寒火的影響,可用於醫治口腔潰瘍、咽喉腫痛、牙周炎、牙齦炎、中耳炎等頭臉部重複爆發的這些與虛火、寒火有關的疾病。

2、祛實火

艾葉泡腳另有引火下行的影響,那末這個影響對我們艾灸人出格好。更何況如今春季色候枯燥,你做艾灸處理不當,簡單上火,這時候,你泡泡腳,再搓搓腳,結果就很好。

3、鎮咳、祛痰,抗過敏

古代藥理研討標明,艾葉中含有的揮髮油具有分明的鎮咳、祛痰及抗過敏影響。以是艾葉煎液熏洗雙腳能止咳的緣由之一,就是口鼻吸入了艾葉的揮發氣體,同時人的足底經脈豐厚,肝、脾、腎等經絡均循布足部,用艾葉煎液熏洗雙足,循經脈入肺腎,溫驅寒邪、肺氣得宣、氣得納、咳喘得止。

中醫以為,艾葉是一味溫經止血藥,善溫中、逐冷、除濕,凡婦人血氣寒滯者,最宜用之。適用於虛寒性出血及腹痛,關於婦女虛寒月經不調、腹痛、崩漏具有很強的藥用療效,是一種婦科良藥。故艾葉有女性的「養生草」之稱呼。

取艾葉30克,生薑100克,白酒100毫升。將艾葉冼凈,生薑切成厚片,放入鍋中,加過量水煎煮30分鐘後,去渣取汁,倒入盆中,加白酒,先熏蒸後泡足。

如果你嫌買材料太麻煩的話,可以完全代替艾草泡腳粉哦。

 

本站聲明:網站內容來源http://www.look543.com,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

※聽過「電子菸」嗎?想知道與一般傳統香菸有何不同嗎?

電子煙能幫助戒菸嗎?專家學者以健康觀點帶您來了解 !

※新手該如何選擇電子菸口味及濃度呢?

人臉檢測和人臉識別原理,微調(Fine-tune)原理

4{icon} {views}

一、MTCNN的原理

  搭建人臉識別系統的第一步是人臉檢測,也就是在圖片中找到人臉的位置。在這個過程中,系統的輸入是一張可能含有人臉的圖片,輸出是人臉位置的矩形框,如下圖所示。一般來說,人臉檢測應該可以正確檢測出圖片中存在的所有人臉,不能用遺漏,也不能有錯檢。  

   

  獲得包含人臉的矩形框后,第二步要做的就是人臉對齊(Face Alignment)。原始圖片中人臉的姿態、位置可能較大的區別,為了之後統一處理,要把人臉“擺正”。為此,需要檢測人臉中的關鍵點(Landmark),如眼睛的位置、鼻子的位置、嘴巴的位置、臉的輪廓點等。根據這些關鍵點可以使用仿射變換將人臉統一校準,以盡量消除姿勢不同帶來的誤差,人臉對齊的過程如下圖所示。

   

  這裏介紹一種基於深度卷積神經網絡的人臉檢測和人臉對齊方法—-MTCNN,它是基於卷積神經網絡的一種高精度的實時人臉檢測和對齊技術。MT是英文單詞Multi-task的縮寫,意思就是這種方法可以同時完成人臉檢測的人臉對齊兩項任務。相比於傳統方法,MTCNN的性能更好,可以更精確的定位人臉,此外,MTCNN也可以做到實時的檢測。

  MTCNN由三個神經網絡組成,分別是P-Net、R-Net、O-Net。在使用這些網絡之前,首先要將原始圖片縮放到不同尺度,形成一個“圖像金字塔”,如下圖所示。

   

  接着會對每個尺度的圖片通過神經網絡計算一遍。這樣做的原因在於:原始圖片中的人臉存在不同的尺度,如有的人臉比較大,有的人臉比較小。對於比較小的人臉,可以在放大后的圖片上檢測;對於比較大的人臉,可以在縮小后的圖片上進行檢測。這樣,就可以在統一的尺度下檢測人臉了。

  現在再來討論第一個網絡P-Net的結構,如下圖所示

   

  P-Net的輸入是一個寬和高皆為12像素,同時是3通道的RGB圖像,該網絡要判斷這個12×12的圖像中是否含有人臉,並且給出人臉框和關鍵點的位置。因此對應的輸出應該由3部分組成:

  (1)第一個部分要判斷該圖像是否是人臉(上圖中的face classification),輸出向量的形狀為1x1x2,也就是兩個值,分別為該圖像是人臉的概率,以及該圖像不是人臉的概率。這兩個值加起來應該嚴格等1。之所以使用兩個值來表示,是為了方便定義交叉熵損失。
  (2)第二個部分給出框的精確位置(上圖中的bounding box regression),一般稱之為框回歸。P-Net輸入的12×12的圖像塊可能並不是完美的人臉框的位置,如有的時候人臉並不正好為方形,有的時候12×12的圖像塊可能偏左或偏右,因此需要輸出當前框位置相對於完美的人臉框位置的偏移。這個偏移由四個變量組成。一般地,對於圖像中的框,可以用四個數來表示它的位置:框左上角的橫坐標、框左上角的縱坐標、框的寬度、框的高度。因此,框回歸輸出的值是:框左上角的橫坐標的相對偏移、框左上角的縱坐標的相對偏移、框的寬度的誤差、框的 高度的誤差。輸出向量的形狀就是上圖中的1x1x4。
  (3)第三個部分給出人臉的5個關鍵點的位置。5個關鍵點分別為:左眼的位置、右眼的位置、鼻子的位置、左嘴角的位置、右嘴角的位置。每個關鍵點又需要橫坐標和縱坐標來表示,因此輸出一共是10維(即1x1x10)

  上面的介紹大致就是P-Net的結構了。在實際計算中,通過P-Net中第一層卷積的移動,會對圖像中每一個12×12的區域做一次人臉檢測,得到的結構如下圖所示:

   

  圖中框的大小各有不同,除了框回歸的影響外,主要是因為將圖片金字塔的各個尺度都使用P-Net計算了一遍,因此形成了大小不同的人臉框。P-Net的結果還是比較粗糙的,所以接下來又使用R-Net進一步調優。R-Net的網絡結構如下圖所示。

   

  這個結構與之前的P-Net非常類似,P-Net的輸入是12x12x3的圖像,R-Net是24x24x3的圖像,也就是說,R-Net判斷24x24x3的圖像中是否含有人臉,以及預測關鍵點的位置。R-Net的輸出和P-Net完全一樣,同樣有人臉判別、框回歸、關鍵點位置預測三部分組成。

  在實際應用中,對每個P-Net輸出可能為人臉的區域都放縮到24×24的大小,在輸入到R-Net中,進行進一步的判定。得到的結果如下圖所示:

   

  顯然R-Net消除了P-Net中很多誤判的情況。

  進一步把所有得到的區域縮放成48×48的大小,輸入到最後的O-Net中,O-Net的結構同樣與P-Net類似,不同點在於它的輸入是48x48x3的圖像,網絡的通道數和層數也更多了。O-Net的網絡的結構如下圖所示:

   

  檢測結果如下圖所示:

   

  從P-Net到R-Net,最後再到O-Net,網絡輸入的圖片越來越大,卷積層的通道數越來越多,內部的層數也越來越多,因此它們識別人臉的準確率應該是越來越高的。同時,P-Net的運行速度是最快的,R-Net的速度其次,O-Net的運行速度最慢。之所以要使用三個網絡,是因為如果一開始直接對圖中的每個區域使用O-Net,速度會非常慢慢。實際上P-Net先做了一遍過濾,將過濾后的結果再交給R-Net進行過濾,最後將過濾后的結果交給效果最好但速度較慢的O-Net進行判別。這樣在每一步都提前減少了需要判別的數量,有效降低了處理時間。

  最後介紹MTCNN的損失定義和訓練過程。MTCNN中每個網絡都有三部分輸出,因此損失也由三部分組成。針對人臉判別部分,直接使用交叉熵損失,針對框回歸和關鍵點判定,直接使用L2損失。最後這三部分損失各自乘以自身的權重再加起來,就形成最後的總損失了。在訓練P-Net和R-Net時,更關心框位置的準確性,而較少關注關鍵點判定的損失,因此關鍵點判定損失的權重很小。對於O-Net,關鍵點判定損失的權重較大。

二、使用深度卷積網絡提取特徵

  經過人臉檢測和人臉對齊兩個步驟,就獲得了包含人臉的區域圖像,接下來就要進行人臉識別了。這一步一般是使用深度卷積網絡,將輸入的人臉圖像轉換為一個向量的表示,也就是所謂的“特徵”。

  如何針對人臉來提取特徵?可以先來回憶VGG16的網絡結構(見),輸入神經網絡的是圖像,經過一系列卷積計算后,全連接分類得到類別概率。

  在通常的圖像應用中,可以去掉全連接層,使用卷積層的最後一層當作圖像的“特徵”。但如果對人臉識別問題同樣採用這種方法,即使用卷積層最後一層做為人臉的“向量表示”,效果其實是不好的。這其中的原因和改進方法是什麼?在後面會談到,這裏先談談希望這種人臉的“向量表示”應該具有哪些性質。

  在理想的狀況下,希望“向量表示”之間的距離可以直接反映人臉的相似度

  對於同一個人的兩張人臉圖像,對應的向量之間的歐幾里得距離應該比較小。對於不同人的兩張人臉圖像,對應的向量之間的歐幾里得距離應該比較大。

  例如,設人臉圖像為$x_{1}$,$x_{2}$,對應的特徵為$f(x_{1})$,$f(x_{2})$,當$x_{1}$,$x_{2}$對應是同一個人的人臉時,$f(x_{1})$,$f(x_{2})$的距離$\left \| f(x_{1}),f(x_{2}) \right \|$2應該很小,而當$x_{1}$,$x_{2}$是不同人的人臉時,$f(x_{1})$,$f(x_{2})$的距離$\left \| f(x_{1}),f(x_{2}) \right \|$2應該很大。

  在原始的CNN模型中,使用的是Softmax損失。Softmax是類別間的損失,對於人臉來說,每一類就是一個人。儘管使用Softmax損失可以區別出每個人,但其本質上沒有對每一類的向量表示之間的距離做出要求。

  舉個例子,使用CNN對MNIST進行分類,設計一個特殊的卷積網絡,讓其最後一層的向量變為2維,此時可以畫出每一類對應的2維向量(圖中一種顏色對應一種類別),如下圖所示:

   

  上圖是我們直接使用softmax訓練得到的結果,它就不符合我們希望特徵具有的特點:

  (1)我們希望同一類對應的向量表示盡可能接近。但這裏同一類(如紫色),可能具有很大的類間距離;
  (2)我們希望不同類對應的向量應該盡可能遠。但在圖中靠中心的位置,各個類別的距離都很近;

  對於人臉圖像同樣會出現類似的情況,對此,有很改進方法。這裏介紹其中兩種:一種是三元組損失函數(Triplet Loss),一種是中心損失函數。 

三、三元組損失的定義

  三元組損失函數的原理:既然目標是特徵之間的距離應該具備某些性質,那麼我們就圍繞這個距離來設計損失。具體的,我們每次都在訓練數據中抽出三張人臉圖像,第一張圖像記為$x_{i}^{a}$,第二張圖像記為$x_{i}^{p}$,第三張圖像記為$x_{i}^{n}$。在這樣的一個“三元組”中,$x_{i}^{a}$和$x_{i}^{p}$對應的是同一個人的圖像,而$x_{i}^{n}$是另外一個不同的人的人臉圖像。因此,距離$\left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}$應該較小,而距離$\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}$應該較大。嚴格來說,三元組損失要求下面的式子成立:

   $\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}+\alpha <\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}$

  然後計算相同人臉之間與不同人臉之間距離的平方

   $\left [ \left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}^{2}+\alpha -\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}^{2} \right ]_{+}$

  上式表達相同人臉間的距離平方至少要比不同人臉間的距離平方小α(取平方主要是為了方便求導),據此,上式實際上就是相當於一個損失函數。這樣的話,當三元組的距離滿足 $\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}+\alpha <\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}$時,不產生任何損失,此時$L_{i}=0$。當距離不滿足上述等式時,就會有值為$\left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}^{2}+\alpha -\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}^{2}$的損失。此外,在訓練時會固定$\left \| f(x) \right \|_{2}=1$,以保證特徵不會無限地“遠離”。

  三元組損失直接對距離進行優化,因此可以解決人臉的特徵表示問題。但是在訓練過程中,三元組的選擇非常地有技巧性。如果每次都是隨機選擇三元組,雖然模型可以正確的收斂,但是並不能達到最好的性能。如果加入”難例挖掘”,即每次都選擇最難分辨率的三元組進行訓練,模型又往往不能正確的收斂。對此,又提出每次都選擇那些“半難”(Semi-hard)的數據進行訓練,讓模型在可以收斂的同時也保持良好的性能。此外,使用三元組損失訓練人臉模型通常還需要非常大的人臉數據集,才能取得較好的效果。

四、中心損失的定義

  與三元組損失不同,中心損失(Center Loss)不直接對距離進行優化,它保留了原有的分類模型,但又為每個類(在人臉模型中,一個類就對應一個人)指定了一個類別中心。同一類的圖像對應的特徵都應該盡量靠近自己的類別中心,不同類的類別中心盡量遠離。與三元組損失函數相比,使用中心損失訓練人臉模型不需要使用特別的採樣方法,而且利用較少的圖像就可以達到與單元組損失相似的效果。下面我們一起來學習中心損失的定義:

   還是設輸入的人臉圖像為$x_{i}$,該人臉對應的類別為$y_{i}$,對每個類別都規定一個類別中心,記作$c_{yi}$。希望每個人臉圖像對應的特徵$f(x_{i})$都盡可能接近其中心$c_{yi}$。因此定義中心損失為:

    $L_{i}=\frac{1}{2}\left \| f(x_{i})-c_{yi}\right \|_{2}^{2}$

  多張圖像的中心損失就是將它們的值加在一起:

   $L_{center}=\sum\limits_{i}L_i$

  這是一個非常簡單的定義。不過還有一個問題沒有解決,那就是如何確定每個類別的中心$c_{yi}$呢?從理論上來說,類別$y_{i}$的最佳中心應該是它對應的所有圖片的特徵的平均值。但如果採取這樣的定義,那麼在每一次梯度下降時,都要對所有圖片計算一次$c_{yi}$,計算複雜度就太高了。針對這種情況,不妨近似一處理下,在初始階段,先隨機確定$c_{yi}$,接着在每個batch內,使用$L_i=\|f(x_i)-c_{yi}\|_2^2$對當前batch內的$c_{yi}$ 也計算梯度,並使用該梯度更新$c_{yi}$ 。此外,不能只使用中心損失來訓練分類模型,還需要加入Softmax損失,也就是說,最終的損失由兩部分構成,即$L = L_{softmax}+\lambda L_{center}$,其中$\lambda $是一個超參數。

  最後來總結使用中心損失來訓練人臉模型的過程。首先隨機初始化各个中心$c_{yi}$,接着不斷地取出batch進行訓練,在每個batch中,使用總的損失$L$,除了使用神經網絡模型的參數對模型進行更新外,也對$c_{yi}$進行計算梯度,並更新中心的位置。

  中心損失可以讓訓練處的特徵具有“內聚性”。還是以MNIST的例子來說,在未加入中心損失時,訓練的結果不具有內聚性。再加入中心損失后,得到的特徵如下圖所示。 

   

從圖中可以看出,當中心損失的權重λ越大時,生成的特徵就會具有越明顯的“內聚性” 。

五、使用特徵設計應用

當提取出特徵后,剩下的問題就非常簡單了。因為這種特徵已經具有了相同人對應的向量的距離小,不同人對應的向量距離大的特點,接下來,一般的應用有以下幾類:

  • 人臉驗證(Face Identification)。就是檢測A、B是否屬於同一個人。只需要計算向量之間的距離,設定合適的報警閾值(threshold)即可。
  • 人臉識別(Face Recognition)。這個應用是最多的,給定一張圖片,檢測數據庫中與之最相似的人臉。顯然可以被轉換為一個求距離的最近鄰問題。
  • 人臉聚類(Face Clustering)。在數據庫中對人臉進行聚類,直接用K-means即可。

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

Android DecorView 與 Activity 綁定原理分析

1{icon} {views}

一年多以前,曾經以為自己對 View 的添加显示邏輯已經有所了解了,事後發現也只是懂了些皮毛而已。經過一年多的實戰,Android 和 Java 基礎都有了提升,是時候該去看看 DecorView 的添加显示。

概論

Android 中 Activity 是作為應用程序的載體存在,代表着一個完整的用戶界面,提供了一個窗口來繪製各種視圖,當 Activity 啟動時,我們會通過 setContentView 方法來設置一個內容視圖,這個內容視圖就是用戶看到的界面。那麼 View 和 activity 是如何關聯在一起的呢 ?

 上圖是 View 和 Activity 之間的關係。先解釋圖中一些類的作用以及相關關係:

  • Activity : 對於每一個 activity 都會有擁有一個 PhoneWindow。

  • PhoneWindow :該類繼承於 Window 類,是 Window 類的具體實現,即我們可以通過該類具體去繪製窗口。並且,該類內部包含了一個 DecorView 對象,該 DectorView 對象是所有應用窗口的根 View。
  • DecorView 是一個應用窗口的根容器,它本質上是一個 FrameLayout。DecorView 有唯一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView( ActionBar 的容器),另一個是 ContentView(窗口內容的容器)。

  • ContentView :是一個 FrameLayout(android.R.id.content),我們平常用的 setContentView 就是設置它的子 View 。

  • WindowManager : 是一個接口,裏面常用的方法有:添加View,更新View和刪除View。主要是用來管理 Window 的。WindowManager 具體的實現類是WindowManagerImpl。最終,WindowManagerImpl 會將業務交給 WindowManagerGlobal 來處理。
  • WindowManagerService (WMS) : 負責管理各 app 窗口的創建,更新,刪除, 显示順序。運行在 system_server 進程。

ViewRootImpl :擁有 DecorView 的實例,通過該實例來控制 DecorView 繪製。ViewRootImpl 的一個內部類 W,實現了 IWindow 接口,IWindow 接口是供 WMS 使用的,WSM 通過調用 IWindow 一些方法,通過 Binder 通信的方式,最後執行到了 W 中對應的方法中。同樣的,ViewRootImpl 通過 IWindowSession 來調用 WMS 的 Session 一些方法。Session 類繼承自 IWindowSession.Stub,每一個應用進程都有一個唯一的 Session 對象與 WMS 通信。

DecorView 的創建 

先從 Mainactivity 中的代碼看起,首先是調用了 setContentView;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

該方法是父類 AppCompatActivity 的方法,最終會調用 AppCompatDelegateImpl 的 setContentView 方法:

// AppCompatDelegateImpl  
public void setContentView(int resId) { this.ensureSubDecor(); ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290); contentParent.removeAllViews(); LayoutInflater.from(this.mContext).inflate(resId, contentParent); this.mOriginalWindowCallback.onContentChanged(); }

ensureSubDecor 從字面理解就是創建 subDecorView,這個是根據主題來創建的,下文也會講到。創建完以後,從中獲取 contentParent,再將從 activity 傳入的 id xml 布局添加到裏面。不過大家注意的是,在添加之前先調用 removeAllViews() 方法,確保沒有其他子 View 的干擾。

    private void ensureSubDecor() {
        if (!this.mSubDecorInstalled) {
            this.mSubDecor = this.createSubDecor(); 
            ......
        }
        ......
    }        

 最終會調用 createSubDecor() ,來看看裏面的具體代碼邏輯:

 private ViewGroup createSubDecor() {
        // 1、獲取主題參數,進行一些設置,包括標題,actionbar 等 
        TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
        if (!a.hasValue(styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        } else {
            if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle, false)) {
                this.requestWindowFeature(1);
            } else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar, false)) {
                this.requestWindowFeature(108);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay, false)) {
                this.requestWindowFeature(109);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay, false)) {
                this.requestWindowFeature(10);
            }

            this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating, false);
            a.recycle();
            // 2、確保優先初始化 DecorView
            this.mWindow.getDecorView();
            LayoutInflater inflater = LayoutInflater.from(this.mContext);
            ViewGroup subDecor = null;
            // 3、根據不同的設置來對 subDecor 進行初始化
            if (!this.mWindowNoTitle) {
                if (this.mIsFloating) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_dialog_title_material, (ViewGroup)null);
                    this.mHasActionBar = this.mOverlayActionBar = false;
                } else if (this.mHasActionBar) {
                    TypedValue outValue = new TypedValue();
                    this.mContext.getTheme().resolveAttribute(attr.actionBarTheme, outValue, true);
                    Object themedContext;
                    if (outValue.resourceId != 0) {
                        themedContext = new ContextThemeWrapper(this.mContext, outValue.resourceId);
                    } else {
                        themedContext = this.mContext;
                    }

                    subDecor = (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar, (ViewGroup)null);
                    this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent);
                    this.mDecorContentParent.setWindowCallback(this.getWindowCallback());
                    if (this.mOverlayActionBar) {
                        this.mDecorContentParent.initFeature(109);
                    }

                    if (this.mFeatureProgress) {
                        this.mDecorContentParent.initFeature(2);
                    }

                    if (this.mFeatureIndeterminateProgress) {
                        this.mDecorContentParent.initFeature(5);
                    }
                }
            } else {
                if (this.mOverlayActionMode) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode, (ViewGroup)null);
                } else {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple, (ViewGroup)null);
                }

                if (VERSION.SDK_INT >= 21) {
                    ViewCompat.setOnApplyWindowInsetsListener(subDecor, new OnApplyWindowInsetsListener() {
                        public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                            int top = insets.getSystemWindowInsetTop();
                            int newTop = AppCompatDelegateImpl.this.updateStatusGuard(top);
                            if (top != newTop) {
                                insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
                            }

                            return ViewCompat.onApplyWindowInsets(v, insets);
                        }
                    });
                } else {
                    ((FitWindowsViewGroup)subDecor).setOnFitSystemWindowsListener(new OnFitSystemWindowsListener() {
                        public void onFitSystemWindows(Rect insets) {
                            insets.top = AppCompatDelegateImpl.this.updateStatusGuard(insets.top);
                        }
                    });
                }
            }

            if (subDecor == null) {
                throw new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ", windowActionBarOverlay: " + this.mOverlayActionBar + ", android:windowIsFloating: " + this.mIsFloating + ", windowActionModeOverlay: " + this.mOverlayActionMode + ", windowNoTitle: " + this.mWindowNoTitle + " }");
            } else {
                if (this.mDecorContentParent == null) {
                    this.mTitleView = (TextView)subDecor.findViewById(id.title);
                }

                ViewUtils.makeOptionalFitsSystemWindows(subDecor);
                ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content);
                ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(16908290);
                if (windowContentView != null) {
                    while(windowContentView.getChildCount() > 0) {
                        View child = windowContentView.getChildAt(0);
                        windowContentView.removeViewAt(0);
                        contentView.addView(child);
                    }

                    windowContentView.setId(-1);
                    contentView.setId(16908290);
                    if (windowContentView instanceof FrameLayout) {
                        ((FrameLayout)windowContentView).setForeground((Drawable)null);
                    }
                }
                // 將 subDecor 添加到 DecorView 中
                this.mWindow.setContentView(subDecor);
                contentView.setAttachListener(new OnAttachListener() {
                    public void onAttachedFromWindow() {
                    }

                    public void onDetachedFromWindow() {
                        AppCompatDelegateImpl.this.dismissPopups();
                    }
                });
                return subDecor;
            }
        }
    }
                    

上面的代碼總結來說就是在做一件事,就是創建 subDecor。攤開來說具體如下:

1、根據用戶選擇的主題來設置一些显示特性,包括標題,actionbar 等。

2、根據不同特性來初始化 subDecor;對 subDecor 內部的子 View 進行初始化。

3、最後添加到 DecorView中。

添加的具體代碼如下:此處是通過調用 

 // AppCompatDelegateImpl   this.mWindow.getDecorView();

 // phoneWindow    public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }
 

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
 // 生成 DecorView             mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
 // 這樣 DecorView 就持有了window             mDecor.setWindow(this);
        }
      ......
}


   protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
   }

到此,DecorView 的創建就講完了。可是我們似乎並沒有看到 DecorView 是被添加的,什麼時候對用戶可見的。

 WindowManager

View 創建完以後,那 Decorview 是怎麼添加到屏幕中去的呢?當然是 WindowManager 呢,那麼是如何將 View 傳到 WindowManager 中呢。

看 ActivityThread 中的 handleResumeActivity 方法:

// ActivityThread
public
void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ...... final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; // If the window hasn't yet been added to the window manager, // and this guy didn't finish itself or start another activity, // then go ahead and add the window. boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManager.getService().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; ...... if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l); } else { // The activity will get a callback for this {@link LayoutParams} change // earlier. However, at that time the decor will not be set (this is set // in this method), so no action will be taken. This call ensures the // callback occurs with the decor set. a.onWindowAttributesChanged(l); } } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } // Get rid of anything left hanging around. cleanUpPendingRemoveWindows(r, false /* force */); // The window is now visible if it has been added, we are not // simply finishing, and we are not starting another activity. if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { performConfigurationChangedForActivity(r, r.newConfig); if (DEBUG_CONFIGURATION) { Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig); } r.newConfig = null; } if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward); WindowManager.LayoutParams l = r.window.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; if (r.activity.mVisibleFromClient) { ViewManager wm = a.getWindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor, l); } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) {           // 這裏也會調用addview r.activity.makeVisible(); } } r.nextIdle = mNewActivities; mNewActivities = r; if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r); Looper.myQueue().addIdleHandler(new Idler()); }

上面的代碼主要做了以下幾件事:

1、獲取到 DecorView,設置不可見,然後通過 wm.addView(decor, l) 將 view 添加到 WindowManager;

2、在某些情況下,比如此時點擊了輸入框調起了鍵盤,就會調用 wm.updateViewLayout(decor, l) 來更新 View 的布局。

3、這些做完以後,會調用 activity 的  makeVisible ,讓視圖可見。如果此時 DecorView 沒有添加到 WindowManager,那麼會添加。 

// Activity
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }

 接下來,看下 addview 的邏輯。 WindowManager 的實現類是 WindowManagerImpl,而它則是通過 WindowManagerGlobal 代理實現 addView 的,我們看下 addView 的方法:

// WindowManagerGlobal  
 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
           // ......
    
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
           // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            } 
}

在這裏,實例化了 ViewRootImpl 。同時調用 ViewRootImpl 的 setView 方法來持有了 DecorView。此外這裏還保存了 DecorView ,Params,以及 ViewRootImpl 的實例。

現在我們終於知道為啥 View 是在 OnResume 的時候可見的呢。

 ViewRootImpl

實際上,View 的繪製是由 ViewRootImpl 來負責的。每個應用程序窗口的 DecorView 都有一個與之關聯的 ViewRootImpl 對象,這種關聯關係是由 WindowManager 來維護的。

先看 ViewRootImpl 的 setView 方法,該方法很長,我們將一些不重要的點註釋掉:

   /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ......
               
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.

                requestLayout();
                ......
            }
        }
    }

這裏先將 mView 保存了 DecorView 的實例,然後調用 requestLayout() 方法,以完成應用程序用戶界面的初次布局。

 public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

因為是 UI 繪製,所以一定要確保是在主線程進行的,checkThread 主要是做一個校驗。接着調用 scheduleTraversals 開始計劃繪製了。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

這裏主要關注兩點:

mTraversalBarrier : Handler 的同步屏障。它的作用是可以攔截 Looper 對同步消息的獲取和分發,加入同步屏障之後,Looper 只會獲取和處理異步消息,如果沒有異步消息那麼就會進入阻塞狀態。也就是說,對 View 繪製渲染的處理操作可以優先處理(設置為異步消息)。

mChoreographer: 編舞者。統一動畫、輸入和繪製時機。也是這章需要重點分析的內容。

mTraversalRunnable :TraversalRunnable 的實例,是一個Runnable,最終肯定會調用其 run 方法:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

doTraversal,如其名,開始繪製了,該方法內部最終會調用 performTraversals 進行繪製。

  void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

到此,DecorView 與 activity 之間的綁定關係就講完了,下一章,將會介紹 performTraversals 所做的事情,也就是 View 繪製流程。 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Java虛擬機詳解(十)——類加載過程

3{icon} {views}

  在上一篇文章中,我們詳細的介紹了Java,那麼這些Class文件是如何被加載到內存,由虛擬機來直接使用的呢?這就是本篇博客將要介紹的——類加載過程。

1、類的生命周期

  類從被加載到虛擬機內存開始,到卸載出內存為止,其聲明周期流程如下:

  

  上圖中紅色的5個部分(加載、驗證、準備、初始化、卸載)順序是確定的,也就是說,類的加載過程必須按照這種順序按部就班的開始。這裏的“開始”不是按部就班的“進行”或者“完成”,因為這些階段通常是互相交叉混合的進行的,通常會在一個階段執行過程中調用另一個階段。

2、加載

  “加載”階段是“類加載”生命周期的第一個階段。在加載階段,虛擬機要完成下面三件事:

  ①、通過一個類的全限定名來獲取定義此類的二進制字節流。

  ②、將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

  ③、在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。

  PS:類的全限定名可以理解為這個類存放的絕對路徑。方法區是JDK1.7以前定義的運行時數據區,而在JDK1.8以後改為元數據區(Metaspace),主要用於存放被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。詳情可以參考這邊該系列的第二篇文章——。

  另外,我們看第一點——通過類的權限定名來獲取定義此類的二進制流,這裏並沒有明確指明要從哪裡獲取以及怎樣獲取,也就是說並沒有明確規定一定要我們從一個 Class 文件中獲取。基於此,在Java的發展過程中,充滿創造力的開發人員在這個舞台上玩出了各種花樣:

  1、從 ZIP 包中讀取。這稱為後面的 JAR、EAR、WAR 格式的基礎。

  2、從網絡中獲取。比較典型的應用就是 Applet。

  3、運行時計算生成。這就是動態代理技術。

  4、由其它文件生成。比如 JSP 應用。

  5、從數據庫中讀取。

  加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,然後在Java堆中實例化一個 java.lang.Class 類的對象,這個對象將作為程序訪問方法區中這些類型數據的外部接口。

  注意,加載階段與連接階段的部分內容(如一部分字節碼文件的格式校驗)是交叉進行的,加載階段尚未完成,連接階段可能已經開始了。

3、驗證

  驗證是連接階段的第一步,作用是為了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

  我們說Java語言本身是相對安全,因為編譯器的存在,純粹的Java代碼要訪問數組邊界外的數據、跳轉到不存在的代碼行之類的,是要被編譯器拒絕的。但是前面我們也說過,Class 文件不一定非要從Java源碼編譯過來,可以使用任何途徑,包括你很牛逼,直接用十六進制編輯器來編寫 Class 文件。

  所以,如果虛擬機不檢查輸入的字節流,將會載入有害的字節流而導致系統崩潰。但是虛擬機規範對於檢查哪些方面,何時檢查,怎麼檢查都沒有明確的規定,不同的虛擬機實現方式可能都會有所不同,但是大致都會完成下面四個方面的檢查。

①、文件格式驗證

  校驗字節流是否符合Class文件格式的規範,並且能夠被當前版本的虛擬機處理。

  一、是否以魔數 0xCAFEBABE 開頭。

  二、主、次版本號是否是當前虛擬機處理範圍之內。

  三、常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)

  四、指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。

  五、CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數據。

  六、Class 文件中各個部分及文件本身是否有被刪除的或附加的其他信息。

  以上是一部分校驗內容,當然遠不止這些。經過這些校驗后,字節流才會進入內存的方法區中存儲,接下來後面的三個階段校驗都是基於方法區的存儲結構進行的。

②、元數據驗證

  第二個階段主要是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範要求。

  一、這個類是否有父類(除了java.lang.Object 類之外,所有的類都應當有父類)。

  二、這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。

  三、如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有普通方法。

  四、類中的字段、方法是否與父類產生了矛盾(例如覆蓋了父類的final字段、或者出現不符合規則的重載)

③、字節碼驗證

  第三個階段字節碼驗證是整個驗證階段中最複雜的,主要是進行數據流和控制流分析。該階段將對類的方法進行分析,保證被校驗的方法在運行時不會做出危害虛擬機安全的行為。

  一、保證任意時刻操作數棧中的數據類型與指令代碼序列都能配合工作。例如不會出現在操作數棧中放置了一個 int 類型的數據,使用時卻按照 long 類型來加載到本地變量表中。

  二、保證跳轉指令不會跳轉到方法體以外的字節碼指令中。

  三、保證方法體中的類型轉換是有效的。比如把一個子類對象賦值給父類數據類型,這是安全的。但是把父類對象賦值給子類數據類型,甚至賦值給完全不相干的類型,這就是不合法的。

④、符號引用驗證

  符號引用驗證主要是對類自身以外(常量池中的各種符號引用)的信息進行匹配性的校驗,通常需要校驗如下內容:

  一、符號引用中通過字符串描述的全限定名是否能夠找到相應的類。

  二、在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。

  三、符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可以被當前類訪問。

4、準備

  準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存是在方法區中進行分配。

  注意:

  一、上面說的是類變量,也就是被 static 修飾的變量,不包括實例變量。實例變量會在對象實例化時隨着對象一起分配在堆中。

  二、初始值,指的是一些數據類型的默認值。基本的數據類型初始值如下(引用類型的初始值為null):

  

 

   比如,定義 public static int value = 123 。那麼在準備階段過後,value 的值是 0 而不是 123,把 value 賦值為123 是在程序被編譯后,存放在類的構造器方法之中,是在初始化階段才會被執行。但是有一種特殊情況,通過final 修飾的屬性,比如 定義 public final static int value = 123,那麼在準備階段過後,value 就被賦值為123了。

5、解析

  解析階段是虛擬機將常量池中的符號引用替換為直接引用的過程。

  符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標不一定已經加載到內存中。

  直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼引用的目標必定已經在內存中存在。

  解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用,分別對應於常量池的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANTS_InterfaceMethodref_info四種類型常量。

6、初始化

   初始化階段是類加載階段的最後一步,前面過程中,除第一個加載階段可以通過用戶自定義類加載器參与之外,其餘過程都是完全由虛擬機主導和控制。而到了初始化階段,則開始真正執行類中定義的Java程序代碼(或者說是字節碼)。

  在前面介紹的準備階段中,類變量已經被賦值過初始值了,而初始化階段,則根據程序員的編碼去初始化變量和資源。

  換句話來說,初始化階段是執行類構造器<clinit>() 方法的過程

  ①、<clinit>() 方法 是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{})中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊中可以賦值,但是不能訪問。

  比如如下代碼會報錯:

  

 

   但是你把第 14 行代碼放到 static 靜態代碼塊的上面就不會報錯了。或者不改變代碼順序,將第 11 行代碼移除,也不會報錯。

  ②、<clinit>() 方法與類的構造函數(或者說是實例構造器<init>()方法)不同,它不需要显示的調用父類構造器,虛擬機會保證在子類的<init>()方法執行之前,父類的<init>()方法已經執行完畢。因此虛擬機中第一個被執行的<init>()方法的類肯定是 java.lang.Object。

  ③、由於父類的<clinit>() 方法先執行,所以父類中定義的靜態語句塊要優先於子類的變量賦值操作。

  ④、<clinit>() 方法對於接口來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不為這個類生成<clinit>() 方法。

  ⑤、接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>() 方法。但接口與類不同的是,執行接口中的<clinit>() 方法不需要先執行父接口的<clinit>() 方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。

  ⑥、接口的實現類在初始化時也一樣不會執行接口的<clinit>() 方法。

  ⑦、虛擬機會保證一個類的<clinit>() 方法在多線程環境中被正確的加鎖和同步。如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>() 方法,其他的線程都需要阻塞等待,直到活動線程執行<clinit>() 方法完畢。如果在一個類的<clinit>() 方法中有很耗時的操作,那麼可能造成多個進程的阻塞。

  比如對於如下代碼:

package com.yb.carton.controller;

/**
 * Create by YSOcean
 */
public class ClassLoadInitTest {


    static class Hello{
        static {
            if(true){
                System.out.println(Thread.currentThread().getName() + "init");
                while(true){}
            }
        }
    }

    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start");
            Hello h1 = new Hello();
            System.out.println(Thread.currentThread().getName()+"run over");
        }).start();


        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start");
            Hello h2 = new Hello();
            System.out.println(Thread.currentThread().getName()+"run over");
        }).start();
    }

}

View Code

  運行結果如下:

  

 

   線程1搶到了執行<clinit>() 方法,但是該方法是一個死循環,線程2將一直阻塞等待。

  知道了類的初始化過程,那麼類的初始化何時被觸發呢?JVM大概規定了如下幾種情況:

  ①、當虛擬機啟動時,初始化用戶指定的類。

  ②、當遇到用以新建目標類實例的 new 指令時,初始化 new 指定的目標類。

  ③、當遇到調用靜態方法的指令時,初始化該靜態方法所在的類。

  ④、當遇到訪問靜態字段的指令時,初始化該靜態字段所在的類。

  ⑤、子類的初始化會觸發父類的初始化。

  ⑥、如果一個接口定義了 default 方法,那麼直接實現或間接實現該接口的類的初始化,會觸發該接口的初始化。

  ⑦、使用反射 API 對某個類進行反射調用時,會初始化這個類。

  ⑧、當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

新房通風幾個月就能入住?不行!還要記好這兩點,直接入住易患癌

2{icon} {views}

隨着甲醛中毒事故的增加,近幾年大家都深刻認識了甲醛的危害。會在面對甲醛污染時,盡可能的去尋找除甲醛方法,以保證自己和家人的健康。可是,專家提醒我們,想徹底逃脫甲醛的“魔爪”,只靠除甲醛方法是不夠的,我們還要改掉一些固有的錯誤觀念。

錯誤觀念一:家裡沒異味就是沒甲醛

很多人都對甲醛沒有太多的了解,總以為家裡沒有刺激的氣味,那家裡肯定不會甲醛超標。其實這是不正確的,甲醛的氣味是很淡的,一般只要不超標三倍以上,我們都聞不到明顯的異味。所以甲醛含量是否超標要用科學的儀器來測量,不能僅憑有沒有氣味就來進行判斷。

錯誤觀念二:通風幾個月就能入住

很多人都以為只要開窗通風就可以去除甲醛,通風幾個月就能入住新房了。其實這樣的做法也是不對的,甲醛的毒氣時間很長,大約需要十幾年的時間來進行揮發。而通風僅僅是依靠風力的作用,一旦室外沒有風力,那麼這個方法就會失效,室內的甲醛濃度會再次上升。

這位專家還教給我們去除甲醛的好辦法,大家可以嘗試使用。

方法一:

它可以說是現下比較流行的環保材料了,其使用的時間比較長,能夠使用三年之久。而且它的治理作用有吸附與分解兩種,能在對甲醛進行吸附的同時,把甲醛完完全全的分解乾淨。因此,它不會在治理甲醛污染的過程中發生飽和,可供使用三年以上,甚至更久。

方法二:植物

我們在家裡適當的擺放幾盆弔蘭、綠蘿等植物,也是可以清除甲醛污染的。但是植物的生命力很弱,容易被甲醛“毒”死。而且它們吸附甲醛的能力也微弱,能除掉的甲醛不多。大家在使用時,最好將它們作為輔助方法。

方法三:活性炭

活性炭是前些年比較流行的吸附材料,能吸附室內的甲醛等污染氣體。只是它的治理能力不足,只有吸附甲醛這一種,沒辦法把甲醛真正的處理乾淨。所以它在治理甲醛污染時,經常出現飽和的情況,需要每使用一個月就更換一次新的。

【其他文章推薦】

實木地板、海島型地板、耐磨地板怎麼挑? 木地板三倍價差的秘密!!

※新屋購入,尋找台中室內設計師?是否可先免費估價丈量?

※挑好磚一點都不難!馬賽克磚挑選眉角小撇步!

※想知道北部最多平價、庫存出清的家具工廠推薦在哪裡?

※分享木質地板DIY自行施工教學影片

近期頻繁咳嗽、睏倦的注意:可能與取暖有關,易甲醛中毒患癌

4{icon} {views}

現在的科技發達,冬天的時候室內的溫度也可以很高,北方有供暖,南方有空調,冬天不再難以度過。但是,溫度的升高就會讓室內的有害氣體活躍,而打開這些取暖設備的時候又往往會關着窗,把家裡變成一個封閉的環境。這時室內的甲醛就會大量釋放出來,讓家裡變得危險。

那麼生活中,我們要如何預防甲醛中毒呢?醫生告訴我們,根據以下幾種癥狀預防就可以。

癥狀一:精神不振

如果室內的甲醛濃度超標了,那麼我們的中樞神經就會被刺激,出現頭疼、頭暈、精神不振等癥狀。所以大家生活中,如果有了以上幾種癥狀,那麼就要及時檢測室內的甲醛含量了。

癥狀二:呼吸困難

由於甲醛會在我們呼吸時,進入呼吸道刺激呼吸道黏膜。所以如果室內的甲醛濃度超標了,那麼我們自身的呼吸道就會受到過大的刺激,進而出現咳嗽、打噴嚏、呼吸困難等癥狀。

這時候我們需要的就是,能24小時持續性除醛的清除材料了。

方法一:

這是資深專家團隊的最新研究結果,內部被添加了許多凈化因子,讓它能夠有效的吸收有害氣體,也能分解有害氣體。因此,它在治理甲醛污染時,不會出現短時間飽和的問題。這樣就不需要一直更換,保障了它三年的使用期。

方法二:除甲醛噴霧

很多有害氣體都是在傢具里散發出來的,這個噴霧就是能讓傢具不再釋放有害氣體的材料。它能在傢具的表面形成一層薄膜,阻擋傢具裡邊的有害氣體往外釋放。只是它的有效期比較短,使用后的兩、三個月就會失效,需要頻繁噴洒維護,才能持續性發揮作用。

方法三:綠色植物

植物也是一種很好的除甲醛方法,放在室內可以幫我們吸收掉適量的有害氣體。不過到了夜晚這種能力就會減弱,而且如果有害氣體濃度過高了,植物也是會很容易死亡的。

【其他文章推薦】

※居家隱形鐵窗安裝施作經驗分享

※純客製手工沙發,古典沙發,專業首選沙發工廠打造屬於您的居家品味!

※解決漏水、壁癌危機,找尋新竹舊屋翻新專業修繕專家

※分享木質地板DIY自行施工教學影片

※想要打造簡約、淡雅兼且收納空間的小資房,台中室內設計推薦哪一家?