第一个完整Andorid项目总结

本文目的:把自己一阶段的东西进行总结,拿出来和大家分享,也希望从读者的评论那里得到启发。

本人水平:华东师范大学软件学院毕业,做过两个J2EE项目,一个J2ME项目,一个Android项目(基本就靠Java混饭吃)。

项目介绍:一款IM应用,类似于QQ,用户群是企业员工,和企业QQ是竞争对手。下载地址:http://apk.gfan.com/Product/App278699.html。

类似项目介绍:陌陌,QQ,微信等等,说白了就是手机即时聊天软件。个人认为,手机聊天软件中,光从用户体验来说,做的最好的是陌陌(也不妄称约炮神器,昨晚上我下载了陌陌,今天就钓到一个贵州少女)。

好了,言归正传,开始项目的点点滴滴。整个项目经历了前期需求分析,技术要难点定位,编码/测试,验收测试,上架(看了项目阶段是不是觉得有问题?他妈的确实有问题,后期吃了很多苦)。

前期需求分析:这个简单,就是在规定的项目时间内,计划完成多少功能。由于是给自己公司做产品,所以肯定要分一期,二期……,由于时间关系,我们第一期只完成最简单的单人聊天。然后根据其他已有的聊天软件,设计组给出了最初的设计稿(由于设计组人太少,只给出了最简单的原型设计,其他的,都是在PC组同事帮助下完成的功能设计)。

技术要难点定位:这里就简单的说了,从我开始做这个项目到现在,觉得有两块地方比较难。1.原始数据的拉取;2.通信协议的选择。下面展开分析。
       原始数据的拉取:做一个聊天软件,大家可能觉得功能难点在聊天模块,其实不然,软件的初始化才是难点。要聊天,你总要有你的好友,好友的状态之类的数据哇。由于我们的软件是针对企业级的,数据量非常大,好友上限是20000,那么意思就是,你还没有聊天之前,可能要拉取20000人的基本资料……在这一块,开始参考了PC版的成熟算法,但是效果非常不理想,性能很棒的手机,在WIFI状态下,拉取20000人的账户,也要几分钟,这是用户不能接收的。后来采用的并发请求的方式,比较好的解决了这个问题。这里的并发指的是:逻辑层把要请求的数据包全部发送给网络层,网络层缓存起来,再不停的发送。(这种方式为什么比发送一个包,接收以后再发送下一个包好?不清楚的可以留言,我会回复)。现阶段,拉取5000人的账户,时间大概在30秒,当然还有优化的空间。做手机应用,应该有一个共识——弱化初始化。什么意思呢?就是登陆流程应该尽量简化,只拉取必要的数据,其他数据,可以在进入功能界面,用户开始使用功能以后再拉取。在本项目中,比如用户照片,用户状态等,都可以在“非繁忙”时段拉取,这样,初始化速度会快很多,用户体验会好很多。
      通信协议的选择:针对这个问题,我写了一篇专门的博客:http://blog.csdn.net/coding_or_coded/article/details/7266536,最后我们采用了TCP协议,后来也遇到了很多问题。在移动环境下,做TCP通信,还是有一定的挑战性的:在2G,3G,WIFI切换情况下,TCP通道都会无条件断掉,更不说在网络不稳定的地段,比如商场,地铁之类的,你的TCP链接会不停的断,然后你就必须不停的重新连接服务器。在这里,TCP连接断掉是不可避免的,我们要做的工作是在断了以后,怎么快速的恢复主功能(聊天功能)。在服务器端,我们采用了TCP复用机制,简单的说就是,你TCP断了,但是你在规定的时间又连上去了,那么,认为你是合法的用户,你就不需要再次走登陆流程了,就可以直接聊天了(这里细节很多,有想深入了解的可以给我留言)。
      补充一些额外的点:
1.通信数据格式:在这种数据密集型的项目中,采用XML,json之类的格式,绝对是找死,你可以很轻松的做出来,但是在正常的网络环境下,你的通信速度,绝对会比同类产品慢一拍,因为你的数据格式决定了你的数据大小,数据大小决定了通信时长。比较通用的做法就是自己拼凑byte数组,规定每个byte要表达的意义,然后一次通信规定一条协议,这可以最大程度的减少通信的数据
2.数据库:由于聊天软件,多用户是必然的,而针对于我们这样的大数据项目,数据库的设计也是一个难点。数据库的设计是我单独完成的,我采用的是一个用户对应一个数据库,这样数据库的查询数据可以得到一定的保证。同时,为了数据响应速度,在适当的尺度,可以考虑冗余数据来减少多表查询,这对速度有很大的帮助。最后,在大批量的插入和读取时,有三点可以帮助我们提升速度:A.使用原生sql语句,而不是android封装的API,封装,代表着优雅,但是也代表着效率低下,在小数据量的时候,我肯定采用android的API,傻逼才用原生语句。但是一旦数据量大了,你就需要掂量掂量。 B.使用sql预编译,这样可以减少sql语句编译次数,也可以较大程度的提升性能。C.使用事务,把多条sql语句放在一个事务中完成,既可以保证数据的完整性,也可以大大的提升性能。在5000条数据的插入测试中,使用上面每一点,基本上都可以提升一倍的速度。简而言之,比如你做一个大批量的插入,开始需要1分钟,但是采用了这三种方式,你最终的时间,只需要几秒钟。(需要成熟实例代码的可以留言)


编码/测试:大家也看到了,技术难点分析以后,就开始编码了,他妈逼的,连一个系统架构设计,或者编码规范制定都没有。这样的导致的结果就是,到了项目后期,由于开始没有做任何的项目规范,每个人的实现方式不同,整个代码的风格非常不一致,每个人负责的那一块,出了问题,只能由当事人修改……    而且由于开始没有良好的系统架构,整个项目模块之间耦合性非常的高,修改一个地方,极可能引起其他问题。程序员非常被动。对不起,我已经无力吐槽了,笑,懂?

测试:以前我很讨厌测试人员,总觉得他们在找麻烦,但是如果你真的想做好一个应用,一个优秀的测试是多么的重要,尊重测试人员吧,他们重复无数次无聊的操作,仅仅是为了找到你犯的错。当然,有些测试也很讨厌,特别是验收测试中(就是快上线了),一个屁大的问题,非要闹的大家都知道,整的开发人员很没有面子,其实在验收测试中,小bug是可以下期再修改的,这个大家都可以理解。

上架:在上架之前,简单说一下打包的问题,打包的时候,要自己生成一个key,那么,这个key的有效期最好是长一点(怎么的也要50年吧),而且这个key一定要保存好,你的应用,在以后的任何一次打包中,都应该使用这个key,如果你不这样做,也可以,等着老板骂娘吧。


下面谈一下项目中遇到的一些问题:

1.软件进不去:由于项目数据量比较大,在性能差一点的手机,拉取数据过程中,总是stack溢出,导致登陆失败,然后软件退出。后来发现是使用了递归方法,当然,为什么使用递归就会导致stack溢出也没有去查(有些人就是喜欢把问题解决了就算了,真正的原因不关心),直接使用了for循环替代递归。当然,从大学学习算法的时候,我就没有搞懂递归,始终觉得,可以设计递归算法的人,很厉害……

2.CPU消耗达到90%:QQ的CPU消耗基本在1%以下,而我们的应用居然不小心就达到90%,你让我情何以堪啊……   后来采用工具分析,也没有分析出什么玩意(可能是我笨),但是笨鸟先飞,老子不知道问题在哪里,那我就一段代码一段代码的注释,直到找到有问题的类,方法,代码段。后来才发现,有一个消息循环没有退出,只要运行到那里以后,就会一直给UI线程的消息队列发送消息,这样导致CPU消耗非常高。CPU消耗高了以后,机器就会发热,热到烫手,都他妈的可以煮鸡蛋了……    而且我把手机连上电脑,边充电边调试,一会儿就会报电量不足,要停一会儿,才可以继续……

3.用户界面卡死:引起原因,在多线程同步的时候,大量使用全局对象锁,导致等待,卡死。在这里给各位屌丝普及一下Java锁的种类(在d8,估计清楚这点的不超过三个),Java锁分为两种,一种是类锁(synchronized加在方法上,而且方法有static关键字),一种是对象锁(synchronized加在方法上,但是这个方法不是静态方法,或者直接是synchronized代码块)。在使用synchronized的时候,范围要尽量小,而且采用的锁对象波及的范围也要小(听不懂的可以留言,就这一点展开说都至少要写一篇文章)。

4.软件不能正常退出:可悲的是,现在都还没有找到具体原因,采用了网上说的多种方案,都他妈的退不出去,具体的bug是activity被杀掉的时候,又莫名其妙的启动了。后来的解决办法是在每个activity生成的时候,给它注册一个广播,然后要杀死所有activity的时候,只需要发送一个广播,其他的就不用管了。

5.沟通:这里讲两个小例子:一个是我同学,他大学基本上没有敲过代码,第一家公司和我一样,也觉得他敲代码的能力不强,但是领导很认可他,后来我才发现,这个人很会交流,他不懂的话,他就把问题抛出来,让大家一起解决。整个团队效率很高,但是有很多程序员不是这样……   第二个例子:我们项目组的人就特别不喜欢交流,严重到他在认真敲代码的时候,你去问他东西,他都很不乐意。当然,他有问题,也不会抛出来,都自己搞……      记得我一个类中有一个方法,我是自己用的,使用的是private关键字,结果,后来程序报错,就报在这个方法,我才发现,这个方法被别人改为public了,而且到处在调用,真的,我无力吐槽……

6.未解决问题:长时间运行于这
后台,程序被Android系统杀死。个我实在是没有找到合适的解决办法,参考了QQ的表象(从软件行为判断,具体的实现我也不清楚),在系统杀死程序的时候,通知栏图标并不会消失,QQ的实现方式估计是:在程序被杀死以后,如果有新的消息,则服务器push到手机,一旦手机接收到消息,则发出提醒(通知栏,声音,震动),用户点击通知栏时,则重新初始化程序。这样的结局就是:用户一致可以收到消息,也就是达到了一个目的:程序一直在运行的假象。如果有这方面经验的朋友请不吝赐教……

7.数据并发问题:根据应用的特点,客户端会多次发送请求包到服务器,比较好的方式是并发处理,一次性把所有包发出去,然后再慢慢的接收。但是这样会有一个问题,TCP的缓冲区可能溢出。这个时候,可以根据一个经验值来做并发控制,比如一次性发50个包,等这些包接收完了以后,才发下一批次的50个包。注意这里的并发控制应该在网络模块处理,如果放在业务模块,会有两个问题:1.业务模块不关心并发控制,并发控制会让业务模块代码凌乱;2.很多业务都需要并发控制,这可能导致并发控制代码满天飞。

8.crash问题:crash是最不能让用户接受,也最让开发人员难受的bug。在项目稳定以后,crash发生的情况不会很多,而且发生的条件不容易模拟,这给crash的修改带来了挑战,不容易复现和定位的bug,是真BUG……   解决这个问题的唯一办法就是crash报告,当发生crash时,记录下crash的堆栈信息,然后下次使用软件时上传crash信息,我们根据crash信息来跟踪和定位bug。crash的堆栈信息一定要尽量详细,这会给修复人员带来帮助。

下面是项目里面一些不错的工具类:

1. 关于SharedPreferences的封装类:
package com.imo.util;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;

import com.imo.global.IMOApp;

/**
 * SharedPreference封装
 */
public class PreferenceManager {
	/**
	 * 在指定的文件中保存数据
	 * 
	 * @param fileName
	 *            文件名称
	 * @param objs
	 *            数组{key,value}
	 */
	public static void save(String fileName, Object[] objs) {
		try {
			SharedPreferences sp = IMOApp.getApp().getSharedPreferences(fileName,Context.MODE_APPEND);//IMOApp.getApp()代表Application
			Editor editor = sp.edit();
			if (objs[1] instanceof String) {
				editor.putString(objs[0].toString(), objs[1].toString());
			} else if (objs[1] instanceof Integer) {
				editor.putInt(objs[0].toString(),Integer.parseInt(objs[1].toString()));
			} else if (objs[1] instanceof Long) {
				editor.putLong(objs[0].toString(),Long.parseLong((objs[1].toString())));
			} else if (objs[1] instanceof Float) {
				editor.putFloat(objs[0].toString(),Float.parseFloat((objs[1].toString())));
			} else if (objs[1] instanceof Boolean) {
				editor.putBoolean(objs[0].toString(),Boolean.parseBoolean((objs[1].toString())));
			}
			editor.commit();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 在指定的文件中读取数据
	 * 
	 * @param fileName
	 *            文件名称
	 * @param objs
	 *            数组{key,defaultValue}
	 */
	public static Object get(String fileName, Object[] objs) {
		try {
			SharedPreferences sp = IMOApp.getApp().getSharedPreferences(fileName,Context.MODE_APPEND);
			if (objs[1] instanceof String) {
				return sp.getString(objs[0].toString(), objs[1].toString());
			} else if (objs[1] instanceof Integer) {
				return sp.getInt(objs[0].toString(),Integer.parseInt(objs[1].toString()));
			} else if (objs[1] instanceof Long) {
				return sp.getLong(objs[0].toString(),Long.parseLong((objs[1].toString())));
			} else if (objs[1] instanceof Float) {
				return sp.getFloat(objs[0].toString(),Float.parseFloat((objs[1].toString())));
			} else if (objs[1] instanceof Boolean) {
				return sp.getBoolean(objs[0].toString(),Boolean.parseBoolean((objs[1].toString())));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
}

2.一个简单的去重算法(设计的目的:客户端不断的接收到消息,但是有可能有重复的消息,客户端要负责去重)
package com.imo.util;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
 * 
 * 简单去重算法
 * 
 * 定制的默认大小为capacity的容器,当容量大于capacity+count时,删除count个元素
 * 
 * @author fengxiaowei
 * 
 */
public class CustomList {
	private Map<Integer, Integer> map = new HashMap<Integer, Integer>();
	private LinkedList<Integer> link = new LinkedList<Integer>();

	private int capacity = 100;

	private int count = capacity / 2;

	public CustomList(int capacity) {
		this.capacity = capacity;
		this.count = capacity / 2;
	}

	public boolean contains(Integer obj) {
		return map.containsKey(obj);
	}

	public void add(Integer obj) {
		if (map.size() >= capacity + count)
			for (int i = 0; i < count; i++) {
				map.remove(link.removeFirst());
			}
		map.put(obj, obj);
		link.add(obj);
	}
	
	public void clear(){
		map.clear();
		link.clear();
	}
	
	@Override
	public String toString() {
		String result ="";
		for(int i=0;i<map.size();i++){
			result = result +"  "+ link.get(i);
		}
		return result;
	}

}

一个Toast的工具类,优化了Toast的显示逻辑(用户不停的点击,会不停的弹出Toast,而且Toast在屏幕上持续很久):
public class ToastUtil {
 
    private static Handler handler = new Handler(Looper.getMainLooper());
 
    private static Toast toast = null;
     
    private static Object synObj = new Object();
 
    public static void showMessage(final Context act, final String msg) {
        showMessage(act, msg, Toast.LENGTH_SHORT);
    }
 
    public static void showMessage(final Context act, final int msg) {
        showMessage(act, msg, Toast.LENGTH_SHORT);
    }
 
    public static void showMessage(final Context act, final String msg,
            final int len) {
        new Thread(new Runnable() {
            public void run() {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        synchronized (synObj) {
                            if (toast != null) {
                                toast.cancel();
                                toast.setText(msg);
                                toast.setDuration(len);
                            } else {
                                toast = Toast.makeText(act, msg, len);
                            }
                            toast.show();
                        }
                    }
                });
            }
        }).start();
    }
 
 
    public static void showMessage(final Context act, final int msg,
            final int len) {
        new Thread(new Runnable() {
            public void run() {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        synchronized (synObj) {
                            if (toast != null) {
                                toast.cancel();
                                toast.setText(msg);
                                toast.setDuration(len);
                            } else {
                                toast = Toast.makeText(act, msg, len);
                            }
                            toast.show();
                        }
                    }
                });
            }
        }).start();
    }
 
}



等待持续更新……

你可能感兴趣的:(android,数据库,tcp,测试,手机,聊天)