米蘭國際摩托車展直擊,電動機車群雄並起

米蘭國際摩托車展(EICMA)在 2018 年已經邁入第 76 屆,是最有歷史也最具規模的摩托車展會之一,再加上義大利本身就是機車大國,米蘭國際摩托車展的地位不言可喻。科技新報在米蘭摩托車展直擊各國最新款的電動機車,一窺未來電動機車的可能性。

隨著環保意識提升和電動技術的發展,電動機車成為重要的發展方向,許多車廠也搶進電動機車市場。除了國內電動機車市場邁向三足鼎立,海外的電動機車也呈現百家爭鳴的局面。2018 年米蘭國際摩托車展的主題是「我們看見了尚未存在的道路」,廠商們展示出自身預見的摩托車未來發展趨勢,其中電動機車就是相當重要的一環。

Harley-Davidson

以重型機車風靡全球的美國摩托車大廠哈雷(Harley-Davidson)在多年傳聞將生產電動機車之後,終於在米蘭國際摩托車展端出了電動重機 LiveWire。LiveWire 源自於 2014 年的 LiveWire 電動機車計畫,經過四年的時間終於開花結果。LiveWire 會以充電式鋰電池提供動力,哈雷表示所有經銷商都會配有開放使用的充電器。這款電動重機可以利用觸控螢幕進行操作,並透過藍牙連結手機來撥放音樂、接聽電話或進行導航,還能夠設定 7 種騎乘模式。

LiveWire 的詳細規格還有很多尚未透漏的資訊,包括電池續航力和性能等等。LiveWire 電動重機共有紅色、黑色和橘色三種款式,預計在 2019 年 1 月上市,售價和上市範圍則尚未公布。「這是哈雷跨入電動摩托車界的重要一步,但只不過是第一步。」哈雷產品規劃副總裁 Marc Mcallister 這麼說。未來哈雷計畫到 2022 年將會有「全系列的電動摩托車」,這樣的豪語能否成真值得拭目以待。

哈雷電動重機 LiveWire。(圖片來源:)

哈雷產品規劃副總裁 Marc Mcallister。(圖片來源:)

哈雷電動重機 LiveWire。(圖片來源:)

Vespa

旗下擁有知名機車品牌偉士牌(Vespa)的義大利機車集團 Piaggio 自然不會錯過本國的摩托車盛事,展示了電動機車 Vespa Elettrica 和油電混合車 Vespa Elettrica X。Vespa Elettrica 雖然早在兩年前就發表,不過直到 2018 年 10 月才在歐洲市場開放預購。Elettrica 具有 4kW 的電動馬達功率,屬於 50cc 級距。動力採用充電式鋰電池,充滿電需要 4 個小時,充電一次最遠可以騎乘 100 公里,可以使用一般的插座充電。Vespa Elettrica 在歐洲定價為 6,390 歐元起,約相當於 22.5 萬元台幣,價格比燃油車版本高上不少。

Vespa Elettrica X 屬於油電混合動力,為了空出空間放置燃油馬達和油箱,Elettrica X 採用了較小的電池。其中燃油動力能騎乘 150 公里,電池則能提供 50 公里的動力,因此續航距離最長可以達到接近 200 公里。Vespa Elettrica X 預計將會在 2019 年 3 月推出。

Vespa Elettrica。(圖片來源:)

Vespa Elettrica X。(圖片來源:)

Energica

2014 年才成立的義大利公司 Energica 在電動重機領域已經有豐富的經驗,在 2017 年的米蘭國際摩托車展就發表過電動重機 EVA EsseEsse 9。Energica 旗下還有 EGO 和 EVA 兩款性能優異的電動重機,其中 EGO 更成為電動摩托車世界盃(FIM Moto-e World Cup)的指定用車。這次 Energica 的展示品當中最受矚目的並非上述的車款,而是與電子大廠三星(Samsung)聯手打造的新車款 Bolid-E。

Bolid-E 建立在 EVA EsseEsse 9 的基礎上,搭載雙方共同開發的 Smart Ride 智慧系統。透過 Smart Ride 系統能讓 Bolid-E 連結車主的三星智慧手錶(Samsung Galaxy Watch),用手錶的應用程式就能操控摩托車。車主可以在手錶上查看充電情形、地圖、車輛的狀態和附近的充電位置等資訊。而且車主只要配戴智慧手錶靠近 Bolid-E,摩托車就會自動感應並解鎖。Bolid-E 還配備兩塊顯示螢幕智慧後照鏡,即時分析鏡頭所拍攝的後方影像,提醒車主可能的危險,提高駕駛的安全性。雖然 Bolid-E 各種智慧功能相當吸睛,但目前只是概念車,尚無任何上市規劃。

Energica Bolid-E。(圖片來源:)

Energica Bolid-E 有著顯示螢幕智慧後照鏡。(圖片來源:)

Energica Bolid-E。(圖片來源:)

在米蘭國際摩托車展上可以看到各個機車品牌隊投入電動機車所做的嘗試,範圍遍及電動速克達、電動自行車和電動重機等等。除了電動化之外,許多機車也走向智慧化,與智慧型裝置有了更多的連結。或許電動機車的發展還有很長的一段路要走,但從車展眾家品牌的摸索中,可以初窺電動機車的未來樣貌。

(合作媒體:。首圖來源:)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

橡樹嶺國家實驗室發表 120 千瓦電動車無線充電系統

手機無線充電越來越普及,那麼未來電動車是不是也能無線充電?美國能源部隸屬的橡樹嶺國家實驗室多年來一直都在研發車用無線充電技術,如今更進一步,功率從 2 年前的 20 千瓦(kilowatt)提升到 120 千瓦,無線充電速度可比擬特斯拉(Tesla)的超級充電樁,可說車用無線充電離商用化實現又更進一步。

橡樹嶺國家實驗室前次發表車用無線充電研究成果是在 2016 年 3 月底,研究合作夥伴包括豐田、思科(Cisco)、大功率無線充電設備商 Evatran,以及美國南卡羅萊納州克萊門森大學的國際汽車研究中心,當時發表的原型的充電功率為 20 千瓦,充電轉換效率為 90%。

時隔 2 年多之後,2018 年 10 月,橡樹嶺國家實驗室發表最新研發成果,功率提升到 120 千瓦,充電效率提升到 97%,與當前有線快速充電裝置相當,研發過程採用電腦模擬,在線圈設計等各方面同時最佳化,為了達到 120 千瓦大功率,同時又不至於大幅增加系統設備體積重量,採用最新的碳化矽電子元件。

無線充電系統的結構是由電網取得電力,轉換為高頻率的交流電,通過線圈產生磁場,磁場變化使得 6 英吋距離外的車上充電線圈產生交流電力,在經由整流器轉為直流電,供電給車子的電池組儲存。橡樹嶺國家實驗室目前的裝置是向車子充電,未來將研發可雙向的裝置,也就是不但可以電網對電動車充電,必要時也能電網從電動車汲取電力,使得所有的電動車都能成為電網的能源儲存資源。

橡樹嶺國家實驗室成功打造 120 千瓦的原型,使得能源部提出更大膽的目標:發展 350~400 千瓦的充電裝置,能在 15 分鐘內充飽汽車電池,因應這個新目標,橡樹嶺國家實驗室下一步將發展 200 千瓦的裝置,最後往 350 千瓦邁進,並強化動態充電技術,所謂動態充電,就是電動車不是停在無線充電裝置上充電,而是例如整段馬路都有無線充電功能,電動車可一面行駛一面充電,目前相關技術都還太過複雜昂貴,橡樹嶺國家實驗室希望能打造可以在高速公路行駛速度下還能充電的動態充電系統。

橡樹嶺國家實驗室目前正與可能的合作代工廠討論未來的商用生產,同時,民間也還有其他競爭者,車用無線充電新創公司 Hevo 已經在進行從原型進入量產的階段,計畫生產數千具產品,未來 18 個月內供應簽約的電力公司和車廠;麻州車用無線充電公司 WiTricity 則於 2018 年 10 月與安潔無線科技簽下授權合約,安潔無線將供應無線充電產品給中國車廠,WiTricity 的 Drive 11 充電技術,充電功率為 11 千瓦,充電效率為 93%。

即使動態充電還是遙遠的夢想,但對市區交通來說,只要路口的馬路下都裝有靜態的無線充電裝置,等待紅綠燈時,就能自然充電,如此一來,電動車將可以整天行駛,永遠不用擔心電力耗盡,也不需到充電樁充電,比起終將需要去加油站加油的內燃機汽車還更方便,甚至車廠可因此推出電池容量較小的版本,而能大為減低成本,如此一來,將可革命性的推動電動車的發展。

(合作媒體:。圖片來源:)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

第九屆中國國際新能源暨智慧汽車論壇 2019

2019 年 4 月 2-3 日∣中國·上海

新能源時代,攜手智慧化未來

2019 年是實施「十三五」規劃的重要一年,《中共中央關於制定國民經濟和社會發展第十三個五年規劃的建議》把新能源汽車推廣列入國家的重要計畫之中,要求提高電動汽車產業化水準。這表明在「十三五」期間,新能源汽車發展在整個國民經濟和社會發展中將處在十分重要的地位,明確了新能源汽車在國民經濟和社會發展中的戰略定位。「十三五」期間,中國將成為世界最大的新能源汽車市場,成為世界新能源汽車的核心主戰場。

在過去八屆新能源汽車論壇成功舉辦的基礎上,由希邁商務諮詢(上海)有限公司主辦的第九屆中國國際新能源暨智慧汽車論壇 2019 即將於 4 月 2 日- 4 月 3 日在上海隆重舉行。新能源汽車系列論壇成功邀請了包括國家發改委能源研究所、世界電動車協會、亞太電動車協會、世界氫能協會、世界分散式能源聯盟、中國工程院等在內的政府單位與研究機構,以及包括 BMW、賓士、奇瑞捷豹路虎、Volkswagen、奧迪、比亞迪、上汽、北汽,大陸,電裝,LG 等在內的知名整車商及綜合零部件商,共同研討新能源汽車產業政策趨勢、技術路線及難點、基礎設施建設、商業模式等並取得了豐碩的成果,獲得了業內外人士的一致好評。

在即將到來的 2019 年,組委會為感謝業內外人士對系列論壇長期以來的支援和關注,將傾情奉上相比歷屆舉辦規模最大的第九屆新能源汽車論壇,涉及 8 個論壇,CEO TALK,頒獎典禮,一對一洽談及雞尾酒會。屆時將誠邀全球範圍內的整車製造商、動力總成公司、動力電池及燃料電池廠商、充電、儲能企業、零部件供應商、核心技術服務提供者和政府官員等近 900 位產業人士一起,對新能源汽車產業面臨的挑戰,機遇與對策各方面進行為期 2 天更深層次並具有建設和戰略性的探討。

會議亮點

Ø  豐富的內容:

大板塊的深度解析

Ø  參會嘉賓:

900+ 高度滿意的企業決策者,200+ 業內知名企業,30+ 國家和地區

Ø  參會嘉賓分析:

17%+ 來自各國政府部門及權威機構,25%+ 來自知名整車商

Ø  演講嘉賓:

70世界新能源汽車產業知名發言嘉賓

Ø  交流機會:

16+ 小時的交流機會:圓桌討論、VIP 午宴和開放式問答

Ø  會議形式:

個論壇,場雞尾酒晚宴 + 一對一洽談,個頒獎典禮,個 CEO Talk

 

會議結構

若您對活動有更多要求,請撥打 021-6045 6030 與我們聯繫,謝謝理解和支持!

我們期待與貴單位一起出席於 2019 年 月 – 3 上海舉辦的第九屆中國國際新能源暨智慧汽車論壇, 以利決策!

 

欲知更多會議詳情,請登陸官方網站:

連絡人:Latika LIU(劉小姐)

電話:021-6045 6030

傳真:021-6047 5887

郵箱:

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

多線程高併發編程(11) — 非阻塞算法實現ConcurrentLinkedQueue源碼分析

  一.背景

  要實現對隊列的安全訪問,有兩種方式:阻塞算法和非阻塞算法。阻塞算法的實現是使用一把鎖(出隊和入隊同一把鎖ArrayBlockingQueue)和兩把鎖(出隊和入隊各一把鎖LinkedBlockingQueue)來實現;非阻塞算法使用自旋+CAS實現。

  
阻塞,顧名思義:當我們的生產者向隊列中生產數據時,若隊列已滿,那麼生產線程會暫停下來,直到隊列中有可以存放數據的地方,才會繼續工作;而當我們的消費者向隊列中獲取數據時,若隊列為空,則消費者線程會暫停下來,直到容器中有元素出現,才能進行獲取操作。
  那麼對於非阻塞來說,非阻塞隊列的執行並不會被阻塞,無論是消費者的出隊,還是生產者的入隊。同時對於入隊和出隊,使用了向後推進策略(重新尋找頭或尾節點)保證併發的數據一致性。

  今天來探究下使用非阻塞算法來實現的線程安全隊列ConcurrentLinkedQueue,它是一個基於鏈接節點的無界線程安全隊列,採用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部,當我們獲取一個元素時,它會返回隊列頭部的元素。它採用了“wait-free”算法(即CAS算法)來實現。即當入隊時,插入的元素依次向後延伸,形成鏈表;而出隊時,則從鏈表的第一個元素開始獲取,依次遞增。

  ConcurrentLinkedQueue的類圖結構:

   從類圖中可以看到,ConcurrentLinkedQueue由head和tail節點組成,每個節點Node由節點元素item和指向下一個節點的引用next組成,節點與節點之間通過next關聯起來組成一張鏈表結構的隊列。

  二.源碼解析

  1. 構造方法

        private static class Node<E> {
            volatile E item;//元素
            volatile Node<E> next;//下一節點
    
            Node(E item) {//添加元素
                UNSAFE.putObject(this, itemOffset, item);
            }
    
            boolean casItem(E cmp, E val) {//cas修改元素
                return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
            }
    
            void lazySetNext(Node<E> val) {//添加節點
                UNSAFE.putOrderedObject(this, nextOffset, val);
            }
    
            boolean casNext(Node<E> cmp, Node<E> val) {//cas修改節點
                return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
            }
    
            private static final sun.misc.Unsafe UNSAFE;
            private static final long itemOffset;
            private static final long nextOffset;
    
            static {
                try {
                    UNSAFE = sun.misc.Unsafe.getUnsafe();
                    Class<?> k = Node.class;
                    //獲得元素的偏移位置
                    itemOffset = UNSAFE.objectFieldOffset
                        (k.getDeclaredField("item"));
                    //獲得下一節點的偏移位置
                    nextOffset = UNSAFE.objectFieldOffset
                        (k.getDeclaredField("next"));
                } catch (Exception e) {
                    throw new Error(e);
                }
            }
        }
        //頭節點
        private transient volatile Node<E> head;
        //尾節點
        private transient volatile Node<E> tail;
        public ConcurrentLinkedQueue() {
            //默認情況下head節點存儲的元素為空,tail節點等於head節點。
            head = tail = new Node<E>(null);
        }
        public ConcurrentLinkedQueue(Collection<? extends E> c) {
            Node<E> h = null, t = null;
            //遍歷集合
            for (E e : c) {
                checkNotNull(e);//檢查是否為空,如果為空拋出空指針異常
                //創建節點和將元素存儲到節點中
                Node<E> newNode = new Node<E>(e);
                if (h == null)//頭節點為空
                    h = t = newNode;//頭和尾節點是創建的節點
                else {
                    t.lazySetNext(newNode);//添加節點
                    t = newNode;//修改尾節點的標識
                }
            }
            //如果集合沒有元素,設置隊列的頭尾節點為空
            if (h == null)
                h = t = new Node<E>(null);
            head = h;//更新隊列的頭節點標識
            tail = t;//更新隊列的尾節點標識
        }
        private static void checkNotNull(Object v) {
            if (v == null)
                throw new NullPointerException();
        }    
  2. 入隊add:

    • 入隊操作主要做兩件事情,第一是將入隊節點設置成當前隊列尾節點的下一個節點;第二是更新tail節點,如果tail節點的next節點不為空,則將入隊節點設置成tail節點,如果tail節點的next節點為空,則將入隊節點設置成tail的next節點,所以tail節點不總是尾節點

    • 上面的分析讓我們從單線程入隊的角度來理解入隊過程,但是多個線程同時進行入隊情況就變得更加複雜,因為可能會出現其他線程插隊的情況。如果有一個線程正在入隊,那麼它必須先獲取尾節點,然後設置尾節點的下一個節點為入隊節點,但這時可能有另外一個線程插隊了,那麼隊列的尾節點就會發生變化,這時當前線程要暫停入隊操作,然後重新獲取尾節點。

    • 源碼解析:從下面可以看出,入隊永遠是返回true,所以不要通過返回值判斷是否入隊成功

      public boolean add(E e) {
              return offer(e);
          }
          public boolean offer(E e) {
              checkNotNull(e);//檢查是否為空
              //創建入隊節點,將元素添加到節點中
              final Node<E> newNode = new Node<E>(e);
              //自旋隊列CAS直到入隊成功
              // 1、根據tail節點定位出尾節點(last node);2、將新節點置為尾節點的下一個節點;3、casTail更新尾節點
              for (Node<E> t = tail, p = t;;) {
                  //p是尾節點,q得到尾節點的next
                  Node<E> q = p.next;
                  //如果q為空
                  if (q == null) {
                      //p是last node,將尾節點的next修改為創建的節點
                      if (p.casNext(null, newNode)) {
                          //p在遍歷後會變化,因此需要判斷,如果不相等即p != t = tail,表示t(= tail)不是尾節點,則將入隊節點設置成tail節點,更新失敗了也沒關係,因為失敗了表示有其他線程成功更新了tail節點
                          if (p != t)
                              casTail(t, newNode);//入隊節點更新為尾節點,允許失敗,因此t= tail並不總是尾節點
                          return true;//結束
                      }
                  }
                  //重新獲取head節點:多線程操作時,輪詢后p有可能等於q,此時,就需要對p重新賦值
                  //(多線程自引用的情況,只有offer()和poll()交替執行時會出現)
                  else if (p == q)
                      //因為併發下可能tail被改了,如果被改了,則使用新的t,否則跳轉到head,從鏈表頭重新輪詢,因為從head開始所有的節點都可達
                      p = (t != (t = tail)) ? t : head;//運行到這裏再繼續自旋遍歷
                  else
                      /**
                       * 尋找尾節點,同樣,當t不等於p時,說明p在上面被重新賦值了,並且tail也被別的線程改了,則使用新的tail,否則循環檢查p的下個節點
                       *  (多offer()情況下會出現)
                       * p=condition?result1:result2
                       *  滿足result1的場景為 :
                       *      獲取尾節點tail的快照已經過時了(其他線程更新了新的尾節點tail),直接跳轉到當前獲得的最新尾節點的地方
                       *  滿足result2的場景為:
                       *      多線程同時操作offer(),執行p.casNext(null, newNode)CAS成功后,未更新尾節點(未執行casTail(t, newNode)方法:兩種原因 1是未滿足前置條件if判斷 2是CAS更新失敗),直接找next節點
                       */
                      p = (p != t && t != (t = tail)) ? t : q;//運行到這裏再繼續自旋遍歷
              }
          }
    1. debug斷點測試案例:

      public static void main(String[] args) throws IndexOutOfBoundsException {
              ConcurrentLinkedQueue c = new ConcurrentLinkedQueue();
              new Thread(()->{
                  int i;
                  for(i=0;i<10;){
                      c.offer(i++);
                      Object poll = c.poll();//註釋或取消進行測試
                      System.out.println(Thread.currentThread().getName()+":"+poll);
                  }
              }).start();
              new Thread(()->{
                  int i;
                  for(i=200;i<210;){
                      c.offer(i++);
                      Object poll = c.poll();//註釋或取消進行測試
                      System.out.println(Thread.currentThread().getName()+":"+poll);
                  }
              }).start();
           }
    2. tail多線程的更新情況:通過p和t是否相等來判斷

  3. 出隊poll:

    • 從上圖可知,並不是每次出隊時都更新head節點,當head節點里有元素時,直接彈出head節點里的元素,而不會更新head節點。只有當head節點里沒有元素時,出隊操作才會更新head節點。採用這種方式也是為了減少使用CAS更新head節點的消耗,從而提高出隊效率。
    • 源碼解析:

      public E poll() {
              restartFromHead:
              //自旋
              for (;;) {
                  //獲得頭節點
                  for (Node<E> h = head, p = h, q;;) {
                      E item = p.item;//獲得頭節點元素
                      //如果頭節點元素不為null並且cas刪除頭節點元素成功
                      if (item != null && p.casItem(item, null)) {
                          //p被修改了
                          if (p != h) // hop two nodes at a time
                              // 如果p 的next 屬性不是null ,將 p 作為頭節點,而 q 將會消失
                              updateHead(h, ((q = p.next) != null) ? q : p);
                          return item;
                      }
                      //如果頭節點的元素為空或頭節點發生了變化,這說明頭節點已經被另外一個線程修改了。
                      // 那麼獲取p節點的下一個節點,如果p節點的下一節點為null,則表明隊列已經空了
                      // 如果 p(head) 的 next 節點 q 也是null,則表示沒有數據了,返回null,則將 head 設置為null
                      // 注意:updateHead 方法最後還會將原有的 head 作為自己 next 節點,方便offer 連接。
                      else if ((q = p.next) == null) {
                          updateHead(h, p);
                          return null;
                      }
                      //如果 p == q,說明別的線程取出了 head,並將 head 更新了。就需要重新開始獲取head節點
                      else if (p == q)
                          continue restartFromHead;
                      // 如果下一個元素不為空,則將頭節點的下一個節點設置成頭節點
                      else
                          p = q;
                  }
              }
          }
          final void updateHead(Node<E> h, Node<E> p) {
              if (h != p && casHead(h, p))
                  // 將舊的頭結點h的next域指向為h
                  h.lazySetNext(h);
          }
  4. 入隊和出隊操作中,都有p == q的情況,在下面這種情況中:

    • 在彈出一個節點之後,tail節點有一條指向自己的虛線,這是什麼意思呢?在poll()方法中,移除元素之後,會調用updateHead方法,其中有h.lazySetNext(h),可以看到,在更新完head之後,會將舊的頭結點h的next域指向為h,上圖中所示的虛線也就表示這個節點的自引用
    • 如果這時,再有一個線程來添加元素,通過tail獲取的next節點則仍然是它本身,這就出現了p == q的情況,出現該種情況之後,則會觸發執行head的更新,將p節點重新指向為head,所有“活着”的節點(指未刪除節點),都能從head通過遍歷可達,這樣就能通過head成功獲取到尾節點,然後添加元素了。

  5. 獲取首部元素peek:

    • 從圖中可以看到,peek操作會改變head指向,執行peek()方法后head會指向第一個具有非空元素的節點。
    • 源碼解析:
      // 獲取鏈表的首部元素(只讀取而不移除)
          public E peek() {
              restartFromHead:
              //自旋
              for (;;) {
                  for (Node<E> h = head, p = h, q;;) {
                      //獲得頭節點元素
                      E item = p.item;
                      //頭節點元素不為空或頭節點下一節點為空(表示鏈表只有一個節點)
                      if (item != null || (q = p.next) == null) {
                          updateHead(h, p);//更新頭節點標識
                          return item;
                      }
                      /如果 p == q,說明別的線程取出了 head,並將 head 更新了。就需要重新開始獲取head節點
                      else if (p == q)
                          continue restartFromHead;
                      // 如果下一個元素不為空,則將頭節點的下一個節點設置成頭節點
                      else
                          p = q;
                  }
              }
          }
  6. 判斷隊列是否為空isEmpty:

        public boolean isEmpty() {
                return first() == null;
        }
        Node<E> first() {
            restartFromHead:
            for (;;) {
                for (Node<E> h = head, p = h, q;;) {
                    //頭節點是否有元素
                    boolean hasItem = (p.item != null);
                    //頭節點有元素或當前鏈表只有一個節點
                    if (hasItem || (q = p.next) == null) {
                        updateHead(h, p);
                        return hasItem ? p : null;//頭節點有值返回節點,否則返回null
                    }
                    else if (p == q)
                        continue restartFromHead;
                    else
                        p = q;
                }
            }
        }
  7. 獲取個數size:在併發環境中,其結果可能不精確,因為整個過程都沒有加鎖,所以從調用size方法到返回結果期間有可能增刪元素,導致統計的元素個數不精確。【在隊列元素很多的時候,size()方法十分消耗性能和時間,只是單純的判斷隊列為空使用isEmpty()即可!!!】

        public int size() {
            int count = 0;
            // first()獲取第一個具有非空元素的節點,若不存在,返回null
            // succ(p)方法獲取p的後繼節點,若p == p的後繼節點,則返回head
            for (Node<E> p = first(); p != null; p = succ(p))
                //節點有元素數量+1
                if (p.item != null)
                    if (++count == Integer.MAX_VALUE)
                        break;
            return count;
        }
        //取下一節點
        final Node<E> succ(Node<E> p) {
            Node<E> next = p.next;
            //若p == p的後繼節點(自引用情況下會出現),則返回head
            return (p == next) ? head : next;
        }
    • 探討:為何 ConcurrentLinkedQueue 中需要遍歷鏈表來獲取 size 而不適用一個原子變量呢?

      • 這是因為使用原子變量保存隊列元素個數需要保證入隊出隊操作和操作原子變量是原子操作,而ConcurrentLinkedQueue 是使用 CAS 無鎖算法的,所以無法做到這個。
  8. 判斷元素是否包含contains:該方法和size方法類似,有可能返回錯誤結果,比如調用該方法時,元素還在隊列裏面,但是遍歷過程中,該元素被刪除了,那麼就會返回false。

        public boolean contains(Object o) {
            if (o == null) return false;
            for (Node<E> p = first(); p != null; p = succ(p)) {
                E item = p.item;
                // 若找到匹配節點,則返回true
                if (item != null && o.equals(item))
                    return true;
            }
            return false;
        }
  9. 刪除元素remove:

        public boolean remove(Object o) {
            //刪除的元素不能為null,
            if (o != null) {
                Node<E> next, pred = null;
                //遍歷,開始獲得頭節點,
                for (Node<E> p = first(); p != null; pred = p, p = next) {
                    boolean removed = false;//刪除的標識
                    E item = p.item;//節點元素
                    if (item != null) {
                        //節點的元素不等於要刪除的元素,獲取下一節點進行遍歷循環操作
                        if (!o.equals(item)) {
                            next = succ(p);//將當前遍歷的節點移到下一節點
                            continue;
                        }
                        //節點元素等於刪除元素,CAS將節點元素置為null
                        removed = p.casItem(item, null);
                    }
                    next = succ(p);//獲取刪除節點的下一節點,
                    //有前節點和後置節點
                    if (pred != null && next != null) // unlink
                        pred.casNext(p, next);//刪除當前節點,即當前節點移除出隊列
                    if (removed)//元素刪除了返回true
                        return true;
                }
            }
            return false;
        }

  三.總結

  • 使用 CAS 原子指令來處理對數據的併發訪問,這是非阻塞算法得以實現的基礎。
  • head/tail 並非總是指向隊列的頭 / 尾節點,也就是說允許隊列處於不一致狀態。 這個特性把入隊 / 出隊時,原本需要一起原子化執行的兩個步驟分離開來,從而縮小了入隊 / 出隊時需要原子化更新值的範圍到唯一變量。這是非阻塞算法得以實現的關鍵。
  • 由於隊列有時會處於不一致狀態。為此,ConcurrentLinkedQueue 使用三個不變式來維護非阻塞算法的正確性。
  • 以批處理方式來更新 head/tail,從整體上減少入隊 / 出隊操作的開銷。
  • 為了有利於垃圾收集,隊列使用特有的 head 更新機制;為了確保從已刪除節點向後遍歷,可到達所有的非刪除節點,隊列使用了特有的向後推進策略。

  四.參考

  • https://blog.csdn.net/qq_38293564/article/details/80798310
  • https://www.ibm.com/developerworks/cn/java/j-lo-concurrent/index.html

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

iOS開發實踐-OOM治理

概覽

說起iOS的OOM問題大家第一想到的應該更多的是內存泄漏(Memory Leak),因為無論是從早期的MRC還是2011年Apple推出的ARC內存泄漏問題一直是iOS開發者比較重視的問題,比如我們熟悉的 Instruments Leaks 分析工具,Xcode 8 推出的 Memory Graph 等都是官方提供的內存泄漏分析工具,除此之外還有類似於FBRetainCycleDetector的第三方工具。不過事實上內存泄漏僅僅是造成OOM問題的一個原因而已,實際開發過程中造成OOM的原因有很多,本文試圖從實踐的角度來分析造成OOM的諸多情況以及解決辦法。

造成OOM的原因

造成OOM的直接原因是iOS的 Jetsam 機製造成的,在Apple的 Low Memory Reports中解釋了具體的運行情況:當內存不足時,系統向當前運行中的App發起applicationDidReceiveMemoryWarning(_ application: UIApplication) 調用和 UIApplication.didReceiveMemoryWarningNotification 通知,如果內存仍然不夠用則會殺掉一些後台進程,如果仍然吃緊就會殺掉當前App。

關於 Jetsam 實現機制其實蘋果已經開源了XNU代碼,可以在這裏查看,核心代碼在 kern_memorystatus 感興趣可以閱讀,其中包含了很多系統調用函數,可以幫助開發者做一些OOM監控等。

一、內存泄漏

內存泄漏造成內存被持久佔用無法釋放,對OOM的影響可大可小,多數情況下並非泄漏的類直接造成大內存佔用而是無法釋放的類引用了比較大的資源造成連鎖反應最終形成OOM。一般分析內存泄漏的工具推薦使用Leaks,後來Apple提供了比較方便的Memory Graph。

Instruments Leaks

Leaks應該是被所有開發者推薦的工具,幾乎搜索內存泄漏就會提到這個工具,但是很多朋友不清楚其實當前Leaks的作用沒有那麼大,多數時候內存泄漏使用Leaks是分析不出來的。不妨運行下面的一個再簡單不過的泄漏情況(在一個導航控制器Push到下面的控制器然後Pop出去進行驗證):

class Demo1ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.customView.block = {
            print(self.view.bounds)
        }
        self.view.addSubview(self.customView)
    }
    
    private lazy var customView:CustomView = {
        let temp = CustomView()
        
        return temp
    }()

    deinit {
        print("Demo1ViewController deinit")
    }
}


class CustomView:UIView {
    var block:(()->Void)?
}

上面這段代碼有明顯的循環引用造成的內存泄漏,但是前面說的兩大工具幾乎都無能為力,首先Leaks是:

網絡上有大量的文章去介紹Leaks如何使用等以至於讓有些同學以為Leaks是一個無所不能的內存泄漏分析工具,事實上Leaks在當前iOS開發環境下檢測出來的內存泄漏比較有限。之所以這樣需要先了解一個App的內存包括哪幾部分:

  1. Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).

  2. Abandoned memory: Memory still referenced by your application that has no useful purpose.

  3. Cached memory: Memory still referenced by your application that might be used again for better performance.

Leaked memory正是Leaks工具所能發現的內存,這部分內存屬於沒有任何對象引用的內存,在內存活動圖中是是不可達內存。

Abandoned memory在應用內存活動圖中存在,但是因為應用程序邏輯問題而無法再次訪問的內存。和內存泄漏最主要的區別是它的引用(包括強引用和弱引用)是存在的,但是不會再用了。比如上面的循環引用問題,VC被Pop后這部分內存首先還是在內存活動圖中的,但是下次再push我們是創建一個新的VC而非使用原來的VC就造成上一次的VC成了廢棄的內存。

如果是早期MRC下創建的對象忘記release之類的使用Leaks是比較容易檢測的,但是 ARC 下就比較少了,實際驗證過程中發現更多的是引用的一些古老的OC庫有可能出現,純Swift幾乎沒有。

Abandoned memory事實上要比leak更難發現,關於如何使用Instruments幫助開發者進行廢棄的內存分析,參見官方Allocations工具的使用:Find abandoned memory

Memory Graph

當然Xcode 8 的Memory Graph也是一大利器,不過如果你這麼想上面的問題很有可能會失望(如下圖),事實上Memory Graph我理解有幾個問題:第一是這個工具要想實際捕獲內存泄漏需要多運行幾次,往往一次運行過程是無法捕獲到內存泄漏的;第二比如上面的子視圖引起的內存泄漏是無法使用它捕獲內存泄漏信息的,VC pop之後它會認為VC沒有釋放它的子視圖沒有釋放也是正確的,事實上VC就應該是被釋放的,不過調整一下上面的代碼比如刪除self.view.addSubview(self.customView)后儘管還存在循環引用但是卻是可以檢測到的(不過實際上怎麼可能那麼做呢),關於這個玄學問題沒有找到相關的說明文檔來解釋。但是事實上 Memory graph 從來也沒有聲明自己是在解決內存泄漏問題,而是內存活動圖分析工具,如果這麼去想這個問題似乎也不算是什麼bug。

第三方工具

事實上看到上面的情況相信很多同學會想要使用第三方工具來解決問題,比如大家用的比較多的MLeaksFinder和PLeakSniffer,兩者不同之處是後者除了可以默認查出 UIViewController 和 UIView 內存泄漏外還可以查出所有UIViewController屬性的內存泄漏算是對前者的一個補充。當然前者還配合了 Facebook 的FBRetainCycleDetector可以分析出循環引用出現的引用關係幫助開發者快速修復循環引用問題。

不過可惜的是這兩款工具,甚至包括 PLeakSniffer 的 Swift 版本都是不支持 Swift 的(準確的說是不支持Swift 4.2,原因是Swift 4.2繼承自 NSObject 的類不會默認添加 @objc 標記 class_copyPropertyList無法訪問其屬性列表,不僅如此Swift5.x中連添加 @objcMembers 也是沒用的),但是 Swift 不是到了5.x才ABI穩定的嗎?,再次查看 Facebook 的 FBRetainCycleDetector 本身就不不支持Swift,具體可以查看這個issue這是官方的回答,如果稍微熟悉這個庫原理的同學應該也不難發現具體的原因,從目前的情況來看當前 FBRetainCycleDetector 的原理在當前swift上是行不通的,畢竟要獲取對象布局以及屬性在Swift 5.x上已經不可能,除非你將屬性標記為@objc,這顯然不現實,走 SWift 的Mirror當前又無法 setValue,所以研究了一下現在開源社區的情況幾乎沒有類似OC的完美解決方案。

Deubgger的LeakMonitorService

LeakMonitorService是我們自己實現的一個Swift內存泄漏分析工具,主要是為了解決上面兩個庫當前運行在Swift 5.x下的問題,首先明確的是當前 Swift 版本是無法訪問其非 @objc 屬性的,這就無法監控所有屬性,但是試想其實只要這個監控可以解決大部分問題它就是有價值的,而通常的內存泄漏也就存在於 UIViewController 和 UIView 中,因此出發點就是檢測 UIViewController 和其根視圖和子視圖的內存泄漏情況。

如果要檢測內存泄漏就要先知道是否被釋放,如果是OC只要Swizzle dealloc方法即可,但是顯然Swift中是無法Swizzle一個deinit方法的,因為這個方法本身就不是runtime method。最後我們確定的解決方案就是通過關聯屬性進行監控,具體的操作(具體實現後面開源出來):

  1. 使用一個集合Objects記錄要監控存在內存泄漏的對象
  2. 給NSObject添加一個關聯屬性:deinitDetector,類型為 Detector 作為NSObject的代理,Detector是一個class,裏面引用一個block,在 deinit 時調用這個 block 從Objects 中移除監控對象
  3. 在 UIViewController 初始化時給 deinitDetector 賦值進行監控,同時將自身添加到 Objects 數組代表可能會發生內存泄漏,在 UIViewController 的將要釋放時檢測監控(一般稍微延遲一會)檢測Objects是否存在當前對象如果是被正確釋放因為其屬性deinitDetector 會將其從 Objects 移除所以就不會有問題,如果出現內存泄漏deinitDetector的內部block不會調用,此時當前控制器還在 Objects 中說明存在內存泄漏
  4. 使用同樣的方法監控UIViewController的根視圖和子視圖即可

需要說明的是監控UIViewController的時機,通常建議添加監控的時機放到viewDidAppear(),檢測監控的時機放到viewDidDisappear()中。原因是此時子視圖相對來說已經完成布局(避免存在動態添加的視圖沒有被監控到),而檢測監控的時機放到viewDidDisappear()中自然也不是所有調用了viewDidDisappear()的控制器就一定釋放了,可以在viewDidDisappear()中配合isMovingFromParentisBeingDismissed屬性進行比較精準的判斷。

常見的內存泄漏

經過 LeakMonitorService 檢測確實在產品中發現了少量的內存泄漏情況,但是很有代表性,這裏簡單的說一下,當然普通的block循環引用、NSTimer、NotificationCenter.default.addObserver()等這裏就不在介紹了,產品檢測中幾乎也沒有發現。

1.block的雙重引用問題

先來看一段代碼:

class LeakDemo2ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let customView = CustomView()
        customView.block1 = {
            [weak self] () -> CustomSubView? in
            guard let weakSelf = self else { return nil }
            let customSubview = CustomSubView()
            customSubview.block2 = {
                 // 儘管這個 self 已經是 weak 了但是這裏也會出現循環引用
                print(weakSelf)
            }
            return customSubview
        }
        
        self.view.addSubview(customView)
    }
    
    deinit {
        print("LeakDemo2ViewController deinit")
    }

}

private class CustomView:UIView {
    var block1:(()->CustomSubView?)?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        if let subview = block1?() {
            self.addSubview(subview)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

private class CustomSubView:UIView {
    var block2:(()->Void)?
}

上面的代碼邏輯並不複雜,customView 的 block 內部已經考慮了循環引用將 self 聲明為 weak 是沒有問題的,出問題的是它的子視圖又嵌套了一個 block2 從而造成了 block2 的嵌套引用關係,而第二個 block2 又引用了 weakSelf 從而造成循環引用(儘管此時的self是第一個 block 內已經聲明成 weakSelf)解決的辦法很簡單隻要內部的 block2 引用的 self 聲明成weak就好了(此時形成的是[weak weakSelf]的關係)。那麼為什麼會這樣的,內部 block2 訪問的也不是當前VC的self對象,而是弱引用怎麼會出問題呢?

原因是當前控制器 self 首先強引用了customView,而customView又通過 addSubview() 強引用了customSubView,這樣依賴其實 self 已經對 customSubView形成了強引用關係。但是 customSubview 本身引用的弱引用weakSelf嗎?(注意是弱引用的weakSelf,不是weakSelf的弱引用),但是需要清楚一點就是外部的弱引用是block1對self的弱引用,也就是在weak table(Swift最新實現在Side table)裏面會記錄block1的弱引用關係,但是block2是不會在這個表中的,所以這裏還是一個強引用,最終造成循環引用關係。

Swift中的weakSelf和strongSelf

補充一下OC中的weakSelf和strongSelf的內容,通常情況下常見的做法:

__weak __typeof__(self) weakSelf = self;
[self.block = ^{
    __strong __typeof(weakSelf)strongSelf = weakSelf;
    if (strongSelf) {
        strongSelf.title = @"xxx";
    }
}];

當然你可以用兩個宏簡化上面的操作:

@weakify(self);
[self.block = ^{
	 @strongify(self);
    if (strongSelf) {
        self = @"xxx";
    }
}];

上面 strongSelf 的主要目的是為了避免block中引用self的方法在執行過程中被釋放掉造成邏輯無法執行完畢,swfit中怎麼做呢,其實很簡單(method1和method2要麼都執行,要麼一個也不執行):

self.block = {
    [weak self] in
    if let strongSelf = self {
        strongSelf.method1()
        strongSelf.method2()
    }
}

但是下面的代碼是不可以的(有可能會出現method2不執行,但是method1會執行的情況):

self.block = {
    [weak self] in
    self?.method1()
    self?.method2()
}

2.delay操作

通常大家都很清楚 NStimer 會造成循環引用(儘管在新的api已經提供了block形式,不必引用target了),但是很少注意 DispatchQueue.main.asyncAfter() 所實現的delay操作,而它的返回值是 DispatchWorkItem 類型通常可以用它來取消一個延遲操作,不過一旦對象引用了 DispatchWorkItem 而在block中又引用了當前對象就形成了循環引用關係,比如:

class LeakDemo3ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.delayItem = DispatchWorkItem {
            print("asyncAfter invoke...\(self)")
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3), execute: self.delayItem!)
    }
    
    deinit {
        print("LeakDemo3ViewController deinit")
    }
    
    private var delayItem:DispatchWorkItem?

}

3.內部函數

其實,如果是閉包大家平時寫代碼都會比較在意避免循環引用,但是如果是內部函數很多同學就沒有那麼在意了,比如下面的代碼:

class LeakDemo4ViewController: UIViewController {

    var block:(()->Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        func innerFunc() {
            print(self)
        }
        
        self.block = {
            [weak self] in
            guard let weakSelf = self else { return }
            innerFunc()
            print(weakSelf)
        }
    }
    
    deinit {
        print("LeakDemo4ViewController deinit")
    }

}

innerfunc() 中強引用了self,而 innerFunc 執行上下文是在block內進行的,所以理論上在block內直接訪問了self,最終造成循環引用。內部函數在swift中是作為閉包來執行的,上面的代碼等價於:

let innerFunc =  {
    print(self)
}

說起block的循環引用這裏可以補充一些情況不會造成循環引用或者是延遲釋放的情況。特別是對於延遲的情況此次在產品中也做了優化,盡可能快速釋放內存避免內存峰值過高。

a.首先pushViewController()和presentViewController()本身是不會引用當前控制器的,比如說下面代碼不會循環引用:

let vc = CustomViewController()
vc.block = {
    print(self)
}
self.present(vc, animated: true) {
    print(self)
}

b.UIView.animation不會造成循環引用

UIView.animate(withDuration: 10.0) {
    self.view.backgroundColor = UIColor.yellow
}

c.UIAlertAction的handler不會引起循環引用(iOS 8 剛出來的時候有問題)

let alertController = UIAlertController(title: "title", message: "message", preferredStyle: UIAlertController.Style.alert)
let action1 = UIAlertAction(title: "OK", style: UIAlertAction.Style.default) { (alertAction) in
    print(self)
}
let action2 = UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel) { (alertAction) in
    print(self)
}
alertController.addAction(action1)
alertController.addAction(action2)
self.present(alertController, animated: true) {
    print(self)
}

d.DispatchQueue asyncAfter會讓引用延遲,這裏的引用也是強引用,但是當asynAfter執行結束會得到釋放,但是不及時

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10)) {
    print(self)
}

e.網絡請求會延遲釋放

如下在請求回來之前self無法釋放:

guard let url = URL(string:"http://slowwly.robertomurray.co.uk/delay/3000/url/http://www.google.co.uk
") else { return }
let dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
    print(self,data)
}
dataTask.resume()

f.其他單例對象有可能延遲釋放,因為單例本身對外部對象強引用,儘管外部對象不會強引用單例,不過釋放是延遲的

class SingletonManager {
    static let shared = SingletonManager()
    
    func invoke(_ block: @escaping (()->Void)) {
        DispatchQueue.global().async {
            sleep(10)
            block()
        }
    }
}

SingletonManager.shared.invoke {
    print(self)
}

Instruments Allocation

前面說過Leaks和Memory Graph的限制,使用監控UIViewController或者UIView的工具對多數內存進行監控,但是畢竟這是多數情況,有些情況下是無法監控到的,那麼此時配合Instruments Allocation就是一個比較好的選擇,首先它可以通過快照的方式快速查對比內存的增長點也就可以幫助分析內存不釋放的原因,另外可以通過它查看當前內存被誰佔用也就有利於幫助我們分析內存佔用有針對性行的進行優化。

首先要了解,當我們向操作系統申請內存時系統分配的內存並不是物理內存地址而是虛擬內存 VM Regions 的地址。每個進程擁有的虛擬內存的空間大小是一樣的,32位的進程可以擁有4GB的虛擬內存,64位進程則更多。當真正使用內存時,操作系統才會將虛擬內存映射到物理內存。所以理論上當兩個進程A和B默認擁有相同的虛擬內存大小,當B使用內存時發現物理內存已經不夠用在OSX上會將不活躍內存寫入硬盤,叫做 swapping out。但是在iOS上面會直接發出內存警告 Memory warning 通知App清理無用內存(事實上也會引入 Compressed memory 壓縮一部分內存,需要的時候解壓)。

當然要使用這個工具之前建議先了解這個工具對內存類別劃分:

  • All Heap Allocations :進程運行過程中堆上分配的內存,簡單理解就是實際分配的內存,包括所有的類實例,比如UIViewController、UIView、Foundation數據結構等。比如:
    • Malloc 512.00KiB: 分配的512k堆內存,類似還有 Malloc 80.00KiB
    • CTRun: Core Text對象內存
  • All Anonymous VM :主要包含一些系統模塊的內存佔用,以 VM: 開頭
    • VM:CG raster data:(光柵化數據,也就是像素數據。注意不一定是圖片,一塊显示緩存里也可能是文字或者其他內容。通常每像素消耗 4 個字節)
    • VM:Statck:棧內存(比如每個線程都會需要500KB)
    • VM:Image IO:(圖片編解碼緩存)
    • VM:IOSurface:用於存儲FBO、RBO等渲染數據的底層數據結構,是跨進程的,通常在CoreGraphics、OpenGLES、Metal之間傳遞紋理數據。
    • CoreAnimation: 動畫資源佔用內存
    • VM:IOAccelerator:圖片的CVPixelBuffer

需要注意,Allocations統計的 Heap Allocations & Anonymous VM(包括:All Heap AllocationsAll Anonymous VM) 並不包括非動態的內存,以及部分其他動態庫創建的VM Region(比如:WebKit,ImageIO,CoreAnimation等虛擬內存區域),相對來說是低於實際運行內存的。

為了進一步了解內存實際分配情況,這裏不妨藉助一下 Instruments VM Tracker 這個工具,對於前面說過虛擬內存,這個工具是可以對虛擬內存實際分配情況有直觀展示的。

Virtual memory(虛擬內存) = Dirty Memory(已經寫入數據的內存) + Clean Memory(可以寫入數據的乾淨的內存) + Compressed Memory(對應OSX上的swapped memory)

Dirty Memory : 包括所有 Heap 中的對象、以上All Anonymous VM以及每個framework的 _DATA 段和 _Dirty_Data 段

Clean Memory:可以寫數據的乾淨的內存,不過對於開發者是read-only,操作系統負責寫入和移除,比如:System Framework、Binary Executable佔用的內存,framework都有_DATA_CONST段(不過當使用framework時會變成 Dirty memory )

Compressed Memory:由於iOS系統是沒有 swapped memory 的,取而代之的是 Compressed Memory ,通過壓縮內存可以降低大概一半的內存。不過遇到內存警告釋放內存的時候情況就複雜了些,比如遇到內存警告后通常可以試圖壓縮內存,而這時開發者會在收到警告后釋放一部分內存,遇到釋放內存的時候內存很可能會從壓縮內存再解壓去釋放反而峰值會增加。

前面提到過 Jetsam 對於內存的控制機制,這裏需要明確它做出內存警告的依據是 phys_footprint,而發生內存警告后系統默認清理的內存是 Clean Memory 而不會清理 Dirty Memory,畢竟有數據的內存系統也不知道是否還有用,無法自動清理。

Resident Memory = Dirty Memory + Clean Memory that loaded in physical memory

Resident Memory:已經被映射到虛擬內存中的物理內存,但是注意只有 phys_footprint 才是真正消耗的物理內存,也正是 Jetsam 判斷內存警告的依據。

Memory Footprint:App 實際消耗的物理內存,Jetsam 判斷內存警告的依據,包括:Dirty Memory 、Compressed Memory、NSCache, Purgeable、IOKit used
和部分加載到物理內存的Clean memory。

如果簡單總結:
Instruments AllocationsHeap Allocations & Anonymous VM 是整個App佔用的一部分,它又分為 Heap Allocations 為開發者申請的內存,而 Anonymous VM 是系統分配內存(但是並不是不需要優化)。這部分儘管不是 App 的所有消耗內存但卻是開發者最關注的。

Instruments VM TrackerDirty MemorySwapped(對應iOS中的 Compressed Memory) 應該是開發者關注的主要內存佔用,比較接近於實際佔用內存,類似的是Xcode Navigator的內存也接近於最終的 Memory Footprint (多了調試佔用的內存而已一般可以認為是 App 實際佔用內存)

關於圖片的內存佔用有必要解釋一下:CGImage 持有原始壓縮格式DataBuffer(DataBuffer佔用本身比較小),通過類似引用計數管理真正的Image Bitmap Buffer,需要渲染時通過 RetainBytePtr 拿到 Bitmap Buffer 塞給VRAM(IOSurface),不渲染時 ReleaseBytePtr 釋 放Bitmap Buffer。通常在使用UIImageView時,系統會自動處理解碼過程,在主線程上解碼和渲染,會佔用CPU,容易引起卡頓。推薦使用ImageIO在後台線程執行圖片的解碼操作(可參考SDWebImageCoder)。但是ImageIO不支持webp。

二、持久化對象

很多時候內存泄漏確實可以很大程度上解決OOM問題,因為類似於UIViewController或者UIView中包含大量UIImageView的情況下,兩者不釋放很可能會有很大一塊關聯的內存得不到釋放造成內存泄漏。但是另一個問題是持久化對象,即使解決了所有內存泄漏的情況也並不代表就真正解決了內存泄漏問題,其中一個重要的因素就是持久化對象。

關於持久化對象這裏主要指的是類似於App進入后在主界面永遠不會釋放的對象,以及某些單例對象。象基本上基本上不kill整個app是無法釋放的,但是如果因為設計原因又在首頁有大量這樣的持久對象那麼OOM的問題理論上更加難以解決,因為此時要修改整個App結構幾乎是不可能的。

這裏簡單對非泄漏OOM情況進行分類:

  1. 首頁及其關聯頁面:比如首頁是UITabbarController相應的tab點擊之後也成為了持久化對象無法釋放
  2. 單例對象:特別是會加載一些大模型的單例,比如說單例中封裝了人臉檢測,如果人臉檢測模型比較大,首次使用人臉識別時加載的模型也會永遠得不到釋放
  3. 複雜的界面層級:Push、Pop是iOS常用的導航操作,但是如果界面設計過於複雜(甚至可以無限Push)那麼層級深了以後前面UINavigationController棧中的對象一直堆疊也會OOM
  4. 耗資源的對象:比如說播放器這種消耗資源的對象,理論上不會在同一個app內播放兩個音視頻,設計成單例反而是比較好的方案
  5. 圖片資源:圖片資源是app內最佔用內存的資源,一個不合適的圖片尺寸就可以導致OOM,比如一張邊長10000px的正方形圖片解碼后的大小是10000 * 10000 * 4 = 381M左右

首先說一下第一種情況,其實在早期iOS中(5.0及其之前的版本)針對以上情況有內存警lunload機制,通常在viewDidUnload()中釋放當前view,同時也是給開發者提供資源卸載的一個比較合適的時機,當UIViewController再次展示時會重新loadView(),而從iOS 6.0之後Apple建議相關操作放到didReceiveMemoryWarning()方法中,主要的原因是因為僅僅釋放當前根視圖並不會帶來大的內存釋放同時又造成了體驗問題,原本一個UITableView已經翻了幾頁了現在又要重新加載一遍。所以結論是在didReceiveMemoryWarning()放一些大的對象釋放操作,而不建議直接釋放view,但是不管怎麼樣一定要做恢復機制。實際的實踐是在我們的MV播放器中做了卸載操作,因為MV的預覽要經過A->B->C的push過程,A、B均包含了MV預覽播放器,而實際測試兩個播放器的內存佔用大概110M上下這是一部分很大的開銷,特別是對於iPhone 6等1g內存的手機。另外針對某個頁面有多個子控制器的情況避免一次加載所有的自控制器的情況,理想的情況是切換到對應的控制器時才會加載對應的控制器。

單例對象是另一種大內存持久對象,通常情況下對象本身佔用內存很有限,做成單例沒有什麼問題,但是這個對象引用的資源才是關注的重點,比如說我們產品中中有個主體識別模塊,依賴於一個AI模型,本身這個模塊也並非App操作的必經路徑,首次使用時加載,但是之後就不會釋放了,這樣一來對於使用過一次的用戶很有可能不再使用就沒必要一直佔用,解決的辦法自然是不用單例。

關於複雜的界面層級則完全是設計上的問題,只能通過界面交互設計進行控制,而對於耗資源對象上面也提到了盡量復用同一個對象即可,這裏不再贅述。

此外,前面說到FBO相關的內存,其實這部分內存也是需要手動釋放的,比如在產品中使用的播放器在用完之後並沒有及時釋放,調用 CVOpenGLESTextureCacheFlush() 及時清理(類似的還有使用基於OpenGL的濾鏡)。

內存峰值飆升

除了持久的內存佔用意外,有時會不恰當的操作會造成內存的飆升出現OOM,儘管這部分內存可能一會會被釋放掉不會長久的佔用內存但是內存的峰值本身就是很危險的操作。

圖片壓縮

首先重點關注一下圖片的內存佔用,圖片應該是最佔用內存的對象資源,理論上UILayer最終展示也會繪製一個bitmap,不過這裏主要說的是UIImage資源。一張圖片要最終展示出來要經過解碼、渲染的步驟,解碼操作的過程就是就是從data到bitmap的過程,這個過程中會佔用大量內存,因為data是壓縮對象,而解碼出來的是實實在在的像素信息。自然在開發中重用一些控件、做圖片資源優化是必要的,不過這些事實上在我們的產品中都是現成的內容,如何進一步優化是我們最關注的的。理論上這個問題可以歸結到第一種情況的範疇,就是如何讓首頁的圖片資源盡可能的小,答案也是顯而易見的:第一解碼過程中盡可能控制峰值,第二能用小圖片的絕不解碼一張大圖片。

比如一個圖片壓縮需求一張巨大的圖片要判斷圖片大小做壓縮處理,假設這張圖片是1280 * 30000的長圖,本來的目的是要判斷圖片大小進行適當的壓縮,比如說超過50M就進行80%壓縮,如果100M就進行50%壓縮,但是遇到的情況是這樣的:本來為了判斷圖片的大小以及保留新的圖片,原圖片A內存佔用大約146M,聲明了一個新對象B保留壓縮后的圖片,但是默認值是A原圖,根據情況給B賦值,實際情況是原圖146M+146M+中間壓縮結果30M左右,當前內存322M直接崩潰。優化這個操作的過程自然是盡量少創建中間變量,也不要賦值默認值,避免峰值崩潰。

關於產品中使用合適的圖片應該是多數app都會遇到的情況,比如首頁默認有10張圖,本來尺寸是比較小的UIImageView也沒有必要使用過大的圖片,不過實際情況很可能是通過後端請求的url來加載圖片。比如說一個64pt * 64pt的UIImageView要展示一個1080 * 1920 pixal的圖片內存佔用達在2x情況下多了126倍之多是完全沒必要的,不過後端的配置自然是不可信的,即使剛開始沒有問題說不準後面運營維護的時候上一張超大的圖片也是很有可能的。解決方式自然是向下採樣,不過這裏建議不要直接使用Core Graphics繪製,避免內存峰值過高,Apple也給了推薦的做法。

常見的壓縮方法:

func compressImage(_ image:UIImage, size:CGSize) -> UIImage? {
        let targetSize = CGSize(width: size.width*UIScreen.main.scale, height: size.height*UIScreen.main.scale)
        UIGraphicsBeginImageContext(targetSize)
        image.draw(in: CGRect(origin: CGPoint.zero, size: targetSize))
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return newImage
    }

推薦的做法:

func downsamplingImage(url:URL, size:CGSize) -> UIImage? {
        let imageSourceOptions = [kCGImageSourceShouldCache:false] as CFDictionary
        guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, imageSourceOptions) else { return nil }
        let maxDimension = max(size.width, size.height) * UIScreen.main.scale
        let downsamplingOptions = [
            kCGImageSourceCreateThumbnailFromImageAlways : true,
            kCGImageSourceShouldCacheImmediately : true ,
            kCGImageSourceCreateThumbnailWithTransform:true,
            kCGImageSourceThumbnailMaxPixelSize : maxDimension
        ] as CFDictionary
        guard let downsampleImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsamplingOptions) else { return nil }
        let newImage = UIImage(cgImage: downsampleImage)
        return newImage
    }

大量循環操作

此外關於一些循環操作,如果操作本身比較耗內存,通常的做法就是使用 autoreleasepool 確保一個操作完成后內存及時釋放,但是在PHImageManager獲取圖片時這種方法並不是太湊效。比如說下面的一段代碼獲取相冊中30張照片保存到沙盒:

guard let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first else { return }
let assets = getAssets() // top 30
for i in 0..<assets.count {
    let option = PHImageRequestOptions()
    option.isSynchronous = false
    option.isNetworkAccessAllowed = true
    PHImageManager.default().requestImage(for: assets[i], targetSize: CGSize(width: 1080, height: 1920), contentMode: PHImageContentMode.aspectFit, options: option) { (image, info) in
        if info?[PHImageResultIsDegradedKey] as? Bool == true {
            return
        }
        if let image = image {
            do {
                let savePath = cachePath + "/\(i).png"
                if FileManager.default.fileExists(atPath: savePath) {
                    try FileManager.default.removeItem(atPath: savePath)
                }
                try image.pngData()?.write(to: URL(fileURLWithPath: savePath))
            } catch {
                print("Error:\(error.localizedDescription)")
            }
        }
    }
}

實測在iOS 13下面內存峰值85M左右,執行后內存65M,比執行前多了52M而且這個內存應該是會一直常駐,這也是網上很多文章中提到的增加autoreleasepool來及時釋放內存的原因。改造之後代碼:

guard let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first else { return }
let assets = getAssets()
for i in 0..<assets.count {
    autoreleasepool(invoking: {
        let option = PHImageRequestOptions()
        option.isSynchronous = false
        option.isNetworkAccessAllowed = true
        PHImageManager.default().requestImage(for: assets[i], targetSize: CGSize(width: 1080, height: 1920), contentMode: PHImageContentMode.aspectFit, options: option) { (image, info) in
            if info?[PHImageResultIsDegradedKey] as? Bool == true {
                return
            }
            if let image = image {
                do {
                    let savePath = cachePath + "/\(i).png"
                    if FileManager.default.fileExists(atPath: savePath) {
                        try FileManager.default.removeItem(atPath: savePath)
                    }
                    try image.pngData()?.write(to: URL(fileURLWithPath: savePath))
                } catch {
                    print("Error:\(error.localizedDescription)")
                }
            }
        }
    })
}

實測之後發現內存峰值降低到了65M左右,執行之後內存在50M左右,也就是峰值和之後常駐內存都有所降低,autoreleasepool有一定作用,但是作用不大,但是理論上這個常駐內存應該恢復到之前的10M左右的水平才對為什麼多了那麼多呢?原因是Photos獲取照片是有緩存的(注意在iPhone 6及以下設備不會緩存),這部分緩存如果進入後台會釋放(主要是IOSurface)。其實這個過程中內存主要包括兩部分 IOSurface 和 CG raster data ,那麼想要降低這兩部分內存其實針對上述場景最好的辦法是使用 PHImageManager.default().requestImageDataAndOrientation() 而不是 PHImageManager.default().requestImage() 實測上述情況內存峰值 18M 左右並且瞬間可降下來。那麼如果需求場景非要使用 PHImageManager.default().requestImage() 怎麼辦呢?答案是使用串行操作降低峰值。

guard let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first else { return }
let semaphore = DispatchSemaphore(value: 0)
self.semaphore = semaphore
DispatchQueue.global().async {
    let assets = self.getAssets()
    for i in 0..<assets.count {
        print(1)
        autoreleasepool(invoking: {
            let option = PHImageRequestOptions()
            option.isSynchronous = false
            option.isNetworkAccessAllowed = true
            PHImageManager.default().requestImageDataAndOrientation(for: assets[i], options: option) { (data, _, orientation, info) in
                if info?[PHImageResultIsDegradedKey] as? Bool == true {
                    return
                }
                defer {
                    semaphore.signal()
                    print(4)
                }
                do {
                    print(3)
                    let savePath = cachePath + "/\(i).png"
                    if FileManager.default.fileExists(atPath: savePath) {
                        try FileManager.default.removeItem(atPath: savePath)
                    }
                    try data?.write(to: URL(fileURLWithPath: savePath))
                } catch {
                    print("Error:\(error.localizedDescription)")
                }
            }
        })
        print(2)
        _ = semaphore.wait(timeout: .now() + .seconds(10))
        print(5)
        
    }
}

通過串行控制以後內存峰值穩定在16M左右,並且執行之後內存沒有明顯增長,但是相應的操作效率自然是下降了,整體時長增高。

總結

本文從內存泄漏和內存佔用兩個角度分析了解決OOM的問題,也是產品中實際遇到問題的一次徹查結果,列舉了常見引起OOM的原因,也對持久內存佔用給了一些實踐的建議,對於比較難發現的leak情況做了示例演示,也是產品實際遇到的,事實上在我們的產品中通過上面的手段OOM降低了80%以上,整體的App框架也並沒有做其他修改,所以有類似問題的同學不妨試一下。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

【Spring】Bean的LifeCycle(生命周期)

菜瓜:水稻,上次說Bean的LifeCycle,還沒講完

水稻:啥?說人話?

菜瓜:spring,bean,生命周期

水稻:。。。那你真的是很棒棒哦!。。。bean生命周期的話,從BeanFactory、ApplicationContext和FactoryBean開始說起

  • 我們知道(你不一定知道)BeanFactory是Spring訪問Bean容器的根接口,源碼的註釋: “The root interface for accessing a Spring bean container.”
  • 而ApplicationContext繼承自BeanFactory,也就是說它具有BeanFactory的屬性和方法,並進一步完善(繼承其他的接口)
  • FactoryBean跟前兩個關係就不怎麼大了,它是spring提供給用戶創建bean的口子,有一些bean創建過程複雜,或者依賴第三方包等(Mybatis-Spring中),可交給用戶自己創建

菜瓜:嗯。。陷入沉思。。(欲言又止)

水稻:講理論不給代碼就是耍流氓

  • package com.vip.qc.postprocessor;
    
    import org.springframework.beans.factory.FactoryBean;
    import org.springframework.stereotype.Component;
    
    /**
     * @author QuCheng on 2020/6/15.
     */
    @Component("fb")
    public class FactoryBeanT implements FactoryBean<FactoryBeanT.CustomBean> {
    
        public FactoryBeanT() {
            System.out.println("實現FactoryBean接口的類自身被放在IOC一級緩存的容器裏面,getObject的對象是在另一個緩存對象中");
        }
    
        @Override
        public CustomBean getObject() {
            return new CustomBean();
        }
    
        @Override
        public Class<?> getObjectType() {
            return CustomBean.class;
        }
    
        static class CustomBean {
            public CustomBean() {
                System.out.println("自定義bean");
            }
        }
    }
    
    測試方法
    @Test
    public void testTransa() {
        BeanFactory context = new AnnotationConfigApplicationContext(ComponentScanD.class);
        System.out.println("factoryBean : " + context.getBean("fb"));
        System.out.println("&factoryBean : " + context.getBean("&fb"));
    }

    測試結果

      實現FactoryBean接口的類自身被放在IOC一級緩存的容器裏面,getObject的對象是在另一個緩存對象中
      自定義bean
      factoryBean : com.vip.qc.postprocessor.FactoryBeanT$CustomBean@214b199c
      &factoryBean : com.vip.qc.postprocessor.FactoryBeanT@20d3d15a

菜瓜:懂了,BeanFactory是Spring的核心–容器,ApplicationContext則是包裹容器的上下文,豐富容器的功能(資源加載,事件驅動等)。FactoryBean也是Spring擴展性的提現

水稻:WC,你這個總結提到了精髓。就是擴展性:如果BeanFactory是核心思想,那麼其他的上下文,後置處理器,還是Aware接口等等,都是為了實現擴展

菜瓜:鋪墊說完了,開始生命周期唄

水稻:這次咱們反過來先看源碼,再看實驗,再總結

  • BeanFactory源碼註釋 – 定義了實現的生命周期
  •  * @author Rod Johnson
     * @author Juergen Hoeller
     * @author Chris Beams
     * @since 13 April 2001
     * @see BeanNameAware#setBeanName
     * @see BeanClassLoaderAware#setBeanClassLoader
     * @see BeanFactoryAware#setBeanFactory
     * @see org.springframework.context.ResourceLoaderAware#setResourceLoader
     * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher
     * @see org.springframework.context.MessageSourceAware#setMessageSource
     * @see org.springframework.context.ApplicationContextAware#setApplicationContext
     * @see org.springframework.web.context.ServletContextAware#setServletContext
     * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization
     * @see InitializingBean#afterPropertiesSet
     * @see org.springframework.beans.factory.support.RootBeanDefinition#getInitMethodName
     * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization
     * @see DisposableBean#destroy
     * @see org.springframework.beans.factory.support.RootBeanDefinition#getDestroyMethodName
     */
    public interface BeanFactory {
  • BeanFactory源碼實現類
  • public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory
    
    protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
            if (System.getSecurityManager() != null) {
                AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                    invokeAwareMethods(beanName, bean);
                    return null;
                }, getAccessControlContext());
            }
            else {
                // BeanNameAware BeanFactoryAware ...
                invokeAwareMethods(beanName, bean);
            }
    
            Object wrappedBean = bean;
            if (mbd == null || !mbd.isSynthetic()) {
                // BeanPostProcessor Before  @PostConstruct
                wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
            }
    
            try {
                // initMethod InitializingBean接口
                invokeInitMethods(beanName, wrappedBean, mbd);
            }
            catch (Throwable ex) {
                throw new BeanCreationException(
                        (mbd != null ? mbd.getResourceDescription() : null),
                        beanName, "Invocation of init method failed", ex);
            }
            if (mbd == null || !mbd.isSynthetic()) {
                // BeanPostProcessor after
                wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
            }
    
            return wrappedBean;
        }
  • 實驗代碼
  • package com.vip.qc.postprocessor;
    
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.config.BeanDefinition;
    import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
    import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
    import org.springframework.stereotype.Component;
    
    /**
     * @author QuCheng on 2020/6/14.
     */
    @Component
    public class BeanFactoryPostProcessorT implements BeanFactoryPostProcessor {
    
        public static final String BEAN_NAME = "initializingBeanT";
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            BeanDefinition initializingBeanT = beanFactory.getBeanDefinition(BEAN_NAME);
            System.out.println("BeanFactoryPostProcessor bean " + initializingBeanT.getBeanClassName());
        }
    }
    
    
    
    package com.vip.qc.postprocessor;
    
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.config.BeanPostProcessor;
    import org.springframework.stereotype.Component;
    
    /**
     * @author QuCheng on 2020/6/14.
     */
    @Component
    public class BeanPostProcessorT implements BeanPostProcessor {
    
        public static final String BEAN_NAMET = "initializingBeanT";
    
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            if (BEAN_NAMET.equals(beanName)) {
                InitializingBeanT processorT = ((InitializingBeanT) bean);
                System.out.println("BeanPostProcessor BeforeInitialization " + processorT);
            }
            return bean;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (BEAN_NAMET.equals(beanName)){
                InitializingBeanT processorT = ((InitializingBeanT) bean);
                System.out.println("BeanPostProcessor AfterInitialization " + processorT);
            }
            return bean;
        }
    
    }
    
    
    package com.vip.qc.postprocessor;
    
    import org.springframework.beans.factory.BeanNameAware;
    import org.springframework.beans.factory.DisposableBean;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    
    /**
     * @author QuCheng on 2020/6/14.
     */
    @Component
    public class InitializingBeanT implements BeanNameAware, InitializingBean, DisposableBean {
    
        public InitializingBeanT() {
            System.out.println("init無參構造 execute");
        }
    
        @PostConstruct
        public void postConstruct() {
            System.out.println("@PostConstruct  execute");
        }
    
        @PreDestroy
        public void preDestroy() {
            System.out.println("@PreDestroy  execute");
        }
    
        @Override
        public void afterPropertiesSet() {
            System.out.println("InitializingBean afterPropertiesSet --> " + this.toString());
        }
    
        @Override
        public void setBeanName(String name) {
            System.out.println("BeanNameAware : " + name);
        }
    
        @Override
        public void destroy() {
            System.out.println("destroy");
        }
    }
    
    
    測試代碼
        @Test
        public void testLifeCycle() {
            AbstractApplicationContext applicationContext = new AnnotationConfigApplicationContext(ComponentScanD.class);
            applicationContext.close();// 這裏不關閉容器的話,註銷bean的方法會看不到打印
        }

    測試結果

      BeanFactoryPostProcessor bean com.vip.qc.postprocessor.InitializingBeanT
      init無參構造 execute
      BeanNameAware : initializingBeanT
      BeanPostProcessor BeforeInitialization com.vip.qc.postprocessor.InitializingBeanT@15bb6bea
      @PostConstruct execute
      InitializingBean afterPropertiesSet –> com.vip.qc.postprocessor.InitializingBeanT@15bb6bea
      BeanPostProcessor AfterInitialization com.vip.qc.postprocessor.InitializingBeanT@15bb6bea
      @PreDestroy execute
      destroy

菜瓜:實現什麼的不重要,接口才是爸爸呀,BeanFactory定義好了生命周期,下面的實現也只是實現罷了

水稻:哈哈,你說的對,一流的公司賣標準

菜瓜:這裏怎麼沒看到循環依賴的處理啊

水稻:是的。這裏的源碼我只截取了bean初始化完成之後的接口調用。循環依賴的處理在它前面。來來來,繼續剛

菜瓜:剛不了剛不了,你一下子搞這麼多玩意給我看,我哪看得完

水稻:您歇着,下次您什麼時候想了解我再給您說

 

總結

  • BeanFactory已經定義了整個的生命周期,子類只是負責實現,demo演示也只是為了證實。我們更應該關注更上層的東西 
  • ApplicationContext是對容器更精細化的包裝,提供了更完善的功能
  • FactoryBean是Spring擴展性的提現,可供用戶自己定義創建bean。擴展性提煉的很好

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

如何 SSH 到 Linux 服務器里的特定目錄及執行命令?

你是不是有遇到過這樣的場景?使用 SSH 命令進入到服務器,然後再用 cd 命令進入到對應目錄,再繼續進行你的工作。

這種操作對於新手來講特別常見,良許之前也是這樣。在本文,老司機將帶你來進行更高效的操作,只需一步即可達到你想要的效果。

而且,不僅僅是實現快速進入到 Linux 服務器特定的目錄,還可以實現在連接上服務器的時候即執行一個對應的命令。

低效操作方式

如果你不知道本文介紹的方法,你很可能是分成兩步來操作的:

第一步:使用 SSH 命令進入到遠程服務器

ssh user@remote-system

第二步:使用 cd 命令進入到你想要的目錄

cd <some-directory>

一條命令快速進入到服務器指定目錄

上面提到的這種方式當然是可以的,但過於低效。這樣操作你需要使用兩條命令,但實際上,你完全可以使用一條命令即可實現你想要的效果,比如:

ssh -t pi@192.168.0.116 'cd /home/pi/tests ; bash'

通過這條命令,我們可以直接就進入到樹莓派(遠程服務器)中對應的目錄里(即 /home/pi/tests)。後續你就可以再繼續你的工作了。

在這裏, -t 選項是表示強制偽終端分配,即使標準輸入不是終端。如果不加的話,可能會有如下提示:

Pseudo-terminal will not be allocated because stdin

這裏我們再用一個動畫來直觀地演示這個過程:

除此之外,你還可以使用下面這個命令:

ssh -t pi@192.168.0.116 'cd /home/pi/tests ; exec bash'

或者:

ssh -t pi@192.168.0.116 'cd /home/pi/tests && exec bash -l'

在這裏,-l 選項將這個 bash 設置為登錄 shell。

在上面的三條命令里,最後的參數都是 bash,是因為我的遠程服務器默認的 shell 解釋器是 bash 。如果你不知道你遠程服務器所使用的 shell 解釋器,可以使用以下命令:

ssh -t pi@192.168.0.116 'cd /home/pi/tests && exec $SHELL'

一條命令遠程執行服務器命令

正如本文開頭所講的,我們不僅可以使用一條命令進入到遠程服務器指定目錄,還可以使用一條命令遠程執行服務器命令。甚至,我們還可以使用一條命令進入到遠程服務器的指定目錄,再執行一條命令。

其實所使用的方法都是一樣的,比如我們想進入到樹莓派的 /home/pi/tests 目錄,再執行 ls -al 命令,我們可以這樣輸入命令:

ssh -t pi@192.168.0.116 'cd /home/pi/tests && ls -al && exec $SHELL'

執行的結果如下:

[Alvin.Alvin-computer]  ssh -t pi@192.168.0.116 'cd /home/pi/tests && ls -al && exec $SHELL'
total 48
drwxr-xr-x  4 pi pi 4096 Apr  5 14:36 .
drwxr-xr-x 21 pi pi 4096 Apr 21 19:26 ..
drwxrwxrwx  7 pi pi 4096 Apr  5 17:28 GIC
drwxrwxrwx  3 pi pi 4096 Apr  5 17:37 gitchat
-rw-r--r--  1 pi pi  474 Apr  5 11:21 liangxu.json
-rwxr-xr-x  1 pi pi 8184 Mar 17 15:34 test
-rwxr-xr-x  1 pi pi 8184 Mar 17 15:34 test2
-rwxr-xr-x  1 pi pi 8184 Mar 17 15:34 test3
-rw-r--r--  1 pi pi  131 Mar 17 15:34 test.c

一個折中的方案

如果你覺得這條命令太長了不好敲,非要先進入到服務器,再 cd 到對應的目錄。那麼,我們可以修改遠程服務器的 .bashrc 文件。

vim ~/.bashrc

將你要執行的命令寫在裏面。比如在這個場景下,我們可以這樣加:

cd /home/pi/tests >& /dev/null

然後我們再執行 :wq 保存文件,再執行以下命令使更改生效:

source ~/.bashrc
或者
. ~/.bashrc

這樣,我們一進入到服務器后,就會自動進入到 /home/pi/tests 目錄里。如下動圖所示:

但是,這個有個明顯的弊端,就是我們只能進入到我們指定的目錄,如果要換成其它目錄,那隻能再改 .bashrc 文件了。

公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

node實現文件屬性批量修改(時間屬性)

前言

在默認情況下,一個文件的創建時間修改時間是系統自己設定的,我們不能修改該的。但我們有時為了某種特殊需要,為了不讓別人一眼看出文件已經給修改了,我們又需要修改文件的創建時間修改時間。那麼如何修改文件夾時間,如何修改文件的創建時間,如何批量修改文件的創建時間修改時間訪問時間呢?別著急,接下來就帶你自己修改他們。所以,閑話不多說啦,開始寫我們的代碼啦~~

ps:小工具推薦NewFileTime,以上簡述摘抄於NewFileTime

簡單的搭建一下

  • 新建一個 files 目錄

  • 初始化一個node項目工程

    npm init -y
    

看到這裏你會發現,其實我沒有安裝依賴,是因為原生的庫有這個自帶的功能嗎?說是也行,說不是也行。原生的utimes目前支持修改文件的修改時間訪問時間,不支持修改文件的創建時間,所以我們需要藉助一個第三方庫來修改。

為什麼不直接安裝這個第三方庫呢?

因為這個庫有些許特殊,分兩種情況,一個是低版本Node可以直接安裝,在我本機的Node13上運行則會失敗。具體原因嘛,可以看看下方的鏈接

ps: 原因 + 解決方案

所以,在低版本的Node我們可以直接npm install @ronomon/utimes,而在版本相對較高的則需要npm i https://github.com/Jule-/utimes.git#napi-migration

這裏也提一嘴,如果@ronomon/utimes安裝失敗的話,是因為這些原生Node拓展是需要編譯的,所以我們可能需要安裝windows-build-tools,即以管理員身份啟動PowerShell並運行:

npm install --global windows-build-tools

安裝完依賴之後就可以正式寫我們的代碼啦,其實這個代碼相對簡單,就是直接調用它的api就好了。

簡單的使用一下

  • 新建一個test-files文件夾

  • test-files文件夾新建1.txt文件供我們測試

  • 編寫如下代碼:

    // 導入 utimes
    const { utimes } = require("@ronomon/utimes");
    utimes(
      "./test-files/1.txt",
      // 創建時間
      +new Date("2010/01/01"),
      // 修改時間
      +new Date("2010/01/02"),
      // 訪問時間
      +new Date("2010/01/03"),
      (err) => {
        //  修改成功的回調
        console.log(`success`);
      }
    );
    
  • 運行代碼,node app.js,是都發現日期發生了改變呢?

看到這裏你以為是不是寫完了,其實也差不多了 ,不過我當然不會讓你收穫這麼少的,至少我們可以看看我們這個最最最簡單的例子的缺點,比如代碼沒有Promise化,那麼我們就封裝一下utimes

/**
 *
 * @param {String} path => 路徑
 * @param {Number} btime => 創建時間,不傳即不修改
 * @param {Number} mtime => 修改時間,不傳即不修改
 * @param {Number} atime => 訪問時間,不傳即不修改
 */
const utimesPromise = (path, btime, mtime, atime) => {
  return new Promise((resolve, reject) => {
    utimes(path, btime, mtime, atime, (err) => (err ? reject(err) : resolve()));
  });
};

當然- -,因為我們使用的是Node,所以我們不需要常規的用new Promise封裝,可以直接使用內置的util這個工具中的promisify方法封裝即可

util.promisify 是在 node.js 8.x 版本中新增的一個工具,用於將老式的 Error first callback 轉換為 Promise 對象,讓老項目改造變得更為輕鬆。在官方推出這個工具之前,民間已經有很多類似的工具了,比如 es6-promisifythenifybluebird.promisify。以及很多其他優秀的工具,都是實現了這樣的功能,幫助我們在處理老項目的時候,不必費神將各種代碼使用 Promise 再重新實現一遍。

所以,我們的封裝又變得更加簡單了,代碼如下:

const { promisify } = require("util");
const utimesPromise = promisify(utimes);

之前的代碼就可以改寫成之前我們那樣的自執行Async Function了,代碼如下:

// ...
(async () => {
  await utimesPromise(
    "./test-files/1.txt",
    // 創建事件
    +new Date("2010/01/01"),
    // 修改時間
    +new Date("2010/01/02"),
    // 訪問時間
    +new Date("2010/01/03")
  );
})();

寫到這裏,你會發現其實我們根本沒有做批量修改,是因為有了之前的經驗,我們可以直接通過glob這個工具獲取所有的路徑,根本不要我們操心,寫起來也十分簡單,所以我打算最後再來寫

  • 安裝glob

    npm i glob -S
    
  • 多建幾個文件用於測試我們的代碼

    得出下面列表:

  • 修改我們的代碼:

    const { utimes } = require("@ronomon/utimes");
    const glob = require("glob");
    const { promisify } = require("util");
    
    /**
     *
     * @param {String} path => 路徑
     * @param {Number} btime => 創建時間,不傳即不修改
     * @param {Number} mtime => 修改時間,不傳即不修改
     * @param {Number} atime => 訪問時間,不傳即不修改
     */
    const utimesPromise = promisify(utimes);
    
    (async () => {
      const paths = glob.sync("./test-files/**");
      const len = paths.length;
      for (let i = 0; i < len; i++) {
        await utimesPromise(
          paths[i],
          +new Date("2010/01/01"),
          +new Date("2010/01/02"),
          +new Date("2010/01/04")
        );
      }
    })();
    
  • 得出結果

這樣子就遞歸了我們所有的文件夾跟子文件了進行修改了,本來想着在加載名字修改的,但苦於- -沒有界面,篇幅也過長,就留着過幾天再寫了。

gitee 地址,github 地址

最後

感謝各位觀眾老爺的觀看 O(∩_∩)O 希望你能有所收穫

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

一文看懂《最大子序列和問題》

引言

在做KB的基礎DP練習題的時候遇到了最大子序列和的變種問題,突然發現自己以前沒做過解題筆記(現補上)

最大子序列和是一道經典的算法題, leetcode 也有原題《53.maximum-sum-subarray》,今天我們就來徹底攻克它。

題目描述

求取數組中最大連續子序列和,例如給定數組為 A = [1, 3, -2, 4, -5], 則最大連續子序列和為 6,即 1 + 3 +(-2)+ 4 = 6。

首先我們來明確一下題意。

  • 題目說的子數組是連續的
  • 題目只需要求和,不需要返回子數組的具體位置。
  • 數組中的元素是整數,但是可能是正數,負數和 0。
  • 子序列的最小長度為 1。

比如:

  • 對於數組 [1, -2, 3, 5, -3, 2], 應該返回 3 + 5 = 8
  • 對於數組 [0, -2, 3, 5, -1, 2], 應該返回 3 + 5 + -1 + 2 = 9
  • 對於數組 [-9, -2, -3, -5, -3], 應該返回 -2

解法一 – 暴力法(超時法)

一般情況下,先從暴力解分析,然後再進行一步步的優化。

思路

我們來試下最直接的方法,就是計算所有的子序列的和,然後取出最大值。
記 Sum[i,….,j]為數組 A 中第 i 個元素到第 j 個元素的和,其中 0 <= i <= j < n,
遍歷所有可能的 Sum[i,….,j] 即可。

我們去枚舉以 0,1,2…n-1 開頭的所有子序列即可,
對於每一個開頭的子序列,我們都去枚舉從當前開始到 n-1 的所有情況。

這種做法的時間複雜度為 O(N^2), 空間複雜度為 O(1)。

代碼

Java:

class MaximumSubarrayPrefixSum {
  public int maxSubArray(int[] nums) {
      int len = nums.length;
      int maxSum = Integer.MIN_VALUE;
      int sum = 0;
      for (int i = 0; i < len; i++) {
        sum = 0;
        for (int j = i; j < len; j++) {
          sum += nums[j];
          maxSum = Math.max(maxSum, sum);
        }
      }
      return maxSum;
  }
}

Python 3:

import sys
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n = len(nums)
        maxSum = -sys.maxsize
        sum = 0
        for i in range(n):
            sum = 0
            for j in range(i, n):
                sum += nums[j]
                maxSum = max(maxSum, sum)

        return maxSum

空間複雜度非常理想,但是時間複雜度有點高。怎麼優化呢?我們來看下下一個解法。

解法二 – 分治法

思路

我們來分析一下這個問題, 我們先把數組平均分成左右兩部分。

此時有三種情況:

  • 最大子序列全部在數組左部分
  • 最大子序列全部在數組右部分
  • 最大子序列橫跨左右數組

對於前兩種情況,我們相當於將原問題轉化為了規模更小的同樣問題。

對於第三種情況,由於已知循環的起點(即中點),我們只需要進行一次循環,分別找出
左邊和右邊的最大子序列即可。

所以一個思路就是我們每次都對數組分成左右兩部分,然後分別計算上面三種情況的最大子序列和,
取出最大的即可。

舉例說明,如下圖:

這種做法的時間複雜度為 O(N*logN), 空間複雜度為 O(1)。

代碼

Java:

class MaximumSubarrayDivideConquer {
  public int maxSubArrayDividConquer(int[] nums) {
      if (nums == null || nums.length == 0) return 0;
      return helper(nums, 0, nums.length - 1);
    }
    private int helper(int[] nums, int l, int r) {
      if (l > r) return Integer.MIN_VALUE;
      int mid = (l + r) >>> 1;
      int left = helper(nums, l, mid - 1);
      int right = helper(nums, mid + 1, r);
      int leftMaxSum = 0;
      int sum = 0;
      // left surfix maxSum start from index mid - 1 to l
      for (int i = mid - 1; i >= l; i--) {
        sum += nums[i];
        leftMaxSum = Math.max(leftMaxSum, sum);
      }
      int rightMaxSum = 0;
      sum = 0;
      // right prefix maxSum start from index mid + 1 to r
      for (int i = mid + 1; i <= r; i++) {
        sum += nums[i];
        rightMaxSum = Math.max(sum, rightMaxSum);
      }
      // max(left, right, crossSum)
      return Math.max(leftMaxSum + rightMaxSum + nums[mid], Math.max(left, right));
    }
}

Python 3 :

import sys
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        return self.helper(nums, 0, len(nums) - 1)
    def helper(self, nums, l, r):
        if l > r:
            return -sys.maxsize
        mid = (l + r) // 2
        left = self.helper(nums, l, mid - 1)
        right = self.helper(nums, mid + 1, r)
        left_suffix_max_sum = right_prefix_max_sum = 0
        sum = 0
        for i in reversed(range(l, mid)):
            sum += nums[i]
            left_suffix_max_sum = max(left_suffix_max_sum, sum)
        sum = 0
        for i in range(mid + 1, r + 1):
            sum += nums[i]
            right_prefix_max_sum = max(right_prefix_max_sum, sum)
        cross_max_sum = left_suffix_max_sum + right_prefix_max_sum + nums[mid]
        return max(cross_max_sum, left, right)

解法三 – 動態規劃

思路

我們來思考一下這個問題, 看能不能將其拆解為規模更小的同樣問題,並且能找出
遞推關係。

我們不妨假設問題 Q(list, i) 表示 list 中以索引 i 結尾的情況下最大子序列和,
那麼原問題就轉化為 Q(list, i), 其中 i = 0,1,2…n-1 中的最大值。

我們繼續來看下遞歸關係,即 Q(list, i)和 Q(list, i – 1)的關係,
即如何根據 Q(list, i – 1) 推導出 Q(list, i)。

如果已知 Q(list, i – 1), 我們可以將問題分為兩種情況,即以索引為 i 的元素終止,
或者只有一個索引為 i 的元素。

  • 如果以索引為 i 的元素終止, 那麼就是 Q(list, i – 1) + list[i]
  • 如果只有一個索引為 i 的元素,那麼就是 list[i]

分析到這裏,遞推關係就很明朗了,即Q(list, i) = Math.max(0, Q(list, i - 1)) + list[i]

舉例說明,如下圖:

這種算法的時間複雜度 O(N), 空間複雜度為 O(1)

代碼

Java:

class MaximumSubarrayDP {
  public int maxSubArray(int[] nums) {
     int currMaxSum = nums[0];
     int maxSum = nums[0];
     for (int i = 1; i < nums.length; i++) {
       currMaxSum = Math.max(currMaxSum + nums[i], nums[i]);
       maxSum = Math.max(maxSum, currMaxSum);
     }
     return maxSum;
  }
}

Python 3:

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n = len(nums)
        max_sum_ending_curr_index = max_sum = nums[0]
        for i in range(1, n):
            max_sum_ending_curr_index = max(max_sum_ending_curr_index + nums[i], nums[i])
            max_sum = max(max_sum_ending_curr_index, max_sum)

        return max_sum

解法四 – 數學分析

思路

我們來通過數學分析來看一下這個題目。

我們定義函數 S(i) ,它的功能是計算以 0(包括 0)開始加到 i(包括 i)的值。

那麼 S(j) – S(i – 1) 就等於 從 i 開始(包括 i)加到 j(包括 j)的值。

我們進一步分析,實際上我們只需要遍歷一次計算出所有的 S(i), 其中 i 等於 0,1,2….,n-1。
然後我們再減去之前的 S(k),其中 k 等於 0,1,i – 1,中的最小值即可。 因此我們需要
用一個變量來維護這個最小值,還需要一個變量維護最大值。

這種算法的時間複雜度 O(N), 空間複雜度為 O(1)。

其實很多題目,都有這樣的思想, 比如之前的《每日一題 – 電梯問題》。

代碼

Java:

class MaxSumSubarray {
  public int maxSubArray3(int[] nums) {
      int maxSum = nums[0];
      int sum = 0;
      int minSum = 0;
      for (int num : nums) {
        // prefix Sum
        sum += num;
        // update maxSum
        maxSum = Math.max(maxSum, sum - minSum);
        // update minSum
        minSum = Math.min(minSum, sum);
      }
      return maxSum;
  }
}

Python 3:

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n = len(nums)
        maxSum = nums[0]
        minSum = sum = 0
        for i in range(n):
            sum += nums[i]
            maxSum = max(maxSum, sum - minSum)
            minSum = min(minSum, sum)

        return maxSum

總結

我們使用四種方法解決了《最大子序列和問題》,
並詳細分析了各個解法的思路以及複雜度,相信下次你碰到相同或者類似的問題
的時候也能夠發散思維,做到一題多解,多題一解

實際上,我們只是求出了最大的和,如果題目進一步要求出最大子序列和的子序列呢?
如果要題目允許不連續呢? 我們又該如何思考和變通?如何將數組改成二維,求解最大矩陣和怎麼計算?
這些問題留給讀者自己來思考。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

石油需求觸頂是危言聳聽?能源巨擘不怕電動車威脅

  全球環保意識高漲,歐洲和中國相繼宣布,未來將禁售汽柴油車。不少專家預測,石油需求即將觸頂,但是能源巨擘對此嗤之以鼻,深信石油需求將持續成長,對再生能源投資只有石油的九牛一毛。   路透社8日報導,二十年前英國石油公司(BP)看好再生能源,不只改換商標,還宣布要在十年內斥資80億美元發展綠能。不料此一大膽舉動慘敗收場,BP的太陽能事業被陸廠打到無力招架,美國風力發電事業更連賣都賣不掉。BP學到教訓,再次聚焦石油,其他油商也看在眼裡,對於綠能投資格外謹慎。   證據何在?路透訪調分析顯示,全球前五大油商,包括BP、Total、雪佛龍(Chevron)、艾克森美孚(Exxon Mobil)、荷蘭殼牌(Royal Dutch Shell),投資替代能源都只是輕描淡寫。Wood Mackenzie估計前五大油商每年投資的1,000億美元中,只有3%用於再生能源。雪佛龍執行長John Watson說,目前沒有石油需求觸頂的跡象,未來10~20年,石油需求將持續成長。   能源巨擘信心滿滿,是看準新興市場的石油需求將持續增加。艾克森美孚估計,2040年亞洲的運輸需求將使得燃料需求提高25%。BP也說,全球生產石油中,有1/5用於汽車,如果電動車真的奪取大量市佔,空運、鐵路、卡車運輸仍會拉高石油需求。油商也大力投資天然氣,算準就算電動車起飛,天然氣能用於發電,需求仍會成長。   (本文內容由授權使用。首圖來源:pixabay)  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司“嚨底家”!

※推薦評價好的iphone維修中心

聚甘新