从图书馆借了一本《Android项目实战——手机安全卫士开发案例解析》,想通过学习源代码来加深对Android重点知识的理解,以及进一步复习领悟JAVA SE。接下来的两个月,一边学习一遍记录重要的知识点,希望自己能有所收获、有所提高。
Splash界面的作用:
1.展现产品的LOGO,提升产品的知名度。
2.初始化的操作(初始化数据库、文件的复制、配置的读取)。
3.根据系统的时间或日期做出相应的判断来加载不同的Splash界面(例如,QQ的登陆界面),提升用户体验。
4.连接服务器,检查获取更新信息,提示用户升级。在此项目中用于连接服务器,检查版本是否需要更新下载,以及初始化数据库。
问题1:PackageManager pm=this.getPackageManager();Android源代码里他是怎么通过getPackageManager()这个方法实例化PackageManager的?
大概明白点了,在源码里找到了ContextImpl.java,里面就有getPackageManager()方法,此方法源码如下:
1 @Override 2 public PackageManager getPackageManager(){ 3 if(mPackageManager !=null){ 4 return mPackageManager; 5 } 6 IPackageManager pm=ActivityThread.getPackageManager(); 7 if(pm!=null){ 8 //Doesn't matter if we make more than one instance. 9 return (mPackageManager=new ApplicationPackageManager(this, pm); 10 } 11 return null; 12 }
去掉标题栏以及设置全屏模式 //设置为无标题栏 requestWindowFeature(Window.FEATURE_NO_TITLE); //设置为全屏模式 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
Splash界面加载时的具体流程:
1.为Splash界面做一个淡进(由暗变明)的动画效果,动画播放时间为2秒。
2.在Splash界面加载时,连接服务器检查软件是否需要更新(后面还需要实现对数据库的复制),其流程如下图:
context和getApplicationContext()的区别
getApplicationContext()返回应用的上下文,生命周期是整个应用,应用摧毁它才摧毁。
context返回当前Activity的上下文,Activity执行了onDestory()方法后摧毁它就摧毁,如果组件是属于当前Activity的,就应该使用context。
getBaseContext()返回由构造函数指定或setBaseContext()设置的上下文。
如果我们要通过一个上下文来执行某个动作,且希望该动作一直处于“活跃”状态,那么应当考虑使用getApplicationContext获取上下文。例如:当使用数据库时,需要传递一个上下文,如果传递的是Activity.this,那么,当Aactivity执行onDestory()方法时,数据库就会被关闭,应用程序就会出现错误。
Dialog窗体是Activity的一部分,一般当关乎到生命周期时,我们才会仔细分析使用哪个上下文,一般情况下使用Activity.this。
创建并显示对话框
1 private void showUpdateDialog(){
2 // 创建对话框的构造器
3 AlertDialog.Builder builder=new Builder(this);
4 //设置对话框提示标题左边的提示标语
5 builder.setIcon(getResources().getDrawable(R.drawable.xxx));
6 //设置对话框的标题
7 builder.setTitle("升级提示");
8 //设置对话框的提示内容
9 builder.setMessage("");
10 //设置升级按钮
11 builder.setPositiveButton("升级", new OnClickListener(){
12
13 @Override
14 public void onClick(DialogInterface dialog, int which) {
15
16 }
17 });
18 builder.setNegativeButton("取消", new OnClickListener(){
19 @Override
20 public void onClick(DialogInterface dialog, int which) {
21 }});
22 builder.create().show();
23 }
private Handler handler=new Handler(){
public void handleMessage(Message msg){
switch(msg.what){
case XXX:
.......
break;
......
}}}
该对象是为了接收子类线程发送过来的消息(子线程与主线程进行通信),将子线程发送过来的消息在handleMessage(Message msg)方法中进行处理。
为什么要将URL放在res/values目录下的config.xml中?
String serverurl=getResources().getString(R.string.serverurl):得到访问服务端配置信息(即服务端的info.xml文件)的URL地址。
该URL地址存在values文件夹下的config.xml文件中,目的在于,当这个URL需要变更时,不需要在代码中进行修改,只需要修改这个配置文件即可实现,降低了开发成本。配置信息如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="serverurl">http://192.168.0.4:8080/info.xml<string> <resources>
问题2:在手机上运行调试时出现了如下图错误
很明显是空指针异常。而且是在SplashActivity.java源代码中的第95行。去代码中仔细查看,原来是缺少一句
r1_splash=findViewById(R.id.rl_splash);即使前面已经声明了全局变量private View r1_splash;但并没有初始化r1_splash。
解析服务端的info.xml文件时,需要创建一个解析XML的业务方法。代码如下:
1 /** 2 * 解析XML数据 3 * 4 */ 5 public class UpdateInfoParser { 6 7 public static UpdateInfo getUpdateInfo(InputStream is) throws XmlPullParserException, IOException{ 8 //获得一个XmlPullParser的解析实例 9 XmlPullParser parser=Xml.newPullParser(); 10 //将要解析的文件流传入 11 parser.setInput(is,"utf-8"); 12 //创建UpdateInfo实例,用于存放解析得到的XML中的数据,最终将该对象返回 13 UpdateInfo info=new UpdateInfo(); 14 //获取当前触发的事件类型 15 int type=parser.getEventType(); 16 //使用while循环,如果获得的事件码是文档结束,那么就结束解析 17 while(type!=XmlPullParser.END_DOCUMENT){ 18 if(type==XmlPullParser.START_TAG){//开始元素 19 if("version".equals(parser.getName())){ 20 //判断当前元素是否是读者需要检索的元素,下同 21 //因为内容页相当于一个节点,所以获取内容时需要调用parser对象的nextText() 22 //方法才可以得到内容 23 String version=parser.nextText(); 24 info.setVersion(version); 25 }else if("description".equals(parser.getName())){ 26 String description=parser.nextText(); 27 info.setDescription(description); 28 }else if("apkurl".equals(parser.getName())){ 29 String apkurl=parser.nextText(); 30 info.setApkurl(apkurl); 31 } 32 } 33 type=parser.next();//触发下一个事件,并返回事件的类型 34 } 35 return info; 36 } 37 }
为Splash界面播放一个动画
AlphaAnimation aa=new AlphaAnimation(0.3f,1.0f)设置了一个透明度由0.3f~1.0f的淡入的动画效果。0.0f表示完全透明,1.0f表示正常显示效果。
aa.setDuration(2000)为设置动画的执行时间,单位为毫秒。
r1_splash.startAnimation(aa)为启动动画。
此外,查阅API还发现Animation类还有这几个子类AnimationSet,RotateAnimation,ScaleAnimation,TranslateAnimation。
new Thread(new CheckVerisonTask()){}.start()
由于联网的过程是一般是一个耗时的操作,为了避免出现ANR异常,我们在主线程中开启一个子线程用于联网核对版本号信息。此时我们还应该在清单文件中配置网络权限的信息:
<uses-permission android:name="android.permission.INTERNET"/>
Handler
handler.sendMessage(msg):通过handler对象向主线程中发送消息,然后在Handler的handleMessage(Message msg)方法中可以处理该消息。
msg.what=GET_INFO_SUCCESS:为msg做一个标记,这样在Handler的handleMessage(Message msg)的Switch中可以获取该标记,以便识别是哪个消息。
下载APK的工具类:
1 /** 2 * 下载的工具类:下载文件的路径;下载文件后保存的路径;关心进度条;上、下文 3 * 4 */ 5 public class DownLoadUtil { 6 /** 7 8 * 下载一个文件 9 * @param urlpath 10 * 路径 11 * @param filepath 12 * 保存到本地的文件路径 13 * @param pd 14 * 进度条对话框 15 * @return 16 * 下载后的apk 17 */ 18 public static File getFile(String urlpath,String filepath,ProgressDialog pd){ 19 try { 20 URL url=new URL(urlpath); 21 File file=new File(filepath); 22 FileOutputStream fos=new FileOutputStream(file); 23 HttpURLConnection conn=(HttpURLConnection) url.openConnection(); 24 //下载的请求是GET方式,conn的默认方式也是GET请求 25 conn.setRequestMethod("GET"); 26 //服务端的响应时间 27 conn.setConnectTimeout(5000); 28 //获取服务端的文件总长度 29 int max=conn.getContentLength(); 30 //将进度条的最大值设置为要下载的文件的总长度 31 pd.setMax(max); 32 //获取要下载的apk的文件输入流 33 InputStream is=conn.getInputStream(); 34 //设置一个缓存区 35 byte[] buf=new byte[1024]; 36 int len=0; 37 int process=0; 38 while((len=is.read(buf))!=-1){ 39 fos.write(buf, 0, len); 40 //没读取一次输入流,就刷新一次下载进度 41 process+=len; 42 pd.setProgress(process); 43 //设置睡眠时间,便于观察下载进度 44 Thread.sleep(30); 45 } 46 //刷新缓存数据到文件中 47 fos.flush(); 48 //关流 49 fos.close(); 50 is.close(); 51 return file; 52 } catch (Exception e) { 53 // TODO Auto-generated catch block 54 e.printStackTrace(); 55 return null; 56 } 57 }
获取一个路径中的文件名的代码:
1 /** 2 * 获取一个路径中的文件名。例如:mymobilesafe.apk 3 * @param urlpath 4 * @return 5 */ 6 public static String getFileName(String urlpath){ 7 return urlpath.substring(urlpath.lastIndexOf("/")+1, urlpath.length()); 8 9 10 }
涉及对Sdcard的操作,需要在清单文件中配置相应的权限:
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
安装APK的方法:
1 /** 2 * 安装一个apk文件 3 * @param file 要安装的完整的文件名 4 */ 5 protected void installApk(File file){ 6 //隐式意图 7 Intent intent=new Intent(); 8 intent.setAction("android.intent.action.VIEW");//设置意图的动作 9 intent.addCategory("android.intent.category.DEFAULT"); 10 //为意图添加额外的数据 11 intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");//设置意图的类型和数据 12 startActivity(intent);//激活该意图
LayoutInflater:
布局填充器,通过getSystemService(Context.LAYOUT_INFLATER_SERVICE)方法可以实例化一个LayoutInflater,也可以通过LayoutInflater inflater=getLayoutInflater();来获取。然后使用inflate()方法在填充xml布局文件。
还可以使用LayoutInflater.from(this)来实例化。
最简单的方式是 View view=View.inflate(this, R.layout.xxx, null);
LayoutInflater的作用类似于findViewById(),不同的是LayoutInflater填充的是layout文件夹下的xml布局文件进行实例化,而findViewById()是某个xml布局文件中的某个widget控件。setContentView()一旦调用, layout就会立刻显示UI;而inflate只会把Layout形成一个以View类实现成的对象。
一般在activity中通过setContentView()将界面显示出来,然后才能使用findViewById()是找xml布局文件下的具体widget控件(如Button、TextView等)
但是如果还需要对其他布局进行操作,这就需要LayoutInflater动态加载, LayoutInflater是用来找res/layout/下的xml布局文件,并且实例化。
gravity和layout_gravity的区别
LinearLayout有两个非常相似的属性:gravity和layout_gravity。
区别在于:android:gravity 是对该view中内容的限定。比如一个button里面的text,可以通过gravity设置text相对于button靠左,靠右,居中。
android:layout_gravity是用来设置该view相对于父view的位置。比如一个button在LinearLayout里,通过layout_gravity就可以设置button相对于LinearLayout是靠左,靠右还是居中。
此项目中,如果在main_item.xml中缺少android:gravity="center_horizontal",图标底下的文字就无法相对于图标居中显示。
baseAdapter
baseAdapter主要作用是给Spiner、ListView、GirdView来填充数据的。这个适配器取得了Adapter的最大控制权:程序要创建多少个列表项,每个列表项的组件都由开发者来决定。继承了baseAdapter接口后需要覆写如下四个方法。
getCount():该方法返回值控制该Adapter将会包含多少个列表项。
getItem(int position):返回第position处的列表项的对象,如果不对这个返回的对象做相应的操作,可以返回一个null。
getItemId(int position):返回第position处的列表项的ID。
getView(int position,View convertView,ViewGroup parent):该方法的返回值决定第position出的列表的组件。
GridView在绘制的时候,系统首先调用getCount()函数,根据它的返回值得到GridView的长度,然后根据这个长度,调用getView逐一绘制每一个(行)。如果getCount()返回值是0,列表将不显示,若return 1 ,则只显示一行。系统显示列表时,首先实例化一个适配器,在适配器中getView方法中完成数据的映射。系统在绘制GridView中的每一个view时,都会调用getView()方法,getView()有三个参数,position表示显示的是第几个,covertView是从布局文件中通过inflate方法填充的布局,即将item.xml文件变为View实例用来显示,然后将xml文件中各个组件有findViewById()实例化。
适配器对象MainAdapter的代码如下:
1 public class MainAdapter extends BaseAdapter { 2 //将tv_name和iv_icon定义为静态的,有利于提高程序运行的效率, 3 //因为getView()方法会被调用好多次,定义为静态后(在内存中只有一份)就避免了 4 //多次在栈内存中创建变量tv_name和iv_icon的引用 5 private static TextView tv_name; 6 private static ImageView iv_icon; 7 //布局填充器 8 private LayoutInflater inflater; 9 //接收MainActivity传递过来的上下文对象 10 private Context context; 11 //将9个item的每一个图片对应的id都存入该数组中 12 private int[] icons={ 13 R.drawable.widget01,R.drawable.widget02,R.drawable.widget03, 14 R.drawable.widget04,R.drawable.widget05,R.drawable.widget06, 15 R.drawable.widget07,R.drawable.widget08,R.drawable.widget09 16 }; 17 //将9个item的每一个标题都存入该数组中 18 private String[] names={ 19 "手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","系统优化","高级工具","设置中心" 20 }; 21 public MainAdapter(Context context){ 22 this.context=context; 23 //获取系统中的布局填充器 24 inflater=(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 25 } 26 /** 27 * 返回gridview有多少个item 28 */ 29 @Override 30 public int getCount() { 31 // TODO Auto-generated method stub 32 return names.length; 33 } 34 /** 35 * 获取每个item对象,如果不对这个返回的item对象做相应的操作 36 * 可以返回一个null,这里我们简单处理一下,返回position 37 */ 38 @Override 39 public Object getItem(int position) { 40 // TODO Auto-generated method stub 41 return position; 42 } 43 /** 44 * 返回当前item的id 45 */ 46 @Override 47 public long getItemId(int position) { 48 // TODO Auto-generated method stub 49 return position; 50 } 51 /** 52 * 返回每一个gridview条目中的view对象 53 */ 54 @Override 55 public View getView(int position, View convertView, ViewGroup parent) { 56 // TODO Auto-generated method stub 57 View view=inflater.inflate(R.layout.main_item, null); 58 tv_name=(TextView) view.findViewById(R.id.tv_main_item_name); 59 iv_icon=(ImageView) view.findViewById(R.id.iv_main_item_icon); 60 tv_name.setText(names[position]); 61 iv_icon.setImageResource(icons[position]); 62 return view; 63 } 64 }
SharedPreferences
提供了一种轻量级的数据存储方式,主要应用在数据量比较少的情况下。它以“key-value”方式将数据保存在一个xml文件中。
使用SharedPreferences存储数据比较简单,步骤如下:
1)使用getSharedPreferences()生成SharedPreferences对象。调用getSharedPreferences()方法时,需要指定如下两个参数:
一是存储数据的xml文件名,这个xml文件存储在"/data/data/包名/shared_prefs/"目录下,其文件名由该参数指定,注意,文件名不需要指定后缀(.xml),系统会在该文件名之后自动添加xml后缀并创建之。
二是操作模式,其取值有三种:MODE_WORLD_READABLE(可读),MODE_WORLD_WRITEABLE(可写)和MODE_PRIVATE(私有)。
2)使用SharedPreferences.Editor的putXXX()方法保存数据。
3)使用SharedPreferences.Editor的commit()方法将上一步保存的数据写到xml文件中。
4)使用SharedPreference的getXXX()方法获取相应的数据。
源代码中:
1 sp=getSharedPreferences("config",MODE_PRIVATE); 2 boolean autoupdate=sp.getBoolean("autoupdate",true);
第一行,获取config.xml文件,如果该文件不存在,将会自动创建该文件,文件的操作类型为私有
第二行,从sp对应的config.xml文件中获取autoupdate所对应的boolean值,如果没有查找到该键,
将返回默认的boolean值true(即第二个参数)。
Checkbox的勾选状态发生改变时,用setOnCheckedChangeListener(new OnCheckedChangeListener(){});进行监听。代码如下:
1 /** 2 * 当Checkbox的状态发生改变时onCheckedChanged()方法被回调 3 */ 4 cb_setting_autoupdate.setOnCheckedChangeListener(new OnCheckedChangeListener() { 5 //第一个参数:当前的Checkbox;第二个参数:当前的Checkbox是否处于勾选状态 6 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 7 // TODO Auto-generated method stub 8 //获取编辑器 9 Editor editor=sp.edit(); 10 //持久化存储当前Checkbox的状态,当下次进入时,已然可以保存当前设置的状态 11 editor.putBoolean("autoupdate", isChecked); 12 //将数据真正提交到sp里面 13 editor.commit(); 14 if(isChecked){//Checkbox处于选中效果 15 //当Checkbox处于勾选状态时,表示自动更新已经开启,同时修改字体颜色 16 tv_setting_autoupdate_status.setTextColor(Color.GREEN); 17 tv_setting_autoupdate_status.setText("自动更新已经开启"); 18 }else{//Checkbox处于未勾选状态 19 tv_setting_autoupdate_status.setTextColor(Color.RED); 20 tv_setting_autoupdate_status.setText("自动更新已经关闭"); 21 22 } 23 } 24 });
为GridView对象中的item设置单击时的事件要使用setOnClickListener(new OnItemClickListener(){});
代码如下:
1 //为gv_main对象设置一个适配器,该适配器的作用是为每个item填充对应的数据 2 gv_main.setAdapter(new MainAdapter(this)); 3 //为GridView对象中的item设置单击时的监听事件 4 gv_main.setOnItemClickListener(new OnItemClickListener(){ 5 //参数一:item的父控件,也就是GridView 6 //参数二:当前单击的item 7 //参数三:当前单击的item在GridView中的位置 8 //参数四:id的值为单击GridView的哪一项对应的数值,单击GridView第九页,那么id就等于8 9 @Override 10 public void onItemClick(AdapterView<?> parent, View view, 11 int position, long id) { 12 // TODO Auto-generated method stub 13 switch(position){ 14 case 8://设置中心 15 //跳转到”设置中心“对应的Activity界面 16 Intent settingIntent=new Intent(MainActivity.this,SettingCenterActivity.class); 17 startActivity(settingIntent); 18 break; 19 } 20 } 21 });
android.view.View.OnClickListener与content.DialogInterface.OnClickListener()冲突
View.OnClickListener: Interface definition for a callback to be invoked when a view is clicked.
DialogInterface.OnClickListener: Interface used to allow the creator of a dialog to run some code when an item on the dialog is clicked..
在同一个activity中需要用到这个两个监听事件,若同时导入会有冲突。 解决办法是调用时都带上全路径名。例如
new android.content.DialogInterface.OnClickListener();
又一次遇到空指针异常:
View view=View.inflate(this, R.layout.first_entry_dialog, null);
//查找view对象中的各个控件
et_first_dialog_pwd=(EditText)view.findViewById(R.id.et_first_dialog_pwd);在这行代码中没有写view,而Activity类也有findViewById方法,如果不加view,则是默认调用this.findViewById,而Activity窗体布局中并没有et_first_dialog_pwd,所以导致了空指针异常。
MD5加密工具类代码(Md5Encoder.java)
1 public class Md5Encoder { 2 public static String encode(String password){ 3 try{ 4 //获取到数字消息的摘要器 5 MessageDigest digest=MessageDigest.getInstance("MD5"); 6 //执行加密操作 7 byte[] result=digest.digest(password.getBytes()); 8 StringBuilder sb=new StringBuilder(); 9 //将每个byte字节的数据转换成十六进制的数据 10 for(int i=0;i<result.length;i++){ 11 int number=result[i]&0xff;////向int[]赋值,&0xff的作用是消除对int前24位的影响 12 //(计算机中使用补码存储数据,如果直接将一个第一位为“1”的byte值赋给int,则前24为将为“1” 13 String str=Integer.toHexString(number);//将十进制的number转换成十六进制的数据 14 if(str.length()==1){//判断加密后的字符长度,如果长度为1,则在该字符前面补0 15 sb.append("0"); 16 sb.append(str); 17 }else{ 18 sb.append(str); 19 } 20 } 21 return sb.toString();//将加密后的字符转成字符串返回 22 }catch(NoSuchAlgorithmException e){//加密器没有被找到,该异常不可能发生,因为填入的“MD5”是正确的 23 e.printStackTrace(); 24 return ""; 25 } 26 } 27 }
我发现:书中52页说“然后在EditText文本框中输入”mp3“后,单击”确定“按钮,当再次进入程序主界面时,就可以看到修改后的标题生效了,”
再次进入程序主界面,需要退出应用后再次打开应用才能看到修改的标题,说明修改的动作发生在onCreate()方法里。但从LostProtectedActivity窗体返回到MainActivity窗体时,并不能看到更新。需要改进一下:还记得Activity从暂停态到运行态所触发的事件是onResume()方法,即当MainActivity重新获得焦点,开始与用户交互时回调此方法。所以我可以在MainActivity中覆写onResume()方法,即可完成更新UI,代码如下:
1 /** 2 * 当MainActivity重新获得焦点(即Activity开始与用户交互)时回调此方法 3 */ 4 @Override 5 protected void onResume() { 6 super.onResume(); 7 sp=this.getSharedPreferences("config",MODE_PRIVATE); 8 if((sp.getString("newname", "")!=null)){ 9 //重新加载GridView的适配器 10 gv_main.setAdapter(new MainAdapter(MainActivity.this));
本来考虑到使用Handler更新UI,费了好大劲还是没有成功。。才发现自己对Handler机制还是糊涂。先在这里重新温习一下Handler的基本知识吧。
Handler:
它的作用有两个——发送消息和处理消息,程序使用Handler发送消息,被Handler发送的消息必须被送到指定的MessageQueue。也就是说,如果Handler正常工作,必须在当前线程中有一个MessageQueue,否则消息就没有MessageQueue进行保存了。不过MessageQueue是由Looper负责管理的,也就是说,如果希望Handler正常工作,就必须在当前线程中有一个Looper对象。为了保证当前线程中有Looper对象,可以分如下两种情况处理。
1.主UI线程中,系统已经初始化了一个Looper对象,因此程序直接创建Handler即可,然后就可以通过Handler来发送消息、处理消息。
2.子线程中,必须由我们创建一个Looper对象,并启动它。创建Looper对象调用它的prepare()方法即可。
prepare()方法保证每个线程最多只有一个Looper对象。然后调用Looper的静态loop()方法来启动它。loop()方法使用一个死循环不断取出MessageQueue中的消息,并将取出的消息分给该消息对应的Handler进行处理。
在子线程中使用Handler的步骤如下:
1.调用Looper的prepare()方法为当前线程创建Looper对象,创建Looper对象时,它的构造器会创建与之配套的MessageQueue。
2.有了Looper之后,创建Handler子类的实例,覆写handlerMessage()方法,该方法负责处理来自于其他线程的消息。
3.调用Looper的loop()方法启动Looper。