前言
在我之前的文章《基于对象消息编程的android开发框架》中对我的对象消息编程框架及其在android方面的应用做了初步的介绍,其中也用两个案例做以简要说明,限于文章篇幅没有深入阐述,在此对上篇文章的移动基站信息案例进行较深入的分析,从而对消息编程进一步的了解。
案例app功能
在通信公司运维中,需要知道移动基站的信号质量,根据信号质量而对基站进行调整或维护。为方便基站信息的搜集,因此我们设计一个手机端app使运维人员能方便的对基站信息采集。那么该app主要有以下功能:
1、首先通过手机获取周围基站的信息。如基站id、信号强度等。
2、根据基站id确定基站的物理位置。
3、将基站信息、基站位置进行本地数据库保存及上传到web服务器。
app将获取的信息传到服务器后,运维部门就可以根据服务器中的数据进行分析,判断移动信号服务地区盲点及基站服务情况。
程序主页面
页面元素:
1、按钮:
历史记录:打开记录页面。保存数据:将基站信息保存到本地数据库。上传数据:将数据上传到远端服务器。自动保存上传:开启自动保存、上传,间隔5秒执行一次。
2、文本标签
分别用来显示手机获得的基站信息、从互联网获得的基站位置信息、上传结果、保存结果。
下面我们首先通过分析代码的流程来熟悉消息编程框架
主页面activity启动:
主页面对应activity代码为TelsignalActivityNew类。
public class TelsignalActivityNew extends TLBaseActivity implements View.OnClickListener{
private int nn=1;
private int mm=1;
private String cell_id="";
private String lac ="";
public TextView signaltext;
TextView jizhantext;
TextView postresulttext;
TextView savetext;
TLMsgBridge msgBridge;
int i=1;
int p=1;
int m=1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_telsignal);
// msgBridge=getMsgBridge("saveReturn");
signaltext = (TextView) findViewById(R.id.signaltext);
jizhantext = (TextView) findViewById(R.id.jizhantext);
postresulttext = (TextView) findViewById(R.id.postresulttext);
savetext = (TextView) findViewById(R.id.savetext);
addButton("button_m", R.id.button_recoders);
addButton("button_post", R.id.button_post);
addButton("button_autosave", R.id.button_autosave);
addButton("button_save", R.id.button_save);
if (ContextCompat.checkSelfPermission(TelsignalActivityNew.this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(TelsignalActivityNew.this, new String[]{ Manifest.permission. ACCESS_COARSE_LOCATION }, 1);
}
setMyApp("TLSignalApp");
putMyApp(new TLMsg("setup"));
}
@Override
protected void onStart(){
super.onStart();
}
@Override
public void onClick(View v) {
TLMsg msg =createMsg();
switch (v.getId()) {
case R.id.button_recoders:
msg.setMsgId("startactivity"); ;
msg.setParam("classname", RecoderActivity.class);
putMsg(appCenter,msg);
break;
case R.id.button_post:
msg.setAction("postData");
putMyApp(msg);
break;
case R.id.button_save:
msg.setAction("saveData");
putMyApp(msg);
break;
case R.id.button_autosave:
msg.clear();
msg.setAction("autosaveData");
putMyApp(msg);
break;
}
}
public TLMsg getMsg(Object fromWho, TLMsg msg) {
switch (msg.getAction()) {
case "getSignal":
showResponse( msg) ;
break;
case "jizhan":
showjizhan( msg) ;
break;
case "postReturn":
postReturn( msg) ;
break;
case "serviceReturn":
serviceReturn( msg) ;
break;
case "saveReturn":
runOnUiThread(new Runnable() {
@Override
public void run() {
savetext.setText("保存成功"+i+"次!");
i++;
}
});
break;
default:
}
return null ;
}
private void serviceReturn(TLMsg msg) {
final String content;
if(msg.getParam("error")!=null && (boolean)msg.getParam("error")==false)
{
content ="上传"+p+"次!"+ TLMsgUtils.msgToGson(msg);
p++;
}
else
if(msg.getParam("netstate")!=null &&(boolean)msg.getParam("netstate")==false )
{
content="没有打开网络";
}
else
{
content="上传错误"+m+"次!";
m++;
}
postresulttext.setText(content);
}
private void postReturn(TLMsg msg) {
String status ="" ;
if(msg.getParam("error")!=null && (boolean)msg.getParam("error")==false)
{ String response =(String) msg.getParam("response");
// response=(String)getBundleMsg().getParam("bundle") +mm+status+response;
postresulttext.setText(response);}
else{
postresulttext.setText("上传错误"+p+"次!");
p++;
}
}
private void showjizhan(TLMsg msg) {
String status ="" ;
if(msg.getParam("error")!=null && (boolean)msg.getParam("error")==false)
{
String response =(String) msg.getParam("response");
jizhantext.setText(response);
}
else
jizhantext.setText("查询网络错误");
}
private void showResponse(TLMsg msg) {
List<BaseDataBean> list =(List<BaseDataBean> )msg.getParam("jizhan") ;
BaseDataBean data =list.get(0);
cell_id =data.getCell_id();
lac =data.getLac();
signaltext.setText(nn+" dbm:"+msg.getParam("dbm")+" level:"+msg.getParam("signallevel")+" cell_id:"+cell_id +" lac:"+lac+
" lte_sinr:"+(int) msg.getParam("lte_sinr")
+" lte_rsrp:"+(int) msg.getParam("lte_rsrp")
+" lte_rsrq:"+(int) msg.getParam("lte_rsrq")
+" lte_rssnr:"+(int) msg.getParam("lte_rssnr")
+" lte_cqi:"+(int) msg.getParam("lte_cqi"));
nn++;
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "请打开位置权限,拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
}
}
}
在activity创建方法onCreate里,首先获取按钮及文本标签的实例,同时对按钮进行监听。为代码方便在父类自定义了一个addButton方法来实现按钮的实例获取及监听。
protected Button addButton(String name ,int id){
Button button = (Button) findViewById(id);
button.setOnClickListener((View.OnClickListener) this);
buttons.put(name,button);
return button;
}
初始化页面元素后,检查位置信息权限是否获取,如果没有申请权限。至此常规activity启动流程到此。
主页面启动后将自动显示出基站信息及通过互联网获取的基站位置信息,这是如何实现的呢。主要是下面两行代码:
setMyApp("TLSignalApp"); // 设置对应的业务模块
putMyApp(new TLMsg("setup")); //给业务模块发生启动消息
activity主要实现输入、输出显示功能,业务逻辑由对应的app模块TLSignalApp来实现。activity也不应该包含任何对象、组件的创建,这样activity是纯粹的view,与其他功能完全的分开。
这个配置的意思是模块名字为
这里通过setMyApp方法设置了对应的业务处理模块TLSignalApp,然后给模块发送了启动消息指令“setup”。细心同学发现,模块TLSignalApp是一个字符串名字,这里没有进行对象实例化,那这如何与具体的对象对应呢 ?这在程序主配置文件app_config.xml里面设置:
<module name="TLSignalApp" classfile=".demo.jizhan.TLSignalApp"></module>"TLSignalApp",指向对应的类文件".demo.jizhan.TLSignalApp"。
框架将自动根据配置文件里面的模块名字由模块工厂创建模块实例。所有的模块、对象都有一个名字,通过名字来调用模块更加灵活,因为字符串比对象句柄更容易传递、保存,而且也实现一定的解耦和独立。如果想更换模块或类,只需更改配置文件对应的类文件即可。
由统一的模块工厂创建对象实例,方便实例的传递、共享。
TLSignalApp模块收到消息指令“setup”后,执行对应的消息指令,在TLSignalApp类代码中:
@Override
protected TLMsg myCheckMsgCmd(Object fromWho, TLMsg msg) {
TLMsg returnMsg=null;
switch (msg.getAction()) {
case "setup":
cell_id="";
lac ="";
putMsg(tltm,new TLMsg("init","listener",this)
.setParam("listenAction","getSignal"));
break;
TLSignalApp通过方法myCheckMsgCmd获取传入的消息,分析指令,然后根据指令完成相应的功能。这里获取setup消息指令后开始启动基站信号采集。为实现信号采集,我设计了信号采集模块TLTelephonyManager类,采集模块在TLSignalApp初始化时进行了实例化,模块TLSignalApp也不负责创建其他对象,而是通过给模块工厂moduleFactory发送消息来获取模块实例,代码如下:
protected void init(){
super.init();
TLMsg msg =createMsg();//构建消息
//设置消息指令及参数
msg.setAction("getModule").setParam("moduleName","cn.tianlong.tlandroid.utils.TLTelephonyManager") ;
//给工厂发送消息获取模块实例
tltm= (TLTelephonyManager) putMsg(moduleFactory,msg).getParam("instance");
TLSignalApp在setup指令中,直接给信号采集模块实例tltm发送启动消息。启动消息通过setParam("listenAction","getSignal")告诉采集模块对于采集结果的返回消息指令是getSignal,采集模块实例tltm通过发送消息指令getSignal来传递采集信息。来看TLSignalApp模块中的返回消息指令代码:
case "getSignal":
getSignal( msg);
break;
TLSignalApp对于getSignal消息执行处理方法getSignal:
private void getSignal(TLMsg msg) {
dbmsg=msg;
List<BaseDataBean> list =(List<BaseDataBean> )msg.getParam("jizhan") ;
data =list.get(0);
dbmsg.setParam("cell_id",cell_id);
if(cell_id.equals(data.getCell_id()) ==false|| lac.equals(data.getLac())==false)
{
cell_id =data.getCell_id();
lac =data.getLac();
dbmsg.setParam("cell_id",cell_id);
queryAddress();
}
msg.setAction("getSignal");
putMsg((IObject) activity,msg);
}
在getSignal方法中,获得采集到的基站的信息,然后通过方法queryAddress()查询互联网获取基站位置信息,同时将基站信息发送给主activity页面显示:
msg.setAction("getSignal");
putMsg((IObject) activity,msg);
在主activity中,消息"getSignal"对应的处理方法为showResponse函数:
private void showResponse(TLMsg msg) {
List<BaseDataBean> list =(List<BaseDataBean> )msg.getParam("jizhan") ;
BaseDataBean data =list.get(0);
cell_id =data.getCell_id();
lac =data.getLac();
signaltext.setText(nn+" dbm:"+msg.getParam("dbm")+" level:"+msg.getParam("signallevel")+" cell_id:"+cell_id +" lac:"+lac+
" lte_sinr:"+(int) msg.getParam("lte_sinr")
+" lte_rsrp:"+(int) msg.getParam("lte_rsrp")
+" lte_rsrq:"+(int) msg.getParam("lte_rsrq")
+" lte_rssnr:"+(int) msg.getParam("lte_rssnr")
+" lte_cqi:"+(int) msg.getParam("lte_cqi"));
nn++;
}
showResponse方法通过设置文本框signaltext内容显示基站信息。
方法queryAddress()用于根据基站id查询基站位置信息:
private void queryAddress(){
TLMsg msg =createMsg();
String url ="http://api.cellocation.com:81/cell/?";
HashMap<String,String> params=new HashMap<>();
params.put("mcc","460");
params.put("mnc","1");
params.put("lac",lac);
params.put("ci",cell_id);
params.put("output","xml");
msg.setAction("get")
.setDestination(WEBSERVER)
.setParam("url",url)
.setParam("params",params)
.setParam("activity",activity)
.setParam("resultFor",this)
.setParam(RESULTACTION,"jizhanData");
putMsg(appCenter,msg);
}
方法的内部流程也基本是先创建消息,然后将消息发送给webserver。这里将消息发送给appCenter模块,通过在消息中指明当前消息目的:.setDestination(WEBSERVER),由appCenter路由转发给WEBSERVER,当然也可以直接将消息发送给WEBSERVER。对于WEBSERVER的返回消息,通过设置消息参数来告诉WEBSERVER结果返回给谁及返回的消息指令:
setParam("resultFor",this) // 结果返回给当前对象
setParam(RESULTACTION,"jizhanData");// 返回的消息指令
在TLSignalApp的消息指令jizhanData中处理中获取到WEBSERVER的返回结果:
private void jizhandata(Object fromWho, TLMsg msg) {
msg.setAction("jizhan");
putMsg((IObject)activity,msg);
}
这里没有进一步的对web结果进行处理,而是直接发送给主activity显示。如果需要对web结果处理后再显示,可以在这加处理逻辑。在主activity代码中,针对发送来的web结果直接显示:
private void showjizhan(TLMsg msg) {
String status ="" ;
if(msg.getParam("error")!=null && (boolean)msg.getParam("error")==false)
{
String response =(String) msg.getParam("response");
jizhantext.setText(response);
}
else
jizhantext.setText("查询网络错误");
}
到此,在主activiy启动后,通过消息的传递,将模块建立、基站信息获取、基站地址获取、获取后的信息显示等各项任务自动完成,消息流程图如下:
从上图看activity启动后的消息流程:
1、activity 给 TLSignalApp发送启动消息
2、TLSignalApp给手机信号采集模块TLTelephonyManager发送启动采集消息
3、采集模块TLTelephonyManager讲采集结果送到TLSignalApp。
4、TLSignalApp将采集结果发送消息到activity,activity显示输出
5、TLSignalApp根据基站id发送消息给WEBSERVER查询互联网基站位置
6、WEBSERVER将查询到的基站位置返回给TLSignalApp。
7、TLSignalApp将基站位置发送消息到activity,activity显示输出
上面消息流程线路自然体现了一个事物之间的逻辑关系。而我们常规的设计缺少事物的逻辑,比如我们常见的一个设计形式:
object 汽车1 =new 汽车();
汽车1.启动();
汽车1.行驶();
汽车1.停止();
这里面为什么先执行启动(),而不是行驶()? 代码不懂为什么,逻辑关系在哪呢?在开发者的头脑里。因此我们看其他人的代码就非常困难,我们要去猜设计者的思想逻辑。
查看历史记录功能
主activity另启动一个activity显示出保存在本地数据库的基站信息:
msg.setMsgId("startactivity"); ;
msg.setParam("classname", RecoderActivity.class);
putMsg(appCenter,msg);
前面我们说过activity只负责输入与输出功能,启动另一个activity的责任交由appCenter模块来执行。在appCenter模块中将"startactivity"消息传递给TLAndroid模块进行启动页面。TLAndroid模块相当于一个虚拟android环境,负责android个性组件的创建,这么设计主要是将模块功能分类,逻辑上比较清晰。在记录RecoderActivity页面里,创建一个recyclerView来显示保存的记录,其中所有的数据查询、显示也遵循上面的消息传递模式。
自动保存上传
已经有了手动保存、上传,自动模式无非是自动调用手动的方法,常规编程是启动两个线程,每个线程周期调用手动的方法,这里就涉及线程建立、传递对象实例及回调方法,这比较麻烦的。对象消息编程一切都是针对消息处理,因此在线程中只要将手动的消息执行即可,在TLSignalApp模块初始化init方法中:
TLMsg tmsgsave=createMsg().setAction("saveData").setDestination(name).setParam("delay","5");
TLMsg tmsgpost=createMsg().setAction("postData").setDestination(name).setParam("delay","5");
jztask= (Jztask) getModule("jztask");
putMsg(jztask,createMsg().setAction("registTask").setParam("msg",tmsgsave));
putMsg(jztask,createMsg().setAction("registTask").setParam("msg",tmsgpost)
.setNextMsg(createMsg().setAction("setup")));
首先构建两个执行消息,这两个消息指明对应的手动数据保存、上传方法,然后获取任务模块jztask(该模块功能为周期运行消息),将执行消息注册到任务模块的任务表中,任务模块届时将自动运行消息。消息可以组成消息链,模块可对消息链中的消息依次执行,因此通过setNextMsg(createMsg().setAction("setup") 设置了下条消息指令为setup,这样任务模块注册完消息任务后自动执行setup消息,模块初始化。
理解了上面的消息传递过程分析,也很容易看懂数据保存、上传等功能的消息流程,这里不再叙述。
总结
以往我们直接调用一个对象的方法,现在通过传递一个消息让对方执行,多了一个传递消息的过程,那么多一个环节的目的是什么呢?是不是多此一举?目的就是自己的事情自己做,不互相牵扯,保持独立和自由。当然具体实践时也要根据情况,简单的或底层的对象如果直接调用方法更有效率也不影响整体的灵活,那就直接调用,并没有绝对的规则。对于消息编程我认为一个可能弊端是运行效率,由过去执行一个方法变成执行多个方法肯定要多花时间。那么这要在具体实际中斟酌是否影响,因为效率存在多个环节,应用需求、设计方法、网络、服务器端、硬件等各方面。目前运行效率与开发效率是个矛盾,框架提高开发效率但必然降低程序运行效率。
我认为任何的设计模式目的都是为了在达到应用需求前提下,尽量提高开发者工作效率。实现这目的关键是设计的灵活性和结构化。垂直上层次化,各层负责各层的事情、互不干涉;水平模块化,各模块自负其责、逻辑关系清晰,这样程序才具有灵活性也更方便维护。如果一个功能跨多层、分散到几个模块中,那么当然查错、更改就比较麻烦了。一个对象的对外开放方法越少,功能越专一,则其独立性越强,各模块相互影响就越小。在我的消息对象中只有两个公共方法——送出消息、接受消息,那么在这两个点检查消息就能清晰的划分责任,如果消息送来的是正确的,那么处理是自己的事情,如果消息送出是正确的,那么处理是对方的事情,责任很明确,这也更适于调试、合作开发。
想进一步了解对象消息框架请访问博客 。