状态机在移动端项目中的使用
作者介绍
RichsJeson,曾就任于亚信科技担任 Android 端主要负责人,现就职某知名互联网公司高级软件开发工程师,花名 Jeson,目前从事 Android 端客户端开发和应用框架技术研究的工作。并且参与 Google Fuchsia 团队的 fuchsia 项目开发以及对 Fuchsia 系统的源码分析。
引言
延续上篇《一种可复用的View的引用方式》,由于 setId 的方式太频繁了,在代码可读性和扩展方面能力较弱。因此,需要采用另外一种方式来解决 ID 设置频繁的问题。
这只是一个原因,另外一个原因是新的需求要求横竖屏布局的情况下实时显示按钮、图片、文字的变化。状态如下:未安装学生端的情况下显示下载按钮->下载中->下载完成->安装中->打开学生端。
于是再次到源码中寻找答案。查看源码以后,提出了一个问题:“需求中的横竖屏切换所使用到的层是一样的,那么能否使用观察者的模式进行事件分发后进行响应呢?” 。YES,在 android 的底层,安卓通过状态机来维护网络、声音、4G、蓝牙等状态,而且这些状态都有一个状态机维护,通过系统多个状态机组成一个大的控制端-状态机树,安卓使用状态机树对每个状态机的状态进行处理。
那么,基于横竖屏实时更新按钮状态的能力也可以基于状态机来实现。这样就能彻底解决以上业务。而且不仅仅是横竖屏,很多地方都可以使用到状态机树。
状态机
mP0 / \ mP1 mS0 / \ mS2 mS1 / \ \mS3 mS4 mS5
那么什么是状态机呢?状态机顾名思义就是对多个状态的管理,每个状态都有自身实现业务的能力,通过状态机来切换一组状态。
安卓中的状态机,是维护安卓中每一个业务的状态,并且可以执行状态切换、状态保持等能力。比如我们 Launcher 主页下拉出来的设置选项,选项中包含了数据、WLAN、手电筒、声音、蓝牙等这些按钮,供用户进行选择。当用户选择开关蓝牙的时候,由安卓的系统状态机切换状态至蓝牙的状态机。这是由于安卓系统为了好管理状态,将每个模块的不同状态组合封装成一个状态机。而这些大大小小的状态机组合成一棵树-状态机树,如上图所示。
了解状态机的原理,我们更多关注的是整个状态机的运作流程是怎样。安卓的状态机充分利用了消息收发的这一项功能,使得消息可以在各个状态中进行处理。
在安卓底层源码中就有一颗这样的树-StateMachine。StateMachine 正是利用了这一特点将条消息发送至消息队列中。每个状态通过 processMessage 方法中获取到从消息队列中已取出的消息,并且执行自身的业务。例如:StateMachine 发送一条消息到消息队列中,将当前状态 A 切换至目标状态B,状态 B 就能接收到消息队列取出来的消息,并对消息做处理。
然而需要注意的是,在状态没有切换的情况下,即使状态机发送了消息,目标 B 状态也仍然收不到消息。这样设计的好处,就是消息不会滥用,而且安卓的状态机中仅能存放的只有 10 条。
状态机的使用
从事件分发到状态机,现在就将这套实现原理应用到项目上。首先,我们来看下 StateMachine 的实现方式,StateMachine 中维护着一个 Handler-SmHandler,该类充当了StateMachine 的助理角色,维护了一组状态以及一组消息,其作用如下:
接收消息,并将消息分发至某一个状态上;
从消息队列中移除旧的消息;
维持消息队列;
添加状态;
执行状态切换;
现在,来学习下如何使用状态机吧!
步骤1:创建状态机,并继承 StateMachine。该状态机执行切换状态和将参数体转换至消息中的能力。
/*** Created by richsjeson on 2017/8/4.* @see <p>VR学生端引导页的状态机</p>*/public class VrStudentStateMachine extends StateMachine implements VrStudentState {private List<BusinessObserve> mViews;private Context mContext;private BaseState beforeState;private BaseState aflterState;private BaseState progressState;private BaseState packageInstallState;private BaseState exceptionErrorState;private BaseState downCompleteState;public static class VrInStallExec { public static final int STATE_ERROR=0; public static final int STATE_UNINSTALL = 1; public static final int STATE_PROGRESS = 2; public static final int STATE_DOWNCOMP = 3; public static final int STATE_INSTALLPROGESS=4; // @IntDef({STATE_UNINSTALL, STATE_PROGRESS, STATE_DOWNCOMP,STATE_INSTALLPROGESS,STATE_ERROR}) public @interface VrState {}}protected VrStudentStateMachine(String name) { super(name);}protected VrStudentStateMachine(String name, Looper looper) { super(name, looper);}public void addLayoutObservers(BusinessObserve observable,Context mContext){ this.mContext=mContext; if(mViews==null){ mViews=new ArrayList<BusinessObserve>(); } mViews.add(observable);}public void startMachine(){ if(beforeState==null){ beforeState=new UnInstallState(mViews,this,mContext); addState(beforeState); } if(aflterState==null){ aflterState=new DownCompleteState(mViews,this,mContext); addState(aflterState); } if(progressState==null){ progressState=new ProgressState(mViews,this,mContext); addState(progressState); } if(packageInstallState==null){ packageInstallState=new PackageInstallState(mViews,this,mContext); addState(packageInstallState); } if(downCompleteState==null){ downCompleteState=new DownCompleteState(mViews,this,mContext); addState(downCompleteState); } if(exceptionErrorState==null){ exceptionErrorState=new ErrorState(mViews,this,mContext); addState(exceptionErrorState); } setInitialState(beforeState); this.start();}//获取当前的statepublic VrStudentState getNowState(){ if(getCurrentState()== progressState){ return (VrStudentState) progressState; }else if(getCurrentState()==beforeState){ return (VrStudentState) beforeState; }else if(getCurrentState()==aflterState){ return (VrStudentState)aflterState; } return (VrStudentState) beforeState;}@Overridepublic void transitionToAflter() { transitionTo(aflterState); sendMessage(obtainMessage(STATE_DOWNCOMP));}@Overridepublic void transitionToBefore() { transitionTo(beforeState); sendMessage(obtainMessage(STATE_UNINSTALL));}@Overridepublic void transitionToProgress() { transitionTo(progressState); sendMessage(obtainMessage(STATE_PROGRESS));}@Overridepublic void trainsitionToInstall() { transitionTo(packageInstallState); sendMessage(obtainMessage(STATE_INSTALLPROGESS));}@Overridepublic void trainsitionToError() { transitionTo(exceptionErrorState); sendMessage(obtainMessage(STATE_ERROR));}}
步骤2:创建状态,以未下载安装学生端为例
/*** Created by richsjeson on 2017/8/4.*/public class UnInstallState extends BaseState {public UnInstallState(List<BusinessObserve> mViews, VrStudentStateMachine mMachine, Context mContext) { super(mViews, mMachine, mContext);}@Overridepublic void processMessage(BaseState state) {}@Overridepublic void dispatchView() { if(mViews!=null && mViews.size()>0){ for(BusinessObserve observer: mViews) { observer.dispatchView(this, null); } }}@Overridepublic boolean processMessage(Message msg) { super.processMessage(msg); android.util.Log.i("VRMachine","BeforeState"); switch (msg.what){ case STATE_UNINSTALL: //分发事件至View上 if(mViews!=null && mViews.size()>0){ for(BusinessObserve observer: mViews) { observer.dispatchView(this, msg); } } break; case STATE_PROGRESS: //切换至下载中的状态 mMachine.transitionToProgress(); break; case STATE_DOWNCOMP: //下载完成->安装中的状态 mMachine.trainsitionToInstall(); case STATE_INSTALLPROGESS: // break; } return true;} }
步骤3:创建状态,以下载学生端中为例
public class ProgressState extends BaseState {/** * <p>下载管理器</p> */private DownloadManager.Request mDownloadRequest;private DownloadManager mDownloadManager;private DownloadManagerObserver observer;final static String url="http://cs.101.com/v0.1/realUrl?" + "path=/ppt_101_mobile/android/vrtransfer/vrstudent.apk&attachment=true&serviceName=ppt_101_mobile";;private long downloadId=0;public ProgressState(List<BusinessObserve> mViews, VrStudentStateMachine mMachine, Context mContext) { super(mViews, mMachine, mContext);}@Overridepublic void processMessage(BaseState state) {}@Overridepublic void dispatchView() {}@Overridepublic boolean processMessage(Message msg) { super.processMessage(msg); android.util.Log.i("VRMachine","start down progress"); startDownloadManager(); return true;}//执行下载操作public void startDownloadManager(){ if(mDownloadManager == null){ mDownloadManager= (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); } if(mDownloadRequest==null){ mDownloadRequest=new DownloadManager.Request(Uri.parse(url)); mDownloadRequest.setTitle("VR学生端下载"); mDownloadRequest.setNotificationVisibility(1); mDownloadRequest.setMimeType("application/vnd.android.package-archive"); mDownloadRequest.allowScanningByMediaScanner(); mDownloadRequest.setDestinationInExternalPublicDir("Download","vrstudent.apk"); if(observer==null){ observer=new DownloadManagerObserver(); } mContext.getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"),true,observer); }else{ mDownloadRequest.setDestinationInExternalPublicDir("Download","vrstudent.apk"); } downloadId=mDownloadManager.enqueue(mDownloadRequest);}//执行状态监听。private class DownloadManagerObserver extends ContentObserver{ public DownloadManagerObserver() { super(downloadHandler); } @Override public boolean deliverSelfNotifications() { return super.deliverSelfNotifications(); } @Override public void onChange(boolean selfChange) { updateProgress(); }}private Handler downloadHandler=new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); if(msg.arg1==msg.arg2){ mMachine.transitionToAflter(); }else{ if(mViews != null && mViews.size()>0){ for(BusinessObserve observe :mViews){ Message message=new Message(); message.what= (int) (((float)msg.arg1/msg.arg2)*100); observe.dispatchView(ProgressState.this,message); } } } } @Override public void dispatchMessage(Message msg) { super.dispatchMessage(msg); }};private void updateProgress() { int[] bytesAndStatus = getBytesAndStatus(downloadId); Message message=Message.obtain(); message.arg1=bytesAndStatus[0]; message.arg2=bytesAndStatus[1]; downloadHandler.sendMessage(message);}/** * 通过query查询下载状态,包括已下载数据大小,总大小,下载状态 * * @param downloadId * @return */private int[] getBytesAndStatus(long downloadId) { int[] downloadBytes = new int[2]; DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId); Cursor cursor = null; try { cursor = mDownloadManager.query(query); if (cursor != null && cursor.moveToFirst()) { //已经下载文件大小 downloadBytes[0] = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); //下载文件的总大小 downloadBytes[1] = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); } } finally { if (cursor != null) { cursor.close(); } } return downloadBytes;}}
设计思路
知道了状态机怎么使用,回到业务上,结合 PUB_SUB 模式来解决我们现有的问题吧。我的设计的目标将事件分发至 View 上,并且要实时更新View的状态,同时还要实现View和业务间的隔离。按照常理,很多开发着都会想着如何利用后台服务或者系统自身的广播,以及 Event-BUS 方式去实现。但是这里不打算用以上解决的方式。
每个状态都能实现自身的业务以及去更新UI的能力。首选,在 Fragment 中初始化状态机以及View的集合,并且初始化状态为未下载的状态(BeforeSate)。紧接着在 VrStateLayout 中点击下载,则通知状态机将当前状态(BeforeStates)切换至下载中的状态(ProgressState)。ProgressState 接收到传入的消息,调用 DownloadManager 执行下载的操作,同时DwonloadManager 在下载过程中,利用 PUB-SUB 模式,将下载的进度更新至 VrStateLayout中,VrStateLayout 根据传入的状态类型,进行UI的切换。
现在,来看下 HostFragment(在DEMO中为MainActivity类)这个类的实现吧。这里以 AndroidVRScence 为例。代码如下:
public class MainActivity extends Activity {private FrameLayout mLinearPort;private FrameLayout mLinearLand;private VrStudentGuildLayout mStartVrLinearLayoutPort;private VrStudentGuildLayout mStartVrLinearLayoutLand;private VrStudentStateMachine mMachine;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(R.layout.activity_main); mLinearPort= (FrameLayout) this.findViewById(R.id.ll_port); mLinearPort.setBackgroundColor(Color.RED); mLinearLand= (FrameLayout) this.findViewById(R.id.ll_land); mLinearLand.setBackgroundColor(Color.BLACK); mStartVrLinearLayoutPort=new VrStudentGuildLayout(this,null); mStartVrLinearLayoutPort.setId(R.id.tv_start_vrresource_port); mStartVrLinearLayoutLand=new VrStudentGuildLayout(this,null); mStartVrLinearLayoutLand.setId(R.id.tv_start_vrresource_land); mLinearPort.addView(mStartVrLinearLayoutPort); mLinearLand.addView(mStartVrLinearLayoutLand); if(mMachine==null){ //保存虚拟机状态 mMachine=new VrStudentStateMachine("VrDonwloadState"); mMachine.addLayoutObservers(mStartVrLinearLayoutLand,getBaseContext()); mMachine.addLayoutObservers(mStartVrLinearLayoutPort,getBaseContext()); //启动状态机 mMachine.startMachine(); Message message=Message.obtain(); message.what=STATE_UNINSTALL; //安卓的状态机需要发送消息到指定的状态中进行响应 mMachine.sendMessage(message); }}//从xml中配置的按钮点击事件,点击后切换至下载public void onStartVrDownlaod(View view){ Message message=Message.obtain(); message.what=STATE_PROGRESS; mMachine.sendMessage(message);}}
首先,VrStudentGuildLayout 就是上述的 View,它分为横屏(mStartVrLinearLayoutLand)和竖屏(mStartVrLinearLayoutPort)并且继承于 BusinessObserve。
其次,将 BusinessObserve 放入 Observe 队列中后初始化状态机,初始状态为未下载的状态。
再次,每个状态切换后,根据消息类型更新UI。
代码如下:
package com.richsjeson.vrstate.view;/** * Created by richsjeson on 2017/8/4. * <p>根据状态机进行UI的处理</p>*/public class VrStudentGuildLayout extends RelativeLayout implements BusinessObserve {//按钮点击private Button starVrSTtudentStatusButton;//private ImageView mImageView;private TextView mTextView;private UIButton mUIButton;private View rootView;private Handler handler=new Handler();public VrStudentGuildLayout(Context context) { super(context); BitmapDrawable bitmap = (BitmapDrawable) mImageView.getDrawable(); bitmap.getBitmap().getHeight(); bitmap.getBitmap().getWidth();}public VrStudentGuildLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); onCreate(context,attrs);}public VrStudentGuildLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); onCreate(context,attrs);}@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)public VrStudentGuildLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); onCreate(context,attrs);}private void onCreate(Context context , AttributeSet attrs){ if(rootView==null){ rootView=LayoutInflater.from(context).inflate(R.layout.frame_vr_storehost,this); } this.mImageView= (ImageView) rootView.findViewById(R.id.iv_action_image); this.mTextView= (TextView) rootView.findViewById(R.id.tv_tipvrstart); this.mUIButton=(UIButton) rootView.findViewById(R.id.download_btn);}@Overridepublic void dispatchView(BaseState mState, Message message) { //根据状态机类型对UI进行处理。 if(mState instanceof UnInstallState){ //状态机初始化后的布局 beforeStateMachine(); }else if(mState instanceof DownCompleteState){ //状态机结束后 afterStateMachine(); }else if(mState instanceof ProgressState){ //状态机下载按钮的布局 progressStateMachine(message); }else if(mState instanceof ErrorState){ errorState(); }}private void progressStateMachine(final Message message) { handler.post(new Runnable() { @Override public void run() { int progress=0; if(message != null){ progress=message.what; } mUIButton.setState(UIButton.STATE_DOWNLOADING); mUIButton.setProgressText("下载中",progress); } });}private void afterStateMachine() { handler.post(new Runnable() { @Override public void run() { mUIButton.setState(UIButton.STATE_NORMAL); mUIButton.setCurrentText("安装中..."); } });}private void beforeStateMachine() { handler.post(new Runnable() { @Override public void run() { mUIButton.setState(UIButton.STATE_NORMAL); mUIButton.setCurrentText("下载"); mUIButton.setProgress(0); } });}public void errorState(){ handler.post(new Runnable() { @Override public void run() { mUIButton.setState(UIButton.STATE_NORMAL); mUIButton.setCurrentText("下载失败"); mUIButton.setProgress(0); } });}}
总结
通过以上的实现,发现很多业务都可以使用到状态机,但是为了避免过度设计,很多时候,都可以从源码中寻找解决方案,当然了,能简易实现尽量就简易实现,如果场景不复杂,就没有必要考虑那么复杂的因素,否则会造成过度设计导致的代码浪费。
以上就是今天要讲解的内容,剩下的有问题群里可以发问。谢谢大家!
更多相关文章
- 状态机设计
- 客户端请求服务器时的状态码讲解
- 设计模式之状态模式
- 最近状态不咋好 ...
- 共享可变状态中出现的问题以及如何避免[每日前端夜话0xDB]
- Java线程之线程状态的转换
- 实现一个简单的 JavaScript 状态机[每日前端夜话0xBF]
- js和jquery使按钮失效为不可用状态的方法
- 如何在进度条全屏表单界面上添加百分比状态