Android software stack,
在開始介紹 RandomMusicPlayer 前, 首先先複習 android software stack,軟體分層是大型軟體專案開發的重要觀念,
當軟體分層後才不會導致軟體過於龐大難以維護,
並且軟體分層後軟體的架構也會比較清晰易懂
Android 是屬於及大型的軟體專案,
在 Android 這個大專案底下又包含了 509 個子專案
因此如果不將軟體分層, 則系統會過於龐大難以維護
並且也使的系統難以開發
Google 基本上把軟體分層了, application, java framework, native framework, kernel
但是根據一般電腦學科的分法, application, java framework, native framework 屬於 user space, kernel 屬於 kernel space.
- kernel : 是一些硬體相關, os 相關, 一般在 CPU 的 kernel mode 執行, 一般的 process 無法操作
- native framework: 這一曾放的東西比較多元, 像是外部的專案 google 把他放入, 像是 skia, libc, 另外還有一些跟硬體相關像是 surface manager, 另外還有一些急需執行效率像是 media framework
- java framework: Android application 是以 java 寫成的, 因此需要 java framework 作為和 android 系統溝通的介面, 並且所有和 android application 相關的 framework 都是在這一曾實作的
- application: 通常是用來做 ui 顯示, 和程式開發商自己專利技術實作的地方
如下圖所示:
1. RandomMusicPlayer:
在開始介紹 android MediaPlayer前, 先借介紹如何撰寫一個簡單的 music
player, 以免一開始就陷入結構極度複雜的 media player 中. 在 android 原生 source code 終有一個很簡單的 music player 的 sample code, 可以到 你的 codebase 中的
development/samples/RandomMusicPlayer 中 study 他的 source code.
在 RandomMusicPlayer 中大致上可以分成四大類的元件
l MainActivity: 他是 RandomMusicPlayer 的 UI 介面, MainActivity 主要是接收使用者的 UI 操作, 並且把這些 UI 操作轉成命令並且送出 intent 讓 MusicIntentReceiver 接收,
l MusicService: MusicIntentReceiver 收到命令後, 透過 MusicService 送 command 給 Android 的 MediaPlayer
l MusicRetriever: MusicRetriever 是這個 application 會叫 RandomMusicPlayer 的主要原因, 他會有一個 default 的 http stream 的 uri 和在 external storage 的歌曲共同構成一個 list, 每次挑選播放的歌曲時, 都是 random 的在這張 list 中隨機挑選一首歌曲播放
l RemoteControl: Remote control 適用在 lockscreen 上操作 musicplayer 的介面
2. MainActivity
以下是 RandomMusicPlayer UI的簡圖,
雖然 google 在 UI 上做了很多的 button, 然而因為這只是一個 sample code, 因此不是每個 UI 元件都有實作, google 實做的只有以下六個 button 而已(mPlayButton, mPauseButton, mSkipButton,
mRewindButton, mStopButton, mEjectButton).
一個 application 的入口點是在 onCreate 這個 callback, onCreate callback 一般負責 init resource,
以下這段程式碼就是 RandomMusicPlayer 的 onCreate method, 他首先會把抓取他需要用到的 UI object 的 instance, 並且為他關心的 object 透過 setOnClickListener 註冊這些 ui 元件相對應的 callback function, mPlayButton.setOnClickListener(this); 是指當 UI 事件發生在 mPlayButton 上時, 到相同的 class 尋找他的 callbackfunction (onClick)
以下這段程式碼就是 RandomMusicPlayer 的 onCreate method, 他首先會把抓取他需要用到的 UI object 的 instance, 並且為他關心的 object 透過 setOnClickListener 註冊這些 ui 元件相對應的 callback function, mPlayButton.setOnClickListener(this); 是指當 UI 事件發生在 mPlayButton 上時, 到相同的 class 尋找他的 callbackfunction (onClick)
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mPlayButton = (Button) findViewById(R.id.playbutton);
mPauseButton = (Button) findViewById(R.id.pausebutton);
mSkipButton = (Button) findViewById(R.id.skipbutton);
mRewindButton = (Button) findViewById(R.id.rewindbutton);
mStopButton = (Button) findViewById(R.id.stopbutton);
mEjectButton = (Button) findViewById(R.id.ejectbutton);
mPlayButton.setOnClickListener(this);
mPauseButton.setOnClickListener(this);
mSkipButton.setOnClickListener(this);
mRewindButton.setOnClickListener(this);
mStopButton.setOnClickListener(this);
mEjectButton.setOnClickListener(this);
}
當使用者觸碰 UI 上的 button 時 framework 會執行行相對應的 onClick function,
以下的程式碼就是 RandomMusicPlayer 的 執行 button press 的相對應 method
public void onClick(View target) {
// Send the correct intent to the MusicService, according to the button that was clicked
if (target == mPlayButton)
startService(new Intent(MusicService.ACTION_PLAY));
else if (target == mPauseButton)
startService(new Intent(MusicService.ACTION_PAUSE));
else if (target == mSkipButton)
startService(new Intent(MusicService.ACTION_SKIP));
else if (target == mRewindButton)
startService(new Intent(MusicService.ACTION_REWIND));
else if (target == mStopButton)
startService(new Intent(MusicService.ACTION_STOP));
else if (target == mEjectButton) {
showUrlDialog();
}
}
在這裡假設, 使用者按 mPlayButton, 那麼 framework 會執行 onClick 的 function,
並且 traget 是 mPlayButton, 如此 onClick 就知道哪個 button 被 click,
並且執行相對應的 method
3. MusicService
在開始 study MusicService 前先們探討一個最基本的問題,為什麼 MusicService 必須獨立成另成一個 service, 而無法跟 MainActivity 合併?
其實跟 Activity 的 life cycle 是相關的,
首先思考使用者行為,
一般使用者用手機聽音樂都會關掉螢幕,
但是當螢幕關閉時 Activity Life cycle 進入了 onPause 的 stage,
此時 activity 的所有資源都必須被釋放掉,
因此如果把 MediaPlayer 的操作放在 activity 時, 當使用者關閉螢幕,
則 android 會把 MediaPlayer 的 instance 釋放掉,
此時音樂就會自動關閉, 因此才會把 MediaPlayer 的操作放在一個 service 中
當進手機休眠時, Service 還會在背景執行.
MusicService 是一個負責在背景執行, 專門接收外部命令的程式,
但別被他的名字騙了, 雖然他是 Service,
但是他還是在 main thread 中執行, 並不是另一條獨立的 thread,
因此他還是受到 life cycle 的限制, 只是沒那麼嚴謹而已
MusicService 在背景執行, 並且接收前景送來的命令, 並且轉成 MediaPlayer 相關的 api,
通知 media player 執行相對映的操作
以下的程式碼就是他接收前景送來命令的程式碼
/**
* Called when we receive an Intent. When we receive an intent sent to us via startService(),
* this is the method that gets called. So here we react appropriately depending on the
* Intent's action, which specifies what is being requested of us.
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
if (action.equals(ACTION_TOGGLE_PLAYBACK)) processTogglePlaybackRequest();
else if (action.equals(ACTION_PLAY)) processPlayRequest();
else if (action.equals(ACTION_PAUSE)) processPauseRequest();
else if (action.equals(ACTION_SKIP)) processSkipRequest();
else if (action.equals(ACTION_STOP)) processStopRequest();
else if (action.equals(ACTION_REWIND)) processRewindRequest();
else if (action.equals(ACTION_URL)) processAddRequest(intent);
return START_NOT_STICKY; // Means we started the service, but don't want it to
// restart in case it's killed.
}
接著我們在看到他如何跟 MediaPlayer 溝通的程式碼, 當然以下的程式碼已經是 application 最底部了,執行到 MediaPlayer 就屬於 java framework 的部份了.
void processPauseRequest() {
if (mState == State.Retrieving) {
// If we are still retrieving media, clear the flag that indicates we should start
// playing when we're ready
mStartPlayingAfterRetrieve = false;
return;
}
if (mState == State.Playing) {
// Pause media player and cancel the 'foreground service' state.
mState = State.Paused;
mPlayer.pause();
relaxResources(false); // while paused, we always retain the MediaPlayer
// do not give up audio focus
}
// Tell any remote controls that our playback state is 'paused'.
if (mRemoteControlClientCompat != null) {
mRemoteControlClientCompat
.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);
}
}
在這段程式碼中首先判斷是否正在抓歌曲清單, 如果正在抓歌曲清單則先暫停,接著判斷目前的播放狀態, if (mState == State.Playing) { 必須有這行是為了保護 MediaPlayer,
以避免在不屬於 MediaPlayer 接受的狀態做違法的操作,
而造成 MediaPlayer 丟出 IllgelStateException
至於 MediaPlayer 有哪些狀態, 並且哪些狀態可做哪些事情,
可以查詢 MediaPlayer sdk 的官網的 MediaPlayer state machine




沒有留言:
張貼留言