Android優化幀動畫過程中的多執行緒模型思考

DK_BurNIng發表於2019-02-03

優化背景:臨近過年,專案有一個過年紅包的需求,紅包大家都玩過是吧,領取紅包產品和UED搞了個很複雜的動畫,這個動畫 因為太過於複雜所以只能用幀動畫來做,但是幀動畫大家懂的,效率很低,而且容易OOM,需求方呢又不願意用低質量的GIF來 展示,所以只能逼迫我們碼農們另外想辦法了。

解決方案:將這個幀動畫的原始檔例如這100張幀動畫需要的png圖片,按照播放順序明明成x1.png x2.png x3.png-----x100.png

1.首先我們至少需要2個執行緒,一個執行緒decode這些圖片資源得到bitmap,另外一個執行緒拿到bitmap以後不管是給imageview還是 surfaceview那都隨便了。反正拿到bitmap以後無非就是設定下時間間隔重新整理view即可。

2.decode的過程我們知道涉及到io,而io的速度往往比cpu速度慢很多,所以這裡為了不讓瓶頸出現,我們可以讓decode的 執行緒存在多個,這樣decode出來的bitmap 放到一個佇列裡面, 我們拿bitmap的執行緒只要從這個佇列裡面按順序取出這些bitmap即可。

3.這個佇列不能太大,因為如果記憶體中有太多圖片就和幀動畫一樣會oom了。這個是一定要注意的。

4.既然有多個執行緒同時在decode這些圖片,那麼一定要注意的是,每張圖片我們只要decode一次,否則資源肯定浪費啊。 所以要保證每張圖片僅僅被decode一次。

所以問題的核心就在於,這是一個典型的 變異的 消費者-生產者模式,只要解決了這個decode執行緒和播放執行緒的同步關係,那麼剩下不管是給imageview播還是給surfaceview 都不再是問題,那都是api的基本使用了。最後我們來抽象出這個變異的模式的特點:

1.生產者可能有很多個。因為可以開多個執行緒就decode加快速度。

2.消費者只有一個。

3.每個資源僅被生產一次。

下面就針對這個模型來處理:

package com.wuyue.test;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by 16040657 on 2019/2/3.
 */
public class Test {

    //假設我們有100張圖片
    static final int MAX_IMAGE_NUM = 100;

    //假設最多隻有3個decode執行緒
    static final int MAX_PRODUCER_NUM = 3;

    //從第一張圖片開始取,不可以重複decode,所以要利用這個原子操作,效能比sync關鍵字不知道高到哪裡去了
    AtomicInteger resourcesCurrentIndex = new AtomicInteger(0);

    //假設我們佇列只同時容納5個bitmap
    LinkedBlockingQueue<Bitmap> queue = new LinkedBlockingQueue<>(5);

    public static void main(String args[]) {
        //先構造出一個100張圖片的 資源池
        int[] resourcesArray = new int[MAX_IMAGE_NUM];
        for (int i = 0; i < MAX_IMAGE_NUM; i++) {
            resourcesArray[i] = i;
        }
        //開始播放吧
        Test test = new Test();
        test.play();

    }

    void play() {
        //先造3個decode執行緒出來 然後start他們
        for (int j = 0; j < MAX_PRODUCER_NUM; j++) {
            Thread thread = new Thread(new DecodeBitmapRunnable(), "thread:" + j);
            thread.start();
        }
        //再造一個消費者執行緒 模型就變成了 3個生產者---1個消費者
        Thread thread = new Thread(new PlayImageRunnable());
        thread.start();
    }

    //模擬一個bitmap物件,這裡放入標號 方便我們除錯
    class Bitmap {
        public int getNumber() {
            return number;
        }

        public void setNumber(int number) {
            this.number = number;
        }

        public Bitmap(int number) {
            this.number = number;
        }

        @Override
        public String toString() {
            return "Bitmap{" +
                    "number=" + number +
                    '}';
        }

        int number;
    }

    /**
     * 製造者執行緒,其實就是decode bitmap 根據你動畫圖片的多少 可以動態設定多個decode執行緒 加快速度
     */
    class DecodeBitmapRunnable implements Runnable {
        @Override
        public void run() {
            while (true) {
                //保證了原子操作,就可以保證每次decode執行緒都是取不同的資源。而且避免了sync關鍵字 效能更好
                int nowIndex = resourcesCurrentIndex.getAndIncrement();
                if (nowIndex > MAX_IMAGE_NUM - 1) {
                    return;
                }

                //模擬decode bitmap的場景 每次decode的場景花費的時間都不同 所以這裡模擬下
                int random = (int) (Math.random() * (100));
                try {
                    Thread.sleep(random);
                } catch (InterruptedException e) {
                }

                //假設經過了random 的 ms時間以後 我們就decode出來一個bitmap了
                Bitmap bitmap = new Bitmap(nowIndex);
                try {
                    //decode出來以後 就放到這個佇列裡面
                    queue.put(bitmap);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }


        }
    }


    /**
     * 消費者執行緒 從佇列裡面取bitmap出來播放 只有一個播放執行緒
     */
    class PlayImageRunnable implements Runnable {

        @Override
        public void run() {

            while (true) {
                try {
                    final Bitmap bitmap = queue.take();
                    System.out.println("consumer get " + bitmap.toString());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }


        }
    }


}

複製程式碼

看下輸出結果

consumer get Bitmap{number=1}
consumer get Bitmap{number=3}
consumer get Bitmap{number=2}
consumer get Bitmap{number=0}
consumer get Bitmap{number=5}
consumer get Bitmap{number=7}
consumer get Bitmap{number=6}
consumer get Bitmap{number=4}
consumer get Bitmap{number=8}
consumer get Bitmap{number=9}
consumer get Bitmap{number=10}
consumer get Bitmap{number=12}
consumer get Bitmap{number=11}
consumer get Bitmap{number=14}
consumer get Bitmap{number=13}
consumer get Bitmap{number=15}
consumer get Bitmap{number=17}
consumer get Bitmap{number=16}
consumer get Bitmap{number=18}
consumer get Bitmap{number=21}
consumer get Bitmap{number=19}
consumer get Bitmap{number=23}
consumer get Bitmap{number=20}
consumer get Bitmap{number=24}
consumer get Bitmap{number=22}
consumer get Bitmap{number=25}
consumer get Bitmap{number=26}
consumer get Bitmap{number=27}
consumer get Bitmap{number=28}
consumer get Bitmap{number=29}
consumer get Bitmap{number=32}
consumer get Bitmap{number=30}
consumer get Bitmap{number=34}
consumer get Bitmap{number=31}
consumer get Bitmap{number=33}
consumer get Bitmap{number=35}
consumer get Bitmap{number=37}
consumer get Bitmap{number=36}
consumer get Bitmap{number=40}
consumer get Bitmap{number=38}
consumer get Bitmap{number=41}
consumer get Bitmap{number=39}
consumer get Bitmap{number=43}
consumer get Bitmap{number=42}
consumer get Bitmap{number=44}
consumer get Bitmap{number=46}
consumer get Bitmap{number=45}
consumer get Bitmap{number=48}
consumer get Bitmap{number=49}
consumer get Bitmap{number=47}
consumer get Bitmap{number=51}
consumer get Bitmap{number=50}
consumer get Bitmap{number=52}
consumer get Bitmap{number=54}
consumer get Bitmap{number=53}
consumer get Bitmap{number=55}
consumer get Bitmap{number=58}
consumer get Bitmap{number=57}
consumer get Bitmap{number=60}
consumer get Bitmap{number=56}
consumer get Bitmap{number=59}
consumer get Bitmap{number=61}
consumer get Bitmap{number=62}
consumer get Bitmap{number=63}
consumer get Bitmap{number=66}
consumer get Bitmap{number=67}
consumer get Bitmap{number=65}
consumer get Bitmap{number=64}
consumer get Bitmap{number=68}
consumer get Bitmap{number=69}
consumer get Bitmap{number=72}
consumer get Bitmap{number=71}
consumer get Bitmap{number=70}
consumer get Bitmap{number=75}
consumer get Bitmap{number=73}
consumer get Bitmap{number=77}
consumer get Bitmap{number=74}
consumer get Bitmap{number=79}
consumer get Bitmap{number=76}
consumer get Bitmap{number=78}
consumer get Bitmap{number=80}
consumer get Bitmap{number=83}
consumer get Bitmap{number=84}
consumer get Bitmap{number=82}
consumer get Bitmap{number=81}
consumer get Bitmap{number=85}
consumer get Bitmap{number=87}
consumer get Bitmap{number=86}
consumer get Bitmap{number=88}
consumer get Bitmap{number=89}
consumer get Bitmap{number=90}
consumer get Bitmap{number=93}
consumer get Bitmap{number=91}
consumer get Bitmap{number=94}
consumer get Bitmap{number=95}
consumer get Bitmap{number=92}
consumer get Bitmap{number=98}
consumer get Bitmap{number=97}
consumer get Bitmap{number=99}
consumer get Bitmap{number=96}
複製程式碼

這裡明顯可以看出來,我們每張圖片都只被decode一次的目的是達到了,但是還有一個嚴重的bug。

我們的順序是錯的,也就是說,我們的動畫既然是ued按照播放標準從第一幀一直到第100幀,那麼這個順序是死的, 但是我們的消費者執行緒 取出來的順序 缺不是0-100. 原因就是:

我們生產者生產出來的資料並沒有按順序放到佇列裡,這就導致了我們取出來的時候順序也是錯的。

下面就要著手解決這個問題,我們用一個物件鎖即可,這個鎖記住了每次put進去的bitmap的編號, 然後每次put操作之前 先拿出這個編號看看和自己的bitmap的序號是不是匹配的,如果匹配就直接put 不匹配就wait,等其他順序的匹配好了再把自己put進去

package com.wuyue.test;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by 16040657 on 2019/2/3.
 */
public class Test {

    //假設我們有100張圖片
    static final int MAX_IMAGE_NUM = 100;

    //假設最多隻有3個decode執行緒
    static final int MAX_PRODUCER_NUM = 3;

    //從第一張圖片開始取,不可以重複decode,所以要利用這個原子操作,效能比sync關鍵字不知道高到哪裡去了
    AtomicInteger resourcesCurrentIndex = new AtomicInteger(0);

    //假設我們佇列只同時容納5個bitmap
    LinkedBlockingQueue<Bitmap> queue = new LinkedBlockingQueue<>(5);

    public static void main(String args[]) {
        //先構造出一個100張圖片的 資源池
        int[] resourcesArray = new int[MAX_IMAGE_NUM];
        for (int i = 0; i < MAX_IMAGE_NUM; i++) {
            resourcesArray[i] = i;
        }
        //開始播放吧
        Test test = new Test();
        test.play();

    }

    void play() {
        PutManager putManager = new PutManager();
        //先造3個decode執行緒出來 然後start他們
        for (int j = 0; j < MAX_PRODUCER_NUM; j++) {
            Thread thread = new Thread(new DecodeBitmapRunnable(putManager), "thread:" + j);
            thread.start();
        }
        //再造一個消費者執行緒 模型就變成了 3個生產者---1個消費者
        Thread thread = new Thread(new PlayImageRunnable());
        thread.start();
    }

    //模擬一個bitmap物件,這裡放入標號 方便我們除錯
    class Bitmap {
        public int getNumber() {
            return number;
        }

        public void setNumber(int number) {
            this.number = number;
        }

        public Bitmap(int number) {
            this.number = number;
        }

        @Override
        public String toString() {
            return "Bitmap{" +
                    "number=" + number +
                    '}';
        }

        int number;
    }

    //其實這個就是加了一個manager物件,每次put一個bitmap進來的時候 就把索引值+1 加1
    //以後的索引值 就代表他想要的下一個幀的序列號,如果準備put的程式的序列號和想要的序列號
    //不一致 那麼就等待 ,一致等到想要的序列號所在的程式來了以後 再put
    //這樣就可以保證 幀的序列是正確的了
    class PutManager {
        public int getFrameIndex() {
            return frameIndex;
        }

        int frameIndex = 0;

        public void putBitmap(Bitmap bitmap) {
            try {
                queue.put(bitmap);
                frameIndex++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 製造者執行緒,其實就是decode bitmap 根據你動畫圖片的多少 可以動態設定多個decode執行緒 加快速度
     */
    class DecodeBitmapRunnable implements Runnable {

        PutManager putManager;

        public DecodeBitmapRunnable(PutManager putManager) {
            this.putManager = putManager;
        }

        @Override
        public void run() {
            while (true) {
                //保證了原子操作,就可以保證每次decode執行緒都是取不同的資源。而且避免了sync關鍵字 效能更好
                int nowIndex = resourcesCurrentIndex.getAndIncrement();
                if (nowIndex > MAX_IMAGE_NUM - 1) {
                    return;
                }

                //模擬decode bitmap的場景 每次decode的場景花費的時間都不同 所以這裡模擬下
                int random = (int) (Math.random() * (100));
                try {
                    Thread.sleep(random);
                } catch (InterruptedException e) {
                }

                //假設經過了random 的 ms時間以後 我們就decode出來一個bitmap了
                Bitmap bitmap = new Bitmap(nowIndex);

                synchronized (putManager) {
//                    System.out.println(Thread.currentThread().getName() + " enter sync " + bitmap.toString() + " putManager.getFrameIndex()=" + putManager.getFrameIndex());
                    while (true) {
                        if (putManager.getFrameIndex() != bitmap.getNumber()) {
                            try {
                                putManager.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        } else {
                            putManager.putBitmap(bitmap);
                            putManager.notifyAll();
                            break;
                        }
                    }
                }
//                System.out.println(Thread.currentThread().getName() + " out sync");

            }


        }
    }


    /**
     * 消費者執行緒 從佇列裡面取bitmap出來播放 只有一個播放執行緒
     */
    class PlayImageRunnable implements Runnable {

        @Override
        public void run() {

            while (true) {
                try {
                    final Bitmap bitmap = queue.take();
                    System.out.println("consumer get " + bitmap.toString());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }


        }
    }


}

複製程式碼

最後看下執行結果:

"C:\Program Files (x86)\Java\jdk1.8.0_131\bin\java" -Didea.launcher.port=7548 "-Didea.launcher.bin.path=D:\Program Files (x86)\JetBrains\IntelliJ IDEA 14.1.7\bin" -Dfile.encoding=GBK -classpath "C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\deploy.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\javaws.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\jfxswt.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\management-agent.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\plugin.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\rt.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-32.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;C:\Program Files (x86)\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;C:\Users\16040657\Downloads\MapTest\out\production\MapTest;D:\Program Files (x86)\JetBrains\IntelliJ IDEA 14.1.7\lib\idea_rt.jar" com.intellij.rt.execution.application.AppMain com.wuyue.test.Test
consumer get Bitmap{number=0}
consumer get Bitmap{number=1}
consumer get Bitmap{number=2}
consumer get Bitmap{number=3}
consumer get Bitmap{number=4}
consumer get Bitmap{number=5}
consumer get Bitmap{number=6}
consumer get Bitmap{number=7}
consumer get Bitmap{number=8}
consumer get Bitmap{number=9}
consumer get Bitmap{number=10}
consumer get Bitmap{number=11}
consumer get Bitmap{number=12}
consumer get Bitmap{number=13}
consumer get Bitmap{number=14}
consumer get Bitmap{number=15}
consumer get Bitmap{number=16}
consumer get Bitmap{number=17}
consumer get Bitmap{number=18}
consumer get Bitmap{number=19}
consumer get Bitmap{number=20}
consumer get Bitmap{number=21}
consumer get Bitmap{number=22}
consumer get Bitmap{number=23}
consumer get Bitmap{number=24}
consumer get Bitmap{number=25}
consumer get Bitmap{number=26}
consumer get Bitmap{number=27}
consumer get Bitmap{number=28}
consumer get Bitmap{number=29}
consumer get Bitmap{number=30}
consumer get Bitmap{number=31}
consumer get Bitmap{number=32}
consumer get Bitmap{number=33}
consumer get Bitmap{number=34}
consumer get Bitmap{number=35}
consumer get Bitmap{number=36}
consumer get Bitmap{number=37}
consumer get Bitmap{number=38}
consumer get Bitmap{number=39}
consumer get Bitmap{number=40}
consumer get Bitmap{number=41}
consumer get Bitmap{number=42}
consumer get Bitmap{number=43}
consumer get Bitmap{number=44}
consumer get Bitmap{number=45}
consumer get Bitmap{number=46}
consumer get Bitmap{number=47}
consumer get Bitmap{number=48}
consumer get Bitmap{number=49}
consumer get Bitmap{number=50}
consumer get Bitmap{number=51}
consumer get Bitmap{number=52}
consumer get Bitmap{number=53}
consumer get Bitmap{number=54}
consumer get Bitmap{number=55}
consumer get Bitmap{number=56}
consumer get Bitmap{number=57}
consumer get Bitmap{number=58}
consumer get Bitmap{number=59}
consumer get Bitmap{number=60}
consumer get Bitmap{number=61}
consumer get Bitmap{number=62}
consumer get Bitmap{number=63}
consumer get Bitmap{number=64}
consumer get Bitmap{number=65}
consumer get Bitmap{number=66}
consumer get Bitmap{number=67}
consumer get Bitmap{number=68}
consumer get Bitmap{number=69}
consumer get Bitmap{number=70}
consumer get Bitmap{number=71}
consumer get Bitmap{number=72}
consumer get Bitmap{number=73}
consumer get Bitmap{number=74}
consumer get Bitmap{number=75}
consumer get Bitmap{number=76}
consumer get Bitmap{number=77}
consumer get Bitmap{number=78}
consumer get Bitmap{number=79}
consumer get Bitmap{number=80}
consumer get Bitmap{number=81}
consumer get Bitmap{number=82}
consumer get Bitmap{number=83}
consumer get Bitmap{number=84}
consumer get Bitmap{number=85}
consumer get Bitmap{number=86}
consumer get Bitmap{number=87}
consumer get Bitmap{number=88}
consumer get Bitmap{number=89}
consumer get Bitmap{number=90}
consumer get Bitmap{number=91}
consumer get Bitmap{number=92}
consumer get Bitmap{number=93}
consumer get Bitmap{number=94}
consumer get Bitmap{number=95}
consumer get Bitmap{number=96}
consumer get Bitmap{number=97}
consumer get Bitmap{number=98}
consumer get Bitmap{number=99}

複製程式碼

嗯 看上去沒問題了。

值得注意的是,這裡我們只是抽象出了最關鍵的執行緒模式,真正你在優化這個幀動畫的時候,一定要注意使用bitmap 物件池

不然如此頻繁的new 物件,容易造成記憶體抖動,在很多低端機上容易觸發gc,導致掉幀。一定記住使用inBitmap屬性噢~

可以大大降低你記憶體抖動的現象。

相關文章