轉載請注明原文地址:http://www.cnblogs.com/ygj0930/p/7742996.html
一:業務場景
基於Android系統的設備上投放廣告,諸如:地鐵廣告屏、自助服務機器上的廣告位等。
二:業務難點
廣告投放的主要矛盾集中於:廣告的本地緩存與及時更新。
廣告本地緩存的必要性:圖片、視頻都是比較吃流量的內容,在不停輪播過程中,如果每展示一張圖片、播放一個視頻,都實時從服務器拉取,那么廣告播多久,流量就消耗多久,這樣明顯是不合算的。
廣告更新的時效性:廣告不是一成不變的,往大了說,可以按日期跨度來規划;小了說,可以按每天的時段來規划。這就要求我們播放的廣告與服務器進行同步。
三:難點解決思路
1:本地緩存的實現
圖片緩存:圖片的緩存很容易實現,android有很多圖片加載框架,這些框架本身就自帶緩存機制。我是采用Glide這個框架,其自帶磁盤緩存、內存緩存兩級緩存機制,我們無需關心它是怎么緩存圖片的。其對緩存內容的訪問機制是通過“鍵值對”的方式——圖片url是key,圖片內容是value。也就是說:第一次加載時,glide會根據url訪問到圖片並且緩存到本地,之后再通過該url進行加載時,glide會直接從本地緩存中把圖片加載出來。
視頻緩存:android的視頻播放控件VideoView自帶單個視頻緩存功能,如果需要循環播放的廣告視頻只有一個的話,只需用videoview的setLooping(true)即可實現,這樣只會在第一次加載視頻url時拉取視頻內容,之后就不再發生網絡請求了。
問題在於,現實中不會全天候循環播放單個視頻的,最起碼也會根據廣告投放的區域、級別,輪播好幾個視頻,這樣的話,videoview的循環播放就不起作用了,每當播放一個新url時都會拉取數據,即使這個視頻它不久前還播放過。
有一種笨辦法:就是先把要播放的視頻下載到sd卡,然后只需輪播下載好的本地視頻即可。 這種方案解決了輪播視頻時的流量消耗痛點,但是不能滿足廣告時效性的要求:它需要定期查詢服務器,檢查本地視頻是否最新,如果服務器的廣告內容發生了變化,又要手動下載新視頻,同時還要處理舊視頻,否則手機容量會被不停下載的視頻文件擠爆。
最優雅的辦法是:使用視頻緩存框架,我推薦使用:danikula大神開源的videocache框架。其緩存內容的訪問機制也是“鍵值對”——如果url曾經加載過,則從本地緩存中加載視頻。至於緩存內容的管理,框架已經自動幫我們完成——使用LRU算法定期清理。
2:時效性的保證
廣告需要定時更新,很多人第一反應就是——使用android的Alarm機制,定時更新內容,這種方案雖然可行,但是太麻煩啦~
上面提到的圖片緩存框架、視頻緩存框架,都設計一個重要、核心的設計理念——以url為鍵,以內容為值。
基於這個理念,我們可以通過動態url來達到實時更新緩存內容的目的,至於更新的頻率,就看你怎么拼接url了。
按天更新:如果是按日期來更新廣告,可以在圖片、視頻的url后面加上“年月日”,這樣的話,就保證了url每日一變,而緩存框架只會在當天第一次加載時拉取數據,后面就直接從本地緩存加載數據了。而之前緩存的內容則會被自動清理掉。
按時段更新:如果是按照一天當中的不同時段來更換播放的廣告,則應該先從服務器拉取有什么時段,然后根據當前時間處於那個時段之間,在url后拼接 時段的開始或結束時間 即可。
按日期區間更新:如果是按照日期跨度來更新,比如說2017/01/01~2017/02/03號播放某幾個視頻。其實這只不過是大概念的時段播放而已,同理,我們先從服務器查詢出當前日期處於哪些視頻的播放時段之間,然后在url后拼接 起始或終止日期 即可。
按日期+時段更新:綜合上面的日期區間、一天當中的時間區間來播放不同廣告:拼接 終止日期+時段的終止時間 即可。
實時更新:如果要保證每次播放都是新的,可以拼接隨機數。
四:實戰舉例
0:工具類准備
public class Utils { //獲取當天年月日,作為動態后綴,每天變化一次 public static String getTimeStamp(){ Calendar now = Calendar.getInstance(); String timeStamp = ""+now.get(Calendar.YEAR)+now.get(Calendar.MONTH)+now.get(Calendar.DAY_OF_MONTH); return timeStamp; } }
1:圖片的輪播與按日期更新
輪播控件:使用convenientbanner。
圖片緩存:使用glide。
1)添加依賴
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.bigkoo:convenientbanner:2.0.5'
2)編寫網絡圖片加載Holder
import android.content.Context; import android.view.View; import android.widget.ImageView; import com.bigkoo.convenientbanner.holder.Holder; import com.bumptech.glide.Glide; /** * Created by yeguojian on 2017/10/24. */ public class NetworkImageHolderView implements Holder<String> { private ImageView imageView; @Override public View createView(Context context) { //你可以通過layout文件來inflate一個輪播的頁面。這里我輪播的頁面只有圖片,所以直接在代碼中創建了 imageView = new ImageView(context); return imageView; } @Override public void UpdateUI(Context context, final int position, String data) { Glide.with(context).load(data).placeholder(備用圖片:網絡圖片加載失敗時顯示).into(imageView); } }
3)編寫輪播頁面,這里我是用Fragment實現的
/** * Created by yeguojian on 2017/9/26. */ public class AdvertFragment extends Fragment { private FrameLayout videoLayout; private ConvenientBanner convenientBanner; private List<String> networkImages; private String[] images; protected ImageLoader imageLoader; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View advert = inflater.inflate(R.layout.advert_fragment,container,false); images=new String[]{ 圖片url+"&date="+Utils.getTimeStamp(),......};//這里保存向服務器請求圖片的url地址們,在后面拼接時間戳參數來達到每天從服務器拉取一次的目的。 convenientBanner = advert.findViewById(R.id.convenientBanner); imageLoader = ImageLoader.getInstance(); imageLoader.init(ImageLoaderConfiguration.createDefault(getActivity())); //網絡加載圖片 networkImages = Arrays.asList(images); convenientBanner.setPages(new CBViewHolderCreator<NetworkImageHolderView>() { @Override public NetworkImageHolderView createHolder() { return new NetworkImageHolderView(); } },networkImages) //設置自動切換(同時設置切換時間間隔) .startTurning(2000) //設置是否手動影響(設置了該項無法手動切換) .setManualPageable(false); return advert; } }
4)圖片輪播碎片的布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_gravity="center" android:background="#000000" android:layout_width="match_parent" android:layout_height="match_parent"> <com.bigkoo.convenientbanner.ConvenientBanner xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/convenientBanner" android:layout_width="match_parent" android:layout_height="match_parent" app:canLoop="true"/> </LinearLayout>
2:多個視頻的輪播緩存與按日更新
1)添加依賴:使用androidvideocache音/視頻緩存框架
compile 'com.danikula:videocache:2.7.0'
2)視頻播放控件使用videoview,具體布局就因項目而異了,這個不影響緩存的實現
3)videoview輪播並緩存網絡視頻的實現
/** * Created by yeguojian on 2017/9/26. */ public class VendingFragment extends Fragment { private VideoView videoView; private HttpProxyCacheServer proxy; //視頻緩存代理 @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View vending = inflater.inflate(R.layout.vending_fragment,container,false); //創建緩存代理 proxy = new HttpProxyCacheServer.Builder(getActivity()) .maxCacheSize(1024 * 1024 * 1024) //1Gb 緩存 .maxCacheFilesCount(5)//最大緩存5個視頻 .build(); videoView = (VideoView) vending.findViewById(R.id.vending_videoView); videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { videoView.stopPlayback(); //播放異常,則停止播放,防止彈窗使界面阻塞 return true; } }); playVideoOne();//播放第一個視頻 return vending; } public void playVideoOne(){ String proxyUrl = proxy.getProxyUrl(videoOneUrl+"&date="+Utils.getTimeStamp()); //視頻url拼接日期,實現按日更新 videoView.setVideoPath(proxyUrl); //為videoview設置播放路徑,而不是設置播放url videoView.start(); videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mPlayer) { playVideoTwo(); //監聽視頻一的播放完成事件,播放完畢就播放視頻二 } }); } public void playVideoTwo(){ String proxyUrl = proxy.getProxyUrl(videoTwoUrl+"&date="+Utils.getTimeStamp()); videoView.setVideoPath(proxyUrl); videoView.start(); videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mPlayer) { playVideoThree(); } }); } public void playVideoThree(){ String proxyUrl = proxy.getProxyUrl(videoThreeUrl+"&date="+Utils.getTimeStamp()); videoView.setVideoPath(proxyUrl); videoView.start(); videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mPlayer) { playVideoOne();//視頻三播放完后播放視頻一,從而實現輪播 } }); } }