精美圖文講解Java AQS 共享式獲取同步狀態以及Semaphore的應用

| 好看請贊,養成習慣

  • 你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star

看到本期內容這麼少,是不是心動了呢?

前言

上一篇萬字長文 Java AQS隊列同步器以及ReentrantLock的應用 為我們讀 JUC 源碼以及其設計思想做了足夠多的鋪墊,接下來的內容我將重點說明差異化,如果有些童鞋不是能很好的理解文中的一些內容,強烈建議回看上一篇文章,搞懂基礎內容,接下來的閱讀真會輕鬆加愉快

AQS 中我們介紹了獨佔式獲取同步狀態的多種情形:

  • 獨佔式獲取鎖
  • 可響應中斷的獨佔式獲取鎖
  • 有超時限制的獨佔式獲取鎖

AQS 提供的模版方法裏面還差共享式獲取同步狀態沒有介紹,所以我們今天來揭開這個看似神秘的面紗

AQS 中的共享式獲取同步狀態

獨佔式是你中沒我,我中沒你的的一種互斥形式,共享式顯然就不是這樣了,所以他們的唯一區別就是:

同一時刻能否有多個線程同時獲取到同步狀態

簡單來說,就是這樣滴:

我們知道同步狀態 state 是維護在 AQS 中的,拋開可重入鎖的概念,我在上篇文章中也提到了,獨佔式和共享式控制同步狀態 state 的區別僅僅是這樣:

所以說想了解 AQS 的 xxxShared 的模版方法,只需要知道它是怎麼控制 state 的就好了

AQS共享式獲取同步狀態源碼分析

為了幫助大家更好的回憶內容,我將上一篇文章的兩個關鍵內容粘貼在此處,幫助大家快速回憶,關於共享式,大家只需要關注【騷紫色】就可以了

自定義同步器需要重寫的方法

AQS 提供的模版方法

故事就從這裏說起吧 (你會發現和獨佔式驚人的相似),關鍵代碼都加了註釋

    public final void acquireShared(int arg) {
      	// 同樣調用自定義同步器需要重寫的方法,非阻塞式的嘗試獲取同步狀態,如果結果小於零,則獲取同步狀態失敗
        if (tryAcquireShared(arg) < 0)
          	// 調用 AQS 提供的模版方法,進入等待隊列
            doAcquireShared(arg);
    }

進入 doAcquireShared 方法:

    private void doAcquireShared(int arg) {
      	// 創建共享節點「SHARED」,加到等待隊列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
          	// 進入“自旋”,這裏並不是純粹意義上的死循環,在獨佔式已經說明過
            for (;;) {
              	// 同樣嘗試獲取當前節點的前驅節點
                final Node p = node.predecessor();
              	// 如果前驅節點為頭節點,嘗試再次獲取同步狀態
                if (p == head) {
                  	// 在此以非阻塞式獲取同步狀態
                    int r = tryAcquireShared(arg);
                  	// 如果返回結果大於等於零,才能跳出外層循環返回
                    if (r >= 0) {
                      	// 這裡是和獨佔式的區別
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

上面代碼第 18 行我們提到和獨佔式獲取同步狀態的區別,貼心的給大家一個更直觀的對比:

差別只在這裏,所以我們就來看看 setHeadAndPropagate(node, r) 到底幹了什麼,我之前說過 JDK 源碼中的方法命名絕大多數還是非常直觀的,該方法直譯過來就是 【設置頭並且傳播/繁衍】。獨佔式只是設置了頭,共享式除了設置頭還多了一個傳播,你的疑問應該已經來了:

啥是傳播,為什麼會有傳播這個設置呢?

想了解這個問題,你需要先知道非阻塞共享式獲取同步狀態返回值的含義:

這裏說的傳播其實說的是 propagate > 0 的情況,道理也很簡單,當前線程獲取同步狀態成功了,還有剩餘的同步狀態可用於其他線程獲取,那就要通知在等待隊列的線程,讓他們嘗試獲取剩餘的同步狀態

如果要讓等待隊列中的線程獲取到通知,需要線程調用 release 方法實現的。接下來,我們走近 setHeadAndPropagate 一探究竟,驗證一下

  // 入參,node: 當前節點
	// 入參,propagate:獲取同步狀態的結果值,即上面方法中的變量 r
	private void setHeadAndPropagate(Node node, int propagate) {
    		// 記錄舊的頭部節點,用於下面的check
        Node h = head; 
    		// 將當前節點設置為頭節點
        setHead(node);
        
    		// 通過 propagate 的值和 waitStatus 的值來判斷是否可以調用 doReleaseShared 方法
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
          	// 如果後繼節點為空或者後繼節點為共享類型,則進行喚醒後繼節點
    				// 這裏後繼節點為空意思是只剩下當前頭節點了,另外這裏的 s == null 也是判斷空指針的標準寫法
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

上面方法的大方向作用我們了解了,但是代碼中何時調用 doReleaseShared 的判斷邏輯還是挺讓人費解的,為什麼會有這麼一大堆的判斷,我們來逐個分析一下:

這裏的空判斷有點讓人頭大,我們先挑出來說明一下:

排除了其他判斷條件的干擾,接下來我們就專註分析 propagate 和 waitStatus 兩個判斷條件就可以了,這裏再將 waitStatus 的幾種狀態展示在這裏,幫助大家理解,【騷粉色】是我們一會要用到的:

propagate > 0

上面已經說過了,如果成立,直接短路後續判斷,然後根據 doReleaseShared 的判斷條件進行釋放

propagate > 0 不成立, h.waitStatus < 0 成立 (注意這裏的h是舊的頭節點)

什麼時候 h.waitStatus < 0 呢?拋開 CONDITION 的使用,只剩下 SIGNAL 和 PROPAGATE,想知道這個答案,需要提前看一下 doReleaseShared() 方法了:

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                  	// CAS 將頭節點的狀態設置為0                
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 設置成功后才能跳出循環喚醒頭節點的下一個節點
                  	unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         // 將頭節點狀態CAS設置成 PROPAGATE 狀態
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

doReleaseShared() 方法中可以看出:

  • 如果讓 h.waitStatus < 0 成立,只能將其設置成 PROPAGATE = -3 的情況,設置成功的前提是 h 頭節點 expected 的狀態是 0;

  • 如果 h.waitStatus = 0,是上述代碼第 8 行 CAS 設置成功,然後喚醒等待中的線程

所以猜測,當前線程執行到 h.waitStatus < 0 的判斷前,有另外一個線程剛好執行了 doReleaseShared() 方法,將 waitStatus 又設置成PROPAGATE = -3

這個理解有點繞,我們還是來畫個圖理解一下吧:

可能有同學還是不太能理解這麼寫的道理,我們一直說 propagate <> = 0 的情況,propagate = 0 代表的是當時/當時/當時 嘗試獲取同步狀態沒成功,但是之後可能又有共享狀態被釋放了,所以上面的邏輯是以防這種萬一,你懂的,嚴謹的併發就是要防止一切萬一,現在結合這個情景再來理解上面的判斷你是否豁然開朗了呢?

繼續向下看,

前序條件不成立,(h = head) == null || h.waitStatus < 0 注意這裏的h是新的頭節點)

有了上面鋪墊,這個就直接畫個圖就更好理解啦,其實就是沒有那麼巧有另外一個線程摻合了

相信到這裏你應該理解共享式獲取同步狀態的全部過程了吧,至於非阻塞共享式獲取同步狀態帶有超時時間獲取同步狀態,結合本文講的 setHeadAndPropagate 邏輯和獨佔式獲取同步狀態的實現過程過程來看,真是一毛一樣,這裏就不再累述了,趕緊打開你的 IDE 去驗證一下吧

我們分析了AQS 的模版方法,還一直沒說 tryAcquireShared(arg) 這個方法是如何被重寫的,想要了解這個,我們就來看一看共享式獲取同步狀態的經典應用 Semaphore

Semaphore 的應用及源碼分析

Semaphore 概念

Semaphore 中文多翻譯為 【信號量】,我還特意查了一下劍橋辭典的英文解釋:

其實就是信號標誌(two flags),比如紅綠燈,每個交通燈產生兩種不同行為

  • Flag1-紅燈:停車
  • Flag2-綠燈:行車

在 Semaphore 裏面,什麼時候是紅燈,什麼時候是綠燈,其實就是靠 tryAcquireShared(arg) 的結果來表示的

  • 獲取不到共享狀態,即為紅燈
  • 獲取到共享狀態,即為綠燈

所以我們走近 Semaphore ,來看看它到底是怎麼應用 AQS 的,又是怎樣重寫 tryAcquireShared(arg) 方法的

Semaphore 源碼分析

先看一下類結構

看到這裏你是否有點跌眼鏡,和 ReentrantLock 相似的可怕吧,如果你有些陌生,再次強烈建議你回看上一篇文章 Java AQS隊列同步器以及ReentrantLock的應用 ,這裏直接提速對比看公平和非公平兩種重寫的 tryAcquireShared(arg) 方法,沒有意外,公平與否,就是判斷是否有前驅節點

方法內部只是計算 state 的剩餘值,那 state 的初始值是多少怎麼設置呢?當然也就是構造方法了:

		public Semaphore(int permits) {
      	// 默認仍是非公平的同步器,至於為什麼默認是非公平的,在上一篇文章中也特意說明過
        sync = new NonfairSync(permits);
    }
    
    NonfairSync(int permits) {
    		super(permits);
    }

super 方法,就會將初始值給到 AQS 中的 state

也許你發現了,當我們把 permits 設置為1 的時候,不就是 ReentrantLock 的互斥鎖了嘛,說的一點也沒錯,我們用 Semaphore 也能實現基本互斥鎖的效果


static int count;
//初始化信號量
static final Semaphore s 
    = new Semaphore(1);
//用信號量保證互斥    
static void addOne() {
  s.acquire();
  try {
    count+=1;
  } finally {
    s.release();
  }
}

But(英文聽力中的重點),Semaphore 肯定不是為這種特例存在的,它是共享式獲取同步狀態的一種實現。如果使用信號量,我們通常會將 permits 設置成大於1的值,不知道你是否還記得我曾在 為什麼要使用線程池? 一文中說到的池化概念,在同一時刻,允許多個線程使用連接池,每個連接被釋放之前,不允許其他線程使用。所以說 Semaphore 可以允許多個線程訪問一個臨界區,最終很好的做到一個限流/限流/限流 的作用

雖然 Semaphore 能很好的提供限流作用,說實話,Semaphore 的限流作用比較單一,我在實際工作中使用 Semaphore 並不是很多,如果真的要用高性能限流器,Guava RateLimiter 是一個非常不錯的選擇,我們後面會做分析,有興趣的可以提前了解一下

關於 Semaphore 源碼,就這麼三下五除二的結束了

總結

不知你有沒有感覺到,我們的節奏明顯加快了,好多原來分散的點在被瘋狂的串聯起來,如果按照這個方式來閱讀 JUC 源碼,相信你也不會一頭扎進去迷失方向,然後沮喪的退出 JUC 吧,然後面試背誦答案,然後忘記,然後再背誦?

跟上節奏,關於共享式獲取同步狀態,Semaphore 只不過是非常經典的應用,ReadWriteLock 和 CountDownLatch 日常應用還是非常廣泛的,我們接下來就陸續聊聊它們吧

靈魂追問

  1. Semaphore 的 permits 設置成1 “等同於” 簡單的互斥鎖實現,那它和 ReentrantLock 的區別還是挺大的,都有哪些區別呢?
  2. 你在項目中是如何使用 Semaphore 的呢?

參考

  1. Java 併發實戰
  2. Java 併發編程的藝術
  3. https://blog.csdn.net/anlian523/article/details/106319294

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

HashSet擴容機制在時間和空間上的浪費,遠大於你的想象

一:背景

1. 講故事

自從這個純內存項目進了大客戶之後,搞得我現在對內存和CPU特別敏感,跑一點數據內存幾個G的上下,特別沒有安全感,總想用windbg抓幾個dump看看到底是哪一塊導致的,是我的代碼還是同事的代碼? 很多看過我博客的老朋友總是留言讓我出一套windbg的系列或者視頻,我也不會呀,沒辦法,人在江湖飄,遲早得挨上幾刀,逼着也得會幾個花架子,廢話不多說,這一篇就來看看 HashSet 是如何擴容的。

二:HashSet的擴容機制

1. 如何查看

了解如何擴容,最好的辦法就是翻看HashSet底層源碼,最粗暴的入口點就是 HashSet.Add 方法。

從圖中可以看到最後的初始化是用 Initialize 的,而且裏面有這麼一句神奇的代碼: int prime = HashHelpers.GetPrime(capacity);,從字面意思看是獲取一個質數,哈哈,有點意思,什麼叫質數? 簡單說就是只能被 1 和 自身 整除的數就叫做質數,那好奇心就來了,一起看看質數是怎麼算的吧! 再次截圖。

從圖中看,HashSet底層為了加速默認定義好了 72 個質數,最大的一個質數是 719w,換句話就是說當元素個數大於 719w 的時候,就只能使用 IsPrime 方法動態計算質數,如下代碼:


public static bool IsPrime(int candidate)
{
	if ((candidate & 1) != 0)
	{
		int num = (int)Math.Sqrt(candidate);
		for (int i = 3; i <= num; i += 2)
		{
			if (candidate % i == 0)
			{
				return false;
			}
		}
		return true;
	}
	return candidate == 2;
}

看完了整個流程,我想你應該明白了,當你第一次Add的時候,默認的空間佔用是 72 個預定義中最小的一個質數 3,看過我之前文章的朋友知道List的默認大小是4,後面就是簡單粗暴的 * 2 處理,如下代碼。


private void EnsureCapacity(int min)
{
	if (_items.Length < min)
	{
		int num = (_items.Length == 0) ? 4 : (_items.Length * 2);
	}
}

2. HashSet 二次擴容探究

當HashSet的個數達到3之後,很顯然要進行二次擴容,這一點不像List用一個 EnsureCapacity 方法搞定就可以了,然後細看一下怎麼擴容。


public static int ExpandPrime(int oldSize)
{
	int num = 2 * oldSize;
	if ((uint)num > 2146435069u && 2146435069 > oldSize)
	{
		return 2146435069;
	}
	return GetPrime(num);
}

從圖中可以看到,最後的擴容是在 ExpandPrime 方法中完成的,流程就是先 * 2, 再取最接近上限的一個質數,也就是 7 ,然後將 7 作為 HashSet 新的Size,如果你非要看演示,我就寫一小段代碼證明一下吧,如下圖:

2. 您嗅出風險了嗎?

<1> 時間上的風險

為了方便演示,我把 72 個預定義的最後幾個質數显示出來。


public static readonly int[] primes = new int[72]
{
	2009191,
	2411033,
	2893249,
	3471899,
	4166287,
	4999559,
	5999471,
	7199369
};

也就是說,當HashSet的元素個數為 2893249 的時候觸發擴容變成了 2893249 * 2 => 5786498 最接近的一個質數為:5999471,也就是 289w 暴增到了 599w,一下子就是 599w -289w = 310w 的空間虛占,這可是增加了兩倍多哦,嚇人不? 下面寫個代碼驗證下。


        static void Main(string[] args)
        {
            var hashSet = new HashSet<int>(Enumerable.Range(0, 2893249));

            hashSet.Add(int.MaxValue);

            Console.Read();
        }

0:000> !clrstack -l

000000B8F4DBE500 00007ffaf00132ae ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 16]
    LOCALS:
        0x000000B8F4DBE538 = 0x0000020e0b8fcc08
0:000> !DumpObj /d 0000020e0b8fcc08
Name:        System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
Size:        64(0x40) bytes
File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-preview.5.20278.1\System.Collections.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffaf0096d10  4000017        8       System.Int32[]  0 instance 0000020e2025e9f8 _buckets
00007ffaf00f7ad0  4000018       10 ...ivate.CoreLib]][]  0 instance 0000020e2bea1020 _slots
00007ffaeffdf828  4000019       28         System.Int32  1 instance          2893250 _count
0:000> !DumpObj /d 0000020e2025e9f8
Name:        System.Int32[]
Size:        23997908(0x16e2dd4) bytes
Array:       Rank 1, Number of elements 5999471, Type Int32 (Print Array)
Fields:
None


而且最重要的是,這裡是一次性擴容的,而非像redis中實現的那樣漸進式擴容,時間開銷也是大家值得注意的。

<2> 空間上的風險

這個有什麼風險呢?可以看一下:289w 和 599w 兩個HashSet的佔用空間大小,這也是我最敏感的。


        static void Main(string[] args)
        {
            var hashSet1 = new HashSet<int>(Enumerable.Range(0, 2893249));

            var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
            hashSet2.Add(int.MaxValue);

            Console.Read();
        }

0:000> !clrstack -l
OS Thread Id: 0x4a44 (0)
000000B1B4FEE460 00007ffaf00032ea ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 18]
    LOCALS:
        0x000000B1B4FEE4B8 = 0x000001d13363cc08
        0x000000B1B4FEE4B0 = 0x000001d13363d648

0:000> !objsize 0x000001d13363cc08
sizeof(000001D13363CC08) = 46292104 (0x2c25c88) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])
0:000> !objsize 0x000001d13363d648
sizeof(000001D13363D648) = 95991656 (0x5b8b768) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])

可以看到, hashSet1的佔用: 46292104 / 1024 / 1024 = 44.1M, hashSet2 的佔用 : 95991656 / 1024 / 1024 = 91.5M,一下子就浪費了: 91.5 - 44.1 = 47.4M

如果你真以為僅僅浪費了 47.4M 的話,那你就大錯特錯了,不要忘了底層在擴容的時候,使用新的 size 覆蓋了老的 size,而這個 老的 size 集合在GC還沒有回收的時候會一直佔用堆上空間的,這個能聽得懂嗎? 如下圖:

要驗證的話可以用 windbg 去託管堆上抓一下 Slot[] m_slotsint[] m_buckets 兩個數組,我把代碼修改如下:


    static void Main(string[] args)
    {
        var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
        hashSet2.Add(int.MaxValue);
        Console.Read();
    }


0:011> !dumpheap -stat
00007ffaf84f7ad0        3    123455868 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

這裏就拿 Slot[] 說事,從上面代碼可以看到,託管堆上有三個 Slot[] 數組,這就有意思了,怎麼有三個哈,是不是有點懵逼,沒關係,我們將三個 Slot[] 的地址找出來,一個一個看。


0:011> !DumpHeap /d -mt 00007ffaf84f7ad0
         Address               MT     Size
0000016c91308048 00007ffaf84f7ad0 16743180     
0000016c928524b0 00007ffaf84f7ad0 34719012     
0000016ce9e61020 00007ffaf84f7ad0 71993676  

0:011> !gcroot 0000016c91308048
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016c928524b0
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016ce9e61020
Thread 2b0c:
    0000006AFAB7E5F0 00007FFAF84132AE ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 15]
        rbp-18: 0000006afab7e618
            ->  0000016C8000CC08 System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
            ->  0000016CE9E61020 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

從上面可以看到,我通過 gcroot 去找這三個地址的引用根,有兩個是沒有的,最後一個有的自然就是新的 599w 的size,對不對,接下來用 !do 打出這三個地址的值。


0:011> !do 0000016c91308048
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        16743180(0xff7b0c) bytes
Array:       Rank 1, Number of elements 1395263, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016c928524b0
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        34719012(0x211c524) bytes
Array:       Rank 1, Number of elements 2893249, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016ce9e61020
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        71993676(0x44a894c) bytes
Array:       Rank 1, Number of elements 5999471, Type VALUETYPE (Print Array)
Fields:
None

從上面的 Rank 1, Number of elements 信息中可以看到,原來託管堆不僅有擴容前的Size :2893249,還有更前一次的擴容Size: 1395263,所以按這種情況算: 託管堆上的總大小近似為: 23.7M + 47.4M + 91.5M = 162.6M,我去,不簡單把。。。 也就是說:託管堆上有 162.6 - 91.5 =71.1M 的未回收垃圾 剛才的 47.4M 的空間虛佔用,總浪費為:118.5M,但願我沒有算錯。。。

3. 有解決方案嗎?

在List中大家可以通過 Capacity 去控制List的Size,但是很遺憾,在 HashSet 中並沒有類似的解決方案,只有一個很笨拙的裁剪方法: TrimExcess,用於將當前Size擴展到最接近的 質數 值, 如下代碼所示:


public void TrimExcess()
{
	int prime = HashHelpers.GetPrime(m_count);
	Slot[] array = new Slot[prime];
	int[] array2 = new int[prime];
	int num = 0;
	for (int i = 0; i < m_lastIndex; i++)
	{
		if (m_slots[i].hashCode >= 0)
		{
			array[num] = m_slots[i];
			int num2 = array[num].hashCode % prime;
			array[num].next = array2[num2] - 1;
			array2[num2] = num + 1;
			num++;
		}
	}
}

應用到本案例就是將 289w 限制到 347w,仍然有 58w的空間佔用。 如下圖:

三: 總結

HashSet的時間和空間上虛占遠比你想象的大很多,而且實占也不小,因為底層用到了雙數組 m_slotsm_buckets,每個Slot還有三個元素: struct Slot { int hashCode;internal int next;internal T value; },所以了解完原理之後謹慎着用吧。

如您有更多問題與我互動,掃描下方進來吧~

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

警訊! 氣候暖化 美病媒蚊疾病13年增兩倍

環境資訊中心外電;姜唯 翻譯;林大利 審校;稿源:ENS

天氣越來越熱,美國各大城市的蚊蟲也變多了。美國政府統計,在過去13年(2004-2016)間,蚊子、蜱蟲和跳蚤傳染病增加了兩倍,出現超過64萬2600例病例。根據美國疾病預防管制中心(CDC)最新的《生命徵象》(vital signs)報告,同一時期由蚊子和蜱蟲引入的傳播新病原體就多達九種。

埃及斑蚊,
Aedes aegypti。圖片來源:美國農業部 USDA-ARS

「茲卡、西尼羅熱、萊姆病和屈公熱,近年蚊子、蜱蟲或跳蚤傳染病越來越多,我們不知道接下來還有什麼病會進入美國。」CDC主任瑞德菲爾醫師(Robert Redfield)說。

此外,紐澤西倡議團體氣候中心(Climate Central)研究發現,隨著氣溫上升,全美大部分地區病媒蚊活躍期逐漸延長,蚊子傳播疾病的風險更大。為了研究溫度對蚊子傳播疾病的作用,該中心分析了各年春季、夏季和秋季平均溫度介於16.1°C(61°F)和33.9°C(93°F)之間的天數。這是黑斑蚊傳染疾病的溫度範圍。

244個城市中,有94%的城市氣溫介於16.1°C和33.9°C的天數增加,顯示疾病傳播的風險增加,即病媒蚊活躍期。

埃及斑紋和白線斑蚊兩種黑斑蚊都會傳播西尼羅熱。自1999年進入美國後,48州都有病例。黑斑蚊還會傳染其他危險疾病,如登革熱、茲卡、屈公熱和黃熱病。

要傳播疾病,蚊子必須叮咬兩次,第一次被傳染,第二次傳染給別人。華氏75度時,發生兩次叮咬的蚊子數量最多。

病媒蚊活躍期最長的前10名城市中,加州佔了三名,其他城市包括新墨西哥州拉斯克魯塞斯、德州埃爾帕索市和亞利桑那州圖森市。這些城市的氣溫都很適合病媒蚊生存跟傳染疾病。

夏天前往美國阿拉斯加森林得必備防蚊頭套。圖片來源: (CC BY-NC 2.0)

氣候中心的分析顯示,自20世紀70年代以來,由於氣溫升高,這些城市的疾病傳播風險一直在增加。部分北方城市則是活躍日延長幅度最大,包括蒙大拿州海倫娜和賓州伊利市。

氣候變遷也可能使某些地方溫度過高,以至於不適合病媒蚊生存。例如,1970年以來鳳凰城的病媒蚊活躍日數減少,因為當地35°C(95°F)以上的天數也增加了,而這已經超出傳染範圍。

244個城市中,只有12個城市疾病活躍天數減少。

Mosquito ‘Danger Days’ Multiply as Climate Warms PRINCETON, New Jersey, August 14, 2018 (ENS)

Sicknesses from mosquito, tick, and flea bites have tripled in the United States, with just over 642,600 cases reported during the 13 years from 2004 through 2016. Nine new germs spread by mosquitoes and ticks were discovered or introduced into the United States during this time, according to the latest “Vital Signs” report by the Centers for Disease Control and Prevention, CDC.

“Zika, West Nile, Lyme, and chikungunya – a growing list of diseases caused by the bite of an infected mosquito, tick, or flea – have confronted the U.S. in recent years, making a lot of people sick. And we don’t know what will threaten Americans next,” said CDC Director Robert Redfield, M.D

In addition, the number of mosquito “disease danger days” is increasing across much of the country as temperatures rise, representing a greater risk for transmission of mosquito-borne diseases, finds new research by the New Jersey-based science and advocacy group Climate Central.

To examine the role temperature is playing in disease transmission from mosquitoes, Climate Central analyzed the number of days each year in the spring, summer, and fall with an average temperature between 61 degrees and 93 degrees Fahrenheit. This is the range for transmission of diseases spread by mosquitoes of the Aedes type.

Of the 244 cities analyzed, 94 percent are seeing an increase in the number of days with temps between 61 and 93, indicating a heightened risk for disease transmission, or “disease danger days.”

Both types of mosquitoes transmit West Nile virus, with cases in all 48 continental states since its introduction to the United States in 1999. Aedes mosquitoes carry other dangerous diseases such as dengue, Zika, chikungunya, and yellow fever.

In order to transmit disease, a mosquito must bite twice – once to acquire the disease and a second time to pass it on. The largest number of these twice-biting mosquitoes are produced at 75 degrees Fahrenheit.

Three California cities rank numbers two, three, and nine on the list of biggest increases in disease danger days. Other cities in the top 10 list include southern and southwestern cities like Las Cruces, New Mexico, El Paso, Texas and Tucson, Arizona, where temperatures are suitable for mosquito survival and some disease transmission already occurs.

The Climate Central analysis suggests that since the 1970s, the risk of disease transmission in these cities has been increasing due to rising temperatures.

A few northern cities also make this list of greatest increases in disease danger days, including Helena, Montana, and Erie, Pennsylvania. 

Climate change may make some locations too hot for mosquito survival and disease transmission. For example, there are fewer disease danger days in Phoenix since 1970. This is likely because the number of days above 95 degrees Fahrenheit in Phoenix is also going up, and 95 degrees is beyond the range of disease transmission.

Out of the 244 cities in the analysis, only 12 are seeing a decrease in the number of disease danger days during this time period.

※ 全文及圖片詳見:

作者

如果有一件事是重要的,如果能為孩子實現一個願望,那就是人類與大自然和諧共存。

於特有生物研究保育中心服務,小鳥和棲地是主要的研究對象。是龜毛的讀者,認為龜毛是探索世界的美德。

延伸閱讀

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

欣欣客運採華德動能方案,台北市首條純電動巴士聯營路線上路

台北市首條純電動聯營巴士,動物園經信義快速道路來回松山車站的 66 路公車,29 日正式營運!這條由台北市政府規劃、欣欣客運公司經營,採用華德動能電動巴士解決方案的全電動巴士路線,將自 29 日開始,進行為期 3 天的免費試乘活動後,正式成為繼捷運之外台北市木柵地區往來松山車站的第 2 條環保大眾交通路線。

29 日通車典禮由台北市長柯文哲親自致詞。他表示,台北市的大眾交通工具將並持著節能、環保、e 化以及安全的方向進行。未來,還將會擴大辦理全電動巴士的營運。而根據欣欣客運公司的說法,這次 66 路全電動巴士路線的營運,其中不只包括 12 輛全電動巴士的運作而已,包含場站、行控中心的建置等,都是採用全環保的概念建置。

在全電動巴士的部分,這次 66 路全電動巴士是採用上市公司車王電旗下子公司華德動能與日本住友商事所提出的解決方案來打造。除了具備 12 公尺長大型電動巴士中重量最輕、爬坡力最強、時速最高以及續航力最久的優點之外。在關鍵零組件上,華德動能自製比例超過 50% 以上,期望未來能藉此能提升國內的電動產業。

而除了電動巴士本身之外,欣欣客運還在位於木柵動物園附近的場站中,建置全套電動巴士營運架構。除了充電樁、儲能設備之外,還有利用行車紀錄與大數據作業方式的行控中心,以了解行車狀況之外,還掌控車輛耗能、能源轉換率、充電效率等,加以提升營運及駕駛安全。

另外,欣欣客運還在場站的車棚上,架設 302 平方公尺的單晶矽太陽能板。每天藉由太陽能板約能發出 150 度電,在 100 度電提供給行控中心使用之外,其他多餘的電儲存下來後,每隔幾天就能充飽一台巴士 250 度的儲電量,達到環保綠能的目的。

根據規劃,欣欣客運預計在 2018 年年底前將把全電動巴士的數量擴增到 30 部。而且在場站的綠能發電上,還考慮加入風力發電的部分,利用當地冬天風大的優勢,提升綠能電力的發電效益。之後,在逐步擴展到其他的路線上。

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

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

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框架也並沒有做其他修改,所以有類似問題的同學不妨試一下。

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

砸錢不手軟,戴姆勒才是特斯拉頭號強敵?

  德國車廠戴姆勒(Daimler)豪擲110億美元,計劃在2020年打造電動車隊,出手之大無人出其右,儼然已成為特斯拉最可畏競爭者。   戴姆勒毫不避諱挑戰特斯拉,該公司九月宣布投資位在美國阿拉巴馬的廠房10億美元,計畫在2020年推出電動SUV,許多媒體認為這是在對特斯拉叫陣。   特斯拉執行長Elon Musk當時並不以為意,甚至推文嘲笑戴姆勒投資規模太小,金額後面少一個零,但沒想到戴姆勒隔一天即透過推特官方帳號宣布,研發下一代電動車經費加碼至100億美元以上,並外加至少10億美元開發電池產品。(BusinessInsider)   除此之外,戴姆勒今年三月還與太陽能面板安裝業者Vivint合作,在加州開展家用電池事業,似乎在模仿特斯拉打造以太陽能為基礎的電動車生態圈。   展望未來,中國可能是戴姆勒與特斯拉的最重要決戰場,因為中國是全球最大汽車市場,且未來準備禁賣汽/柴油車。特斯拉赴上海設廠計畫目前還在籌備階段,而戴姆勒七月已與北京汽車集團合資7.5億美元在中國建立電動車生產據點。   (本文內容由授權使用。首圖來源:public domain CC0)  

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

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

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

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

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

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

※超省錢租車方案

聚甘新

暖化讓害蟲大量繁殖 將吃掉全球兩成糧食

摘錄自2018年08月31日蘋果日報美國報導

一項研究指出,全球氣候暖化將讓昆蟲大量繁殖,這些飢餓的昆蟲將蠶食穀物,導致人類的糧食減少。

美國華盛頓大學氣候科學家多伊奇(Curtis Deutsch)領導的研究指出,昆蟲目前吞噬了全球約10%的糧食,若氣候變化持續,到本世紀末,這個比率將增加至15%到20%。

研究探討歐洲玉米螟(European corn borer)和亞洲水稻螟蟲(Asiatic rice borer)兩種害蟲在氣候上升時所造成的破壞,發現牠們在穀物生長的關鍵時刻會大量繁殖,較炎熱的天氣又會加快牠們的新陳代謝,讓牠們食量增大。研究人員的預測是基於電腦模擬的害蟲和氣候活動,有關研究已在《科學》期刊(Science)刊登。

研究人員計算出若氣溫由現在再上升1.5℃,這些飢餓的害蟲會再蠶食5,300萬噸的小麥、稻米和粟米。研究估計,在這種較溫暖的環境下,美國粟米、小麥和稻米的損失,會比現在的數量再多三分之一,俄羅斯稻米的損失會飆升6倍,而另外9個國家——北韓、蒙古、芬蘭、吉爾吉斯、喬治亞、不丹、亞美尼亞、英國和丹麥——的小麥損失,至少增加一倍。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

聚甘新

亞洲人大魚大肉需求升 環境負擔增

摘錄自2018年9月4日中央社報導

研究人員今天(4日)表示,亞洲未來30年對肉和海鮮的需求量將日益增長,導致溫室氣體和食品中使用的抗生素大量增加,對環境帶來危害及負擔。

路透社報導,根據新加坡諮詢公司Asia Researchand Engagement Pte Ltd報告,隨著人口、收入和都市化的增加,2017年到2050年間肉品及海鮮的需求將升高78%。

研究共同作者譚瑟琳(Serena Tan,音譯)表:「我們想強調的是,由於人口眾多、漲幅之快,將為環境帶來壓力。若能體認這件事,並掌握相關背景,我們就可以解決問題。」

都市區域增加意味民眾在電力和冰箱的取得更加方便,進而推升肉品和海鮮需求。然而,收入增加才是肉品和海鮮需求升高的主要推手。

譚瑟琳表示,印尼、柬埔寨、寮國、緬甸和巴基斯坦是可能造成肉類和海鮮消費增長的幾個主要國家;而如中國等高齡化國家,則可能限制成長。

為減低對環境的影響,譚瑟琳說,糧食生產方可透過實施雨水截留、使用永續動物飼料、並收及牛隻製造的甲烷等方式來提高效率。而監督機構、消費者和投資者則可向連鎖餐廳和生產商施壓,限制在肉類使用抗生素。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

聚甘新

特斯拉買氣未受死亡車禍打擊!Q3銷量飆111% 歷史高

美國豪華電動車製造商特斯拉(Tesla Motors Inc.)剛剛繳出了有史以來最為亮麗的單季銷售成績單,顯示6月30日一名「Model S」電動車車主雖開啟Autopilot自駕模式、卻仍在佛州不幸喪命的事件,並未打擊特斯拉買氣。

(Photo Credit: Tesla Motors)
2015年同期跳增111%、季增70%,還創單季歷史高。其中,15,800部為Model S轎車、8,700部為Model X休旅車(SUV)。除此之外,特斯拉還有5,500部電動車雖已打造完成、但仍未送達客戶手中,這些數據會等Q4再認列。

特斯拉指出,Q3產量上升至25,185部,較Q2的18,345部季增37%,而Q4雖然天數較少且冬季天候和年底假期將使送貨受影響,但Q4的交貨量、產量仍有望持平或略高於Q3。特斯拉重申,今(2016)年下半年50,000部的目標仍維持不變。

特斯拉今年稍早面臨生產不順的困境,直到6月才逐漸紓解,公司6月誓言要在Q3把週產量由當時的2,000部拉升至2,200部、Q4還要續增至2,400部。不過,特斯拉實際公布的Q3產量仍只有25,185部,代表每週僅生產約2,100部。

特斯拉的平價電動車款「Model 3」大受歡迎、接單夯爆,該公司打算在Q4集資量產Model 3,為此執行長Elon Musk (見圖) 9月還特地要求員工撙節開支、開足火力拉高銷售量,因為Q3將是公司跟投資人證明特斯拉有能力轉虧為盈的最後機會。

不過,Musk的要求顯然引起一些問題。不少網友在線上論壇上爆料,指稱特斯拉有部分銷售員,9月開始以折扣價積極遊說顧客購買展示車跟試駕車。這引發分析人士批評,認為特斯拉此舉將壓縮毛利。對此,Musk上週特地向員工發函,要求員工務必遵守絕不對新車折扣的營運方針。

本文由嘉實資訊 MoneyDJ 授權使用 記者 郭妍希 報導

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

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

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

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

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

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

※超省錢租車方案

聚甘新

2017第八屆廣州國際新能源汽車工業展覽會

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

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

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

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

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

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

※超省錢租車方案

聚甘新