因为移动设备的硬件配置各不相同,为了防止使用了不存在的设备资源,所以要对设备的硬件情况进行检查。一般情况下,前置摄像头、部分传感器在低端手机上是没有的,像SD卡也可能因为用户没插卡使得找不到SD卡资源。下面是校验这些硬件设备的说明:
Android4.0之后增加了多存储卡的支持,故一般手机有内置存储卡和外置存储卡(即SD卡),其中外置存储卡便是可选的。获取各个存储卡的磁盘路径,可通过系统服务STORAGE_SERVICE构造StorageManager对象,再使用反射机制调用getVolumePaths内部方法获得。磁盘路径符合Environment.getExternalStorageDirectory().getPath()的,就是默认的内置存储卡,否则就是外置存储卡。具体的示例代码如下:
public static String[] getVolumePaths(Context ctx) {
String[] paths = null;
StorageManager storMgr = (StorageManager) ctx.getSystemService(Activity.STORAGE_SERVICE);
Method method = null;
try {
method = storMgr.getClass().getMethod("getVolumePaths");
} catch (NoSuchMethodException e) {
e.printStackTrace();
return paths;
}
try {
paths = (String[]) method.invoke(storMgr);
} catch (Exception e) {
e.printStackTrace();
}
return paths;
}
后置摄像头对手机来说是标配,但前置摄像头就有部分低端机不支持。摄像头的详细介绍参见《Android开发笔记(五十六)摄像头拍照》。 检查前置摄像头是否存在,可通过获取摄像头个数来判断,个数多于一个就表示有前置摄像头。示例代码如下:
private void checkCamera() {
int cameraCount = Camera.getNumberOfCameras();
mDesc = String.format("%s\n\n摄像头个数=%d", mDesc, cameraCount);
for (int i=0; i<cameraCount; i++) {
Camera camera = Camera.open(i);
Parameters params = camera.getParameters();
List<Size> sizes = params.getSupportedPreviewSizes();
mDesc = String.format("%s\n%s摄像头支持的分辨率有%d种",
mDesc, (i==0)?"前置":"后置", sizes.size());
for (int j=0; j<sizes.size(); j++) {
Size size = sizes.get(j);
mDesc = String.format("%s\n分辨率%d为:宽%d*高%d", mDesc, j+1, size.width, size.height);
}
camera.release();
}
tv_check_hardware.setText(mDesc);
}
Android的传感器种类繁多,可是大多数手机都只支持少数几种,所以使用传感功能前要先校验当前设备是否存在对应的传感器。传感器的详细介绍参见《Android开发笔记(五十九)巧用传感器》。 获取当前支持的传感器列表的示例代码如下:
private String[] mSensorType = {
"加速度", "磁场", "方向", "陀螺仪", "光线",
"压力", "温度", "距离", "重力", "线性加速度",
"旋转矢量", "湿度", "环境温度", "无标定磁场", "无标定旋转矢量",
"未校准陀螺仪", "特殊动作", "步行检测", "计步器", "地磁旋转矢量"};
private void checkSensor() {
SensorManager sensroMgr = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
List<Sensor> sensorList = sensroMgr.getSensorList(Sensor.TYPE_ALL);
mDesc = String.format("%s\n\n传感器个数=%d", mDesc, sensorList.size());
for (int i=0; i<sensorList.size(); i++) {
Sensor sensor = sensorList.get(i);
mDesc = String.format("%s\n传感器%d的类型=%s,名称=%s",
mDesc, i+1, mSensorType[sensor.getType()-1], sensor.getName());
}
tv_check_hardware.setText(mDesc);
}
由于移动设备上资源有限,因此常常需要判断当前的剩余资源是否足够。比如说,发现剩余内存较低,则app不再进行大量消耗内存的操作,避免设备死机;又比如发现剩余磁盘空间不足,则app不再存储个头较大的图片或视频,避免设备爆盘;再比如发现当前应用的流量消耗较大,则app自动减少联网操作,避免被用户拉入黑名单。
获取设备的剩余内存大小,以及内存总量,可通过系统服务ACTIVITY_SERVICE构造ActivityManager对象,从中获得每个进程的内存使用情况。实现的示例代码如下:
//获取设备的内存总大小,单位KB
public static long getMemoryTotalSize() {
long totalSize;
// /proc/meminfo读出的内核信息进行解释
String path = "/proc/meminfo";
String content = null;
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path), 8);
String line;
if ((line = br.readLine()) != null) {
content = line;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
int begin = content.indexOf(':');
int end = content.indexOf('k');
// 截取字符串信息
content = content.substring(begin + 1, end).trim();
totalSize = Integer.parseInt(content);
return totalSize;
}
//获取当前的内存剩余大小,单位KB
public static long getMemoryLeftSize(Context ctx) {
ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
am.getMemoryInfo(mi);
return mi.availMem / 1024;
}
//获取本app使用的内存大小,单位KB
public static int getAppUserdMemory(Context ctx) {
int userdMemory = 0;
String packageName = ctx.getPackageName();
ActivityManager actMgr = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
// 获得系统里正在运行的所有进程
List<RunningAppProcessInfo> runningList = actMgr.getRunningAppProcesses();
for (RunningAppProcessInfo runningAppProcessInfo : runningList) {
// 进程ID号
int pid = runningAppProcessInfo.pid;
// 用户ID
int uid = runningAppProcessInfo.uid;
// 进程名
String processName = runningAppProcessInfo.processName;
if (processName.equals(packageName) != true) {
continue;
}
// 占用的内存
int[] pids = new int[] { pid };
MemoryInfo[] infoList = actMgr.getProcessMemoryInfo(pids);
if (infoList.length > 0) {
MemoryInfo info = infoList[0];
userdMemory = info.dalvikPrivateDirty;
}
}
return userdMemory;
}
}
磁盘分内部存储和外部存储(即SD卡)两种,内部存储的磁盘路径由下面代码获得:
String path = Environment.getDataDirectory().getPath();
外部存储的默认磁盘路径由下面代码获得:
String path = Environment.getExternalStorageDirectory().getPath();
获取磁盘剩余空间,以及总空间,实现的示例代码如下:
//获取指定路径的总空间,单位字节
public static long getStorageTotalSize(String path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
return getStorageTotalSizeNew(path)/1024;
} else {
return getStorageTotalSizeOld(path)/1024;
}
}
@SuppressWarnings("deprecation")
private static long getStorageTotalSizeOld(String path) {
File sdcardDir = new File(path);
StatFs sf = new StatFs(sdcardDir.getPath());
long blockSize = sf.getBlockSize();
long blockCount = sf.getBlockCount();
return blockSize*blockCount;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private static long getStorageTotalSizeNew(String path) {
File sdcardDir = new File(path);
StatFs sf = new StatFs(sdcardDir.getPath());
long blockSize = sf.getBlockSizeLong();
long blockCount = sf.getBlockCountLong();
return blockSize*blockCount;
}
//获取指定路径的剩余空间,单位字节
public static long getStorageLeftSize(String path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
return getStorageLeftSizeNew(path)/1024;
} else {
return getStorageLeftSizeOld(path)/1024;
}
}
@SuppressWarnings("deprecation")
private static long getStorageLeftSizeOld(String path) {
File sdcardDir = new File(path);
StatFs sf = new StatFs(sdcardDir.getPath());
long blockSize = sf.getBlockSize();
long blockCount = sf.getAvailableBlocks();
return blockSize*blockCount;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private static long getStorageLeftSizeNew(String path) {
File sdcardDir = new File(path);
StatFs sf = new StatFs(sdcardDir.getPath());
long blockSize = sf.getBlockSizeLong();
long blockCount = sf.getAvailableBlocksLong();
return blockSize*blockCount;
}
Android的流量数据保存在系统文件中,每次开机都会清零,所以查看系统文件得到的已使用流量,其实只是本次开机后的流量数据。系统级别的流量文件路径是/proc/net/dev,应用级别的流量文件路径是/proc/uid_stat/uid/tcp_rcv(注意中间的“uid”要替换为数字的应用id)。 不想解析文件的话,也可以使用Android的工具类TrafficStats来读取流量,该工具的常用方法如下: getTotalRxBytes : 获取接收流量的总字节数。 getTotalTxBytes : 获取发送流量的总字节数。 getMobileRxBytes : 获取移动接收流量的总字节数。(包括2G/3G/4G流量,不包括wifi流量) getMobileTxBytes : 获取移动发送流量的总字节数。 getUidRxBytes : 获取本进程接收流量的总字节数。本进程的应用ID可调用Process.myUid()获得。 getUidTxBytes : 获取本进程发送流量的总字节数。
查看app申请了哪些permission权限,可通过下面代码来检查:
private String[] mPerArray;
PackageManager pm = getPackageManager();
try {
PackageInfo pack = pm.getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
mPerArray = pack.requestedPermissions;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
if (mPerArray!=null && mPerArray.length>0) {
mDesc = String.format("%s\n当前请求的权限个数=%d", mDesc, mPerArray.length);
for (int i=0; i<mPerArray.length; i++) {
mDesc = String.format("%s\n权限%d的名称=%s", mDesc, i+1, mPerArray[i]);
}
} else {
mDesc = String.format("%s\n请求权限列表失败", mDesc);
}
tv_check_permission.setText(mDesc);
不过即使app申请了必要的权限,运行时仍有可能出错,原因除了缺少对应的硬件之外,还可能是相关功能未开启,甚至可能是安全软件强行屏蔽了部分权限。检查功能的开关状态(例如数据连接、GPS等),具体例子参见《Android开发笔记(五十五)手机设备基本操作》。如果是被安全软件屏蔽权限,则app很可能会扔出运行时异常,此时在代码中加入异常捕获情节,即可即时判断拥有权限与否。 下面是几个常用业务场景的权限检查例子:
判断是否能够正常定位,除了检查功能开关状态,还要检查是否存在定位提供者。定位功能的详细介绍参见《Android开发笔记(四十六)手机相关事件》。 检查定位权限的示例代码如下:
if (checkValid("android.permission.ACCESS_FINE_LOCATION") != true) {
return;
} else if (checkValid("android.permission.ACCESS_COARSE_LOCATION") != true) {
return;
} else {
if (SwitchUtil.getMobileDataStatus(this) == true
|| SwitchUtil.getGpsStatus(this) == true) {
LocationManager locMgr = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
String bestProvider = locMgr.getBestProvider(criteria, true);
if (bestProvider != null) {
tv_check_permission.setText("正常定位");
} else {
Log.d(TAG, "bestProvider is null");
tv_check_permission.setText("无法获取定位提供者");
}
} else {
tv_check_permission.setText("无法定位,GPS和数据连接均未开启");
}
}
判断是否能够正常拍照,如无法拍照则app在执行Camera的open方法时,(即打开摄像头时)会扔出异常“java.lang.RuntimeException: Fail to connect to camera service”。拍照功能的详细介绍参见《Android开发笔记(五十六)摄像头拍照》。 检查拍照权限的示例代码如下:
if (checkValid("android.permission.CAMERA") != true) {
return;
} else {
try {
Camera camera = Camera.open();
camera.release();
tv_check_permission.setText("正常拍照");
} catch (Exception e) {
e.printStackTrace();
tv_check_permission.setText("拍照失败:" + e.getMessage());
}
}
判断是否能够正常录音,如无法录音则app在执行MediaRecorder的setAudioSource方法时,(即打开麦克风时)会扔出异常“java.lang.RuntimeException: setAudioSource failed.”。录音功能的详细介绍参见《Android开发笔记(五十七)录像录音与播放》。 检查录音权限的示例代码如下:
if (checkValid("android.permission.RECORD_AUDIO") != true) {
return;
} else {
try {
MediaRecorder mRecorder = new MediaRecorder();
mRecorder.setAudioSource(AudioSource.MIC); // 如被关闭录音权限,则setAudioSource就会扔出异常
mRecorder.setAudioSamplingRate(10); // 设置音频的采样率,单位赫兹(Hz)
mRecorder.setAudioChannels(2); // 设置音频的声道数。1表示单声道,2表示双声道
mRecorder.setAudioEncodingBitRate(1000); // 设置音频每秒录制的字节数
mRecorder.setOutputFormat(OutputFormat.DEFAULT);
mRecorder.setAudioEncoder(AudioEncoder.AMR_NB);
mRecorder.setMaxDuration(3000);
mRecorder.setOutputFile(createRecordDir().getAbsolutePath());
mRecorder.prepare();
mRecorder.release();
tv_check_permission.setText("正常录音");
} catch (Exception e) {
e.printStackTrace();
tv_check_permission.setText("录音失败:" + e.getMessage());
}
}