Android 硬件传感器

  • 1. 传感器入门 

    自从苹果公司在2007年发布第一代iPhone以来,以前看似和手机挨不着边的传感器也逐渐成为手机硬件的重要组成部分。如果读者使用过iPhone、HTC Dream、HTC Magic、HTC Hero以及其他的Android手机,会发现通过将手机横向或纵向放置,屏幕会随着手机位置的不同而改变方向。这种功能就需要通过重力传感器来实现,除了重力传感器,还有很多其他类型的传感器被应用到手机中,例如磁阻传感器就是最重要的一种传感器。虽然手机可以通过GPS来判断方向,但在GPS信号不好或根本没有GPS信号的情况下,GPS就形同虚设。这时通过磁阻传感器就可以很容易判断方向(东、南、西、北)。有了磁阻传感器,也使罗盘(俗称指向针)的电子化成为可能。 在Android应用程序中使用传感器要依赖于android.hardware.SensorEventListener接口。通过该接口可以监听传感器的各种事件。SensorEventListener接口的代码如下:

    package android.hardware;
    public interface SensorEventListener 
    {
        public void onSensorChanged(SensorEvent event);
        public void onAccuracyChanged(Sensor sensor, int accuracy);    
    }

    在SensorEventListener接口中定义了两个方法:onSensorChanged和onAccuracyChanged。当传感器的值发生变化时,例如磁阻传感器的方向改变时会调用onSensorChanged方法。当传感器的精度变化时会调用onAccuracyChanged方法。onSensorChanged方法只有一个SensorEvent类型的参数event,其中SensorEvent类有一个values变量非常重要,该变量的类型是float[]。但该变量最多只有3个元素,而且根据传感器的不同,values变量中元素所代表的含义也不同。


    在解释values变量中元素的含义之前,先来介绍一下Android的坐标系统是如何定义X、Y、Z轴的。


    下面是values变量的元素在主要的传感器中所代表的含义。

    1.1 方向传感器


    在方向传感器中values变量的3个值都表示度数,它们的含义如下:

    values[0]:该值表示方位,也就是手机绕着Z轴旋转的角度。0表示北(North);90表示东(East);180表示南(South);270表示西(West)。如果values[0]的值正好是这4个值,并且手机是水平放置,表示手机的正前方就是这4个方向。可以利用这个特性来实现电子罗盘,实例76将详细介绍电子罗盘的实现过程。

    values[1]:该值表示倾斜度,或手机翘起的程度。当手机绕着X轴倾斜时该值发生变化。values[1]的取值范围是-180≤values[1]≤180。假设将手机屏幕朝上水平放在桌子上,这时如果桌子是完全水平的,values[1]的值应该是0(由于很少有桌子是绝对水平的,因此,该值很可能不为0,但一般都是-5和5之间的某个值)。这时从手机顶部开始抬起,直到将手机沿X轴旋转180度(屏幕向下水平放在桌面上)。在这个旋转过程中,values[1]会在0到-180之间变化,也就是说,从手机顶部抬起时,values[1]的值会逐渐变小,直到等于-180。如果从手机底部开始抬起,直到将手机沿X轴旋转180度,这时values[1]会在0到180之间变化。也就是values[1]的值会逐渐增大,直到等于180。可以利用values[1]和下面要介绍的values[2]来测量桌子等物体的倾斜度。

    values[2]:表示手机沿着Y轴的滚动角度。取值范围是-90≤values[2]≤90。假设将手机屏幕朝上水平放在桌面上,这时如果桌面是平的,values[2]的值应为0。将手机左侧逐渐抬起时,values[2]的值逐渐变小,直到手机垂直于桌面放置,这时values[2]的值是-90。将手机右侧逐渐抬起时,values[2]的值逐渐增大,直到手机垂直于桌面放置,这时values[2]的值是90。在垂直位置时继续向右或向左滚动,values[2]的值会继续在-90至90之间变化。

    1.2 加速传感器

        该传感器的values变量的3个元素值分别表示X、Y、Z轴的加速值。例如,水平放在桌面上的手机从左侧向右侧移动,values[0]为负值;从右向左移动,values[0]为正值。读者可以通过本节的例子来体会加速传感器中的值的变化。要想使用相应的传感器,仅实现SensorEventListener接口是不够的,还需要使用下面的代码来注册相应的传感器。

    //  获得传感器管理器
    SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE);
    
    //  注册方向传感器
    sm.registerListener(this, sm.getDefaultSensor(Sensor.TYPE_ORIENTATION),
    
    SensorManager.SENSOR_DELAY_FASTEST);

    如果想注册其他的传感器,可以改变getDefaultSensor方法的第1个参数值,例如,注册加速传感器可以使用Sensor.TYPE_ACCELEROMETER。在Sensor类中还定义了很多传感器常量,但要根据手机中实际的硬件配置来注册传感器。如果手机中没有相应的传感器硬件,就算注册了相应的传感器也不起任何作用。getDefaultSensor方法的第2个参数表示获得传感器数据的速度。SensorManager.SENSOR_DELAY_ FASTEST表示尽可能快地获得传感器数据。除了该值以外,还可以设置3个获得传感器数据的速度值,这些值如下:

    SensorManager.SENSOR_DELAY_NORMAL:默认的获得传感器数据的速度。
    SensorManager.SENSOR_DELAY_GAME:如果利用传感器开发游戏,建议使用该值。
    SensorManager.SENSOR_DELAY_UI:如果使用传感器更新UI中的数据,建议使用该值。

    1.3 重力感应器


    加速度传感器的类型常量是Sensor.TYPE_GRAVITY。重力传感器与加速度传感器使用同一套坐标系。values数组中三个元素分别表示了X、Y、Z轴的重力大小。Android SDK定义了一些常量,用于表示星系中行星、卫星和太阳表面的重力。下面就来温习一下天文知识,将来如果在地球以外用Android手机,也许会用得上。

    public static final float GRAVITY_SUN= 275.0f;
    public static final float GRAVITY_MERCURY= 3.70f;
    public static final float GRAVITY_VENUS= 8.87f;
    public static final float GRAVITY_EARTH= 9.80665f;
    public static final float GRAVITY_MOON= 1.6f;
    public static final float GRAVITY_MARS= 3.71f;
    public static final float GRAVITY_JUPITER= 23.12f;
    public static final float GRAVITY_SATURN= 8.96f;
    public static final float GRAVITY_URANUS= 8.69f;
    public static final float GRAVITY_NEPTUNE= 11.0f;
    public static final float GRAVITY_PLUTO= 0.6f;
    public static final float GRAVITY_DEATH_STAR_I= 0.000000353036145f;
    public static final float GRAVITY_THE_ISLAND= 4.815162342f;

    1.4 光线传感器


    光线传感器的类型常量是Sensor.TYPE_LIGHT。values数组只有第一个元素(values[0])有意义。表示光线的强度。最大的值是120000.0f。Android SDK将光线强度分为不同的等级,每一个等级的最大值由一个常量表示,这些常量都定义在SensorManager类中,代码如下:

    public static final float LIGHT_SUNLIGHT_MAX =120000.0f;
    public static final float LIGHT_SUNLIGHT=110000.0f;
    public static final float LIGHT_SHADE=20000.0f;
    public static final float LIGHT_OVERCAST= 10000.0f;
    public static final float LIGHT_SUNRISE= 400.0f;
    public static final float LIGHT_CLOUDY= 100.0f;
    public static final float LIGHT_FULLMOON= 0.25f;
    public static final float LIGHT_NO_MOON= 0.001f;

    上面的八个常量只是临界值。读者在实际使用光线传感器时要根据实际情况确定一个范围。例如,当太阳逐渐升起时,values[0] 的值很可能会超过LIGHT_SUNRISE ,当values[0] 的值逐渐增大时,就会逐渐越过LIGHT_OVERCAST ,而达到LIGHT_SHADE ,当然,如果天特别好的话,也可能会达到LIGHT_SUNLIGHT ,甚至更高。

    1.5 陀螺仪传感器

    陀螺仪传感器的类型常量是Sensor.TYPE_GYROSCOPE。values数组的三个元素表示的含义如下:

    当手机逆时针旋转时,角速度为正值,顺时针旋转时,角速度为负值。陀螺仪传感器经常被用来计算手机已转动的角度,代码如下:

    private static final float NS2S = 1.0f / 1000000000.0f;
    private float timestamp;
    public void onSensorChanged(SensorEvent event)
    {
        if (timestamp != 0) 
        {
        //  event.timesamp表示当前的时间,单位是纳秒(1百万分之一毫秒)
                  final float dT = (event.timestamp - timestamp) * NS2S;
                  angle[0] += event.values[0] * dT;
                  angle[1] += event.values[1] * dT;
                  angle[2] += event.values[2] * dT;
         }
         timestamp = event.timestamp;
    }

    上面代码中通过陀螺仪传感器相邻两次获得数据的时间差(dT )来分别计算在这段时间内手机延X 、 Y 、Z 轴旋转的角度,并将值分别累加到angle 数组的不同元素上。

    1.6 其他传感器

    其他传感器在前面几节介绍了加速度传感器、重力传感器、光线传感器、陀螺仪传感器以及方向传感器。除了这些传感器外,Android SDK还支持如下的几种传感器。关于这些传感器的使用方法以及与这些传感器相关的常量、方法,读者可以参阅官方文档。

    虽然AndroidSDK定义了十多种传感器,但并不是每一部手机都完全支持这些传感器。例如,Google Nexus S支持其中的9种传感器(不支持压力和温度传感器),而HTC G7只支持其中的5种传感器。如果使用了手机不支持的传感器,一般不会抛出异常,但也无法获得传感器传回的数据。读者在使用传感器时最好先判断当前的手机是否支持所使用的传感器。

    2. 测试手机中有哪些传感器

    我们可以通过如下三步使用传感器。
    (1)编写一个截获传感器事件的类。该类必须实现android.hardware.SensorEventListener接口。
    (2)获得传感器管理对象(SensorManager对象)。
    (3)使用SensorManager.registerListener方法注册指定的传感器。
    通过上面三步已经搭建了传感器应用程序的框架。而具体的工作需要在SensorEventListener接口的onSensorChanged和onAccuracyChanged方法中完成。SensorEventListener接口的定义如下:

    package android.hardware;
    public interfaceSensorEventListener 
    {
        // 传感器数据变化时调用
        public void onSensorChanged(SensorEventevent);
        // 传感器精确度变化时调用
        public void onAccuracyChanged(Sensorsensor, int accuracy);
    }

    SensorManager 对象通过getSystemService 方法获得,代码如下:

    SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);

    通常手机中包含了若干个传感器模块(如方向传感器、光线传感器等),因此,注册传感器需要指定传感器的类型,如下面的代码注册了光线传感器。

    sensorManager.registerListener(this,sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT),
    SensorManager.SENSOR_DELAY_FASTEST);

    registerListener 方法有三个参数。第1 个参数是实现SensorEventListener 接口的对象。第2 个参数用于指定传感器的类型。AndroidSDK 预先定义了表示各种传感器的常量,这些常量都被放在Sensor 类中。例如,上面代码中的Sensor.TYPE_LIGHT 。第3 个参数表示传感器获得数据的速度。该参数可设置的常量如下: 

    上面四种类型获得传感器数据的速度依次递减。从理论上说,获得传感器数据的速度越快,消耗的系统资源越大。因此建议读者根本实际情况选择适当的速度获得传感器的数据。 
    如果想停止获得传感器数据,可以使用unregisterSensor 方法注销传感器事件对象。unregisterSensor 方法的定义如下:

    public voidunregisterListener(SensorEventListener listener)
    public voidunregisterListener(SensorEventListener listener, Sensor sensor)

    unregisterSensor 方法有两个重载形式。第一个重载形式用于注销所有的传感器对象。第二个重载形式用于注销指定传感器的事件对象。其中Sensor 对象通过SensorManager.getDefaultSensor 方法获得。getDefaultSensor方法只有一个int 类型的参数,表示传感器的类型。如Sensor.TYPE_LIGHT 表示光线传感器。 

    注意:一个传感器对像可以处理多个传感器。也就是说,一个实现
     SensorEventListener 接口的类可以接收多个传感器传回的数据。为了区分不同的传感器,需要使用 Sensor.getType 方法来获得传感器的类型。getType 方法的将在本节的例子中详细介绍。 

    通过SensorManager.getSensorList 方法可以获得指定传感器的信息,也可以获得手机支持的所有传感器的信息,代码如下:

    // 获得光线传感器
    List<Sensor>sensors = sensorManager.getSensorList(Sensor.TYPE_LIGHT);
    // 获得手机支持的所有传感器
    List<Sensor>sensors = sensorManager.getSensorList(Sensor.TYPE_ALL);

    下面给出一个完整的例子来演示如何获得传感器传回的数据。本例从如下4 个传感器获得数据,同时输出了测试手机中支持的所有传感器名称。 

    本例需要在真机上运行。由于不同的手机可能支持的传感器不同(有的手机并不支持Android SDK 中定义的所有传感器),因此,如果运行程序后,无法显示某个传感器的数据,说明当前的手机并不支持这个传感器。笔者已使用Google Nexus S 测试了本例。如果读者使用的也是GoogleNexus S ,则会输出如图1 类似的信息。


    图1 获得传感器传回的数据


    本例的完整代码如下:

    package mobile.android. sensor;
    
    import java.util.List;
    import android.app.Activity;
    import android.hardware.Sensor;
    import android.hardware.SensorEvent;
    import android.hardware.SensorEventListener;
    import android.hardware.SensorManager;
    import android.os.Bundle;
    import android.widget.TextView;
    
    public class Main extends Activity implements SensorEventListener
    {
        private TextView tvAccelerometer;
        private TextView tvMagentic;
        private TextView tvLight;
        private TextView tvOrientation;
        private TextView tvSensors;
    
        @Override
        public void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            //  获得SensorManager对象
            SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    
            //  注册加速度传感器
            sensorManager.registerListener(this,
                    sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
                    SensorManager.SENSOR_DELAY_FASTEST);
    
            //  注册磁场传感器
            sensorManager.registerListener(this,
                    sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
                    SensorManager.SENSOR_DELAY_FASTEST);
    
            //  注册光线传感器
            sensorManager.registerListener(this,
                    sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT),
                    SensorManager.SENSOR_DELAY_FASTEST);
    
            //  注册方向传感器
            sensorManager.registerListener(this,
                    sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION),
                    SensorManager.SENSOR_DELAY_FASTEST);
    
            tvAccelerometer = (TextView) findViewById(R.id.tvAccelerometer);
            tvMagentic = (TextView) findViewById(R.id.tvMagentic);
            tvLight = (TextView) findViewById(R.id.tvLight);
            tvOrientation = (TextView) findViewById(R.id.tvOrientation);
            tvSensors = (TextView)findViewById(R.id.tvSensors);
            
            //  获得当前手机支持的所有传感器
            List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
            for(Sensor sensor:sensors)
            {
                //  输出当前传感器的名称
                tvSensors.append(sensor.getName() + "\n");
            }
        }
        @Override
        public void onSensorChanged(SensorEvent event)
        {
            //  通过getType方法获得当前传回数据的传感器类型
            switch (event.sensor.getType())
            {
                case Sensor.TYPE_ACCELEROMETER:            //  处理加速度传感器传回的数据
                    String accelerometer = "加速度\n" + "X:" + event.values[0] + "\n"
                            + "Y:" + event.values[1] + "\n" + "Z:" + event.values[2] + "\n";
                    tvAccelerometer.setText(accelerometer);
                    break;
                case Sensor.TYPE_LIGHT:                    //  处理光线传感器传回的数据
                    tvLight.setText("亮度:" + event.values[0]);
                    break;
                case Sensor.TYPE_MAGNETIC_FIELD:            //  处理磁场传感器传回的数据
                    String magentic = "磁场\n" + "X:" + event.values[0] + "\n" + "Y:"
                            + event.values[1] + "\n" + "Z:" + event.values[2] + "\n";
                    tvMagentic.setText(magentic);
                    break;
                case Sensor.TYPE_ORIENTATION:                //  处理方向传感器传回的数据
                    String orientation = "方向\n" + "X:" + event.values[0] + "\n"
                            + "Y:" + event.values[1] + "\n" + "Z:" + event.values[2] + "\n";
                    tvOrientation.setText(orientation);
                    break;
            }
        }
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy)
        {
        }
    }

    上面的代码中使用了event.values数组中的数据来获得传感器传回的数据。这个values数组非常重要,它的长度为3。但不一定每一个数组元素 都有意义。对于不同的传感器,每个数组元素的含义不同。在下面的部分将详细介绍不同传感器中values数组各个元素的含义。 

    注意:虽然使用Sensor.TYPE_ALL可以获得手机支持的所有传感器信息,但不能使用Sensor.TYPE_ALL注册所有的传感器,也就是getDefaultSensor方法的参数值必须是某个传感器的类型常量,而不能是Sensor.TYPE_ALL。

    3. 传感器应用

    3.1 电子罗盘

    电子罗盘又叫电子指南针。在实现本例之前,先看一下如图1所示的运行效果。

    图1 电子罗盘
    其中N、S、W和E分别表示北、南、西和东4个方向。


    本例只使用了onSensorChanged事件方法及values[0]。由于指南针图像上方是北,当手机前方是正北时(values[0]=0),图 像不需要旋转。但如果不是正北,就需要将图像按一定角度旋转。假设当前values[0]的值是60,说明方向在东北方向。也就是说,手机顶部由北向东旋 转。这时如果图像不旋转,N的方向正好和正北的夹角是60度,需要将图像逆时针(从东向北旋转)旋转60度,N才会指向正北方。因此,可以使用在 11.2.3节介绍的旋转补间动画来旋转指南针图像,代码如下:

    public void onSensorChanged(SensorEvent event)
    {
        if (event.sensor.getType() == Sensor.TYPE_ORIENTATION)
        {  
            float degree = event.values[0];    
            //  以指南针图像中心为轴逆时针旋转degree度
            RotateAnimation ra = new RotateAnimation(currentDegree, -degree,
                    Animation.RELATIVE_TO_SELF, 0.5f,
                    Animation.RELATIVE_TO_SELF, 0.5f);
            //  在200毫秒之内完成旋转动作
            ra.setDuration(200);
            //  开始旋转图像
            imageView.startAnimation(ra); 
            //  保存旋转后的度数,currentDegree是一个在类中定义的float类型变量
            currentDegree = -degree;
        }
    }

    上面的代码中使用了event.values数组中的数据来获得传感器传回的数据。这个values数组非常重要,它的长度为3。但不一定每一个数组元素 都有意义。对于不同的传感器,每个数组元素的含义不同。在下面的部分将详细介绍不同传感器中values数组各个元素的含义。 

    注意:虽然使用Sensor.TYPE_ALL可以获得手机支持的所有传感器信息,但不能使用Sensor.TYPE_ALL注册所有的传感器,也就是getDefaultSensor方法的参数值必须是某个传感器的类型常量,而不能是Sensor.TYPE_ALL。

    3.2  计步器

    还可以利用方向传感器做出更有趣的应用,例如利用values[1]或values[2]的变化实现一个计步器。由于人在走路时会上下振动,因此,可以通 过判断values[1]或values[2]中值的振荡变化进行计步。基本原理是在onSensorChanged方法中计算两次获得 values[1]值的差,并根据差值在一定范围之外开始计数,代码如下:

    public void onSensorChanged(SensorEvent event)
    {
        if (flag)
        {
            lastPoint = event.values[1];
            flag = false;
        }
        //  当两个values[1]值之差的绝对值大于8时认为走了一步
        if (Math.abs(event.values[1] - lastPoint) > 8)
        {
            //  保存最后一步时的values[1]的峰值
            lastPoint = event.values[1];
            //  将当前计数显示在TextView组件中
            textView.setText(String.valueOf(++count));
        }
    }

    本例设置3个按钮用于控制计步的状态,这3个按钮可以控制开始计步、重值(将计步数清0)和停止计步。这3个按钮的单击事件代码如下:

    public void onClick(View view)
    {
        String msg = "";
        switch (view.getId())
        {
            //  开始计步
            case R.id.btnStart:
                sm = (SensorManager) getSystemService(SENSOR_SERVICE);
                //  注册方向传感器
                sm.registerListener(this, sm
                        .getDefaultSensor(Sensor.TYPE_ORIENTATION),
                        SensorManager.SENSOR_DELAY_FASTEST);
                msg = "已经开始计步器.";
                break;
            //  重置计步器
            case R.id.btnReset:
                count = 0;
                msg = "已经重置计步器.";
                break;
            //  停止计步
            case R.id.btnStop:
                //  注销方向传感器
                sm.unregisterListener(this);
                count = 0;
                msg = "已经停止计步器.";
                break;
        }
        textView.setText(String.valueOf(count));
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }

    运行本例后,单击【开始】按钮,将手机放在兜里,再走两步看看,计步的效果如图1所示。 
     
    图1 计步器


    3.3  增强版计步器

    程序运行界面如图4所示, 其中主窗体包括两个部分,上半部分用于显示最近7 天每天走过的步数, 以及今日走过的步数, 通过一个自定义的View 来实现; 下半部分用于摆放3个程序控制按钮。

    图1 计步器主界面


    在程序运行时自动连接数据库, 读取历史数据并将其可视化地显示到屏幕上。程序运行后会自动启动一个Service 组件, 用于检测手机的加速度状态, 当用户携带手机步行时, 传感器会捕获到这个动作并更新记录已走步数的计数器。如果此时程序正在前台显示, 那么在屏幕中除了刷新走过的步数之外, 还将播放一小段走路的动画。点击“删除数据” 按钮会删除掉数据库中存储的历史数据。点击“停止服务” 按钮会停止后台Service 的执行, 同时状态栏将不再显示计步器的Notification。点击“转为后台” 按钮将关闭程序界面, 但保留后台执行的Service。当用户按住手机屏幕的状态栏往下拖拉时, 会在展开的状态栏中看到本程序的Notification, 如图2 所示。

    图2  手机状态栏中的计步器


    本应用程序使用了Android 平台内置的SQLite 嵌入式数据库, 数据库中包含一张名为“step_table” 的表, 用来存放历史的已走步数信息, 表1 列出了step_table 表各个字段的情况。

    表1  step_table表的结构


    开发应用正式功能之前首先要开发对数据库访问的辅助类MySQLiteHelper, 其主要的功能为连接并打开SQLite 数据库,代码如下:

    package wyf.wpf; //声明所在包
    import android.content.Context; //引入相关类
    …//省略部分引入相关类的代码
    import android.database.sqlite.SQLiteOpenHelper;
    public class MySQLiteHelper extends SQLiteOpenHelper {
        public static final String TABLE_NAME = "step_table";
        public static final String ID = "id";
        public static final String STEP = "step";
        public static final String UPDATE_DATE = "up_date";
        public MySQLiteHelper(Context context, String name, CursorFactory factory,int version) {//构造器
            super(context, name, factory, version);
        }
        public void onCreate(SQLiteDatabase db) {
            db.execSQL ("create table if not exists " + TABLE_NAME + "(" //创建数据库表
                +ID+" integer primary key,"
                +STEP + " integer)");
        }
        public void onUpgrade(SQLiteDatabase db, int oldVersion , int newVersion) {}//对onUpgrade 方法的重写
    }

    在上述代码中创建了一个继承自SQLiteOpenHelper 类的子类, 并重写了其中的onCreate 和onUpgrade 方法。onCreate 方法将在数据库第一次被创建时调用, 本案例在该方法中执行了创建表的代码。onUpgrade 方法在数据库版本发生变化时调用。 

    完成了数据库辅助类的开发后就可以开发WalkingActivity类了, 其是应用程序的用户界面, 主要功能是按照XML 布局文件的内容显示界面并与用户进行交互, 代码如下:

    package wyf.wpf; //声明所在包
    
    import java.util.ArrayList; //引入相关类
    import android.app.Activity;
    …//省略部分引入相关类的代码 
    import android.view.View.OnClickListener;
    import android.widget.Button;
    
    public class WalkingActivity extends Activity implements OnClickListener{
        WalkingView wv; //WalkingView 对象引用
        //数据库名称
        public static final String DB_NAME = "step.db";
        MySQLiteHelper mh; //声明数据库辅助类
        SQLiteDatabase db; //数据库对象
        Button btnToBackstage; //转入后台按钮
        Button btnStopService; //停止服务按钮
        Button btnDeleteData; //删除数据按钮
        StepUpdateReceiver receiver;
    
        //定义一个继承自BroadcastReceiver 的内部类 StepUpdateReceiver 来接受传感器的信息
        public class StepUpdateReceiver extends BroadcastReceiver{
    
            public void onReceive(Context context, Intent intent) {
                Bundle bundle = intent.getExtras();//获得Bundle
                int steps = bundle.getInt("step");//读取步数
                wv.stepsToday = steps;
                wv.isMoving = true;
                wv.postInvalidate(); //刷新WalkingView
            }
        }
    
        //重写onCreate 方法,在Activity 被创建时调用
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);//设置当前屏幕
            wv = (WalkingView)
            findViewById(R.id.walkingView);
            btnToBackstage = (Button)
            findViewById(R.id.btnDispose);
            btnToBackstage.setOnClickListener(this);
            btnStopService = (Button)findViewById(R.id.btnStop);
            btnStopService.setOnClickListener(this);
            btnDeleteData = (Button)findViewById(R.id.btnDeleteData);
            btnDeleteData.setOnClickListener(this);
            //注册Receiver
            receiver = new StepUpdateReceiver();
            IntentFilter filter = new IntentFilter();
            filter.addAction("wyf.wpf.WalkingActivity");
            registerReceiver(receiver, filter);
            //启动注册了传感器监听的Service
            Intent i = new Intent(this,WalkingService.class);
            startService(i);
            mh=new MySQLiteHelper(this,DB_NAME,null,1);
            requireData(); //向Service 请求今日走过步数
        }
    
        //重写onDestroy 方法
        protected void onDestroy() {
            unregisterReceiver(receiver); //注销Receiver
            super.onDestroy();
        }
    
        //重写OnClickListener 接口的onClick 方法
        public void onClick(View view) {}
    
        //方法:向Service 请求今日走过的步数
        public void requireData(){}
    }

    上述代码为Walking Activity 类的代码框架, 由WalkingActivity 实现了OnClickListener 接口, 所以需要对接中的onClick 方法进行重写, 重写的onClick 方法代码如下:

    public void onClick(View view) {
        if(view == btnStopService){
            //停止后台服务
            Intent intent = new Intent();
            intent.setAction("wyf.wpf.WalkingService");
            intent.putExtra("cmd",
            WalkingService.CMD_STOP);
            sendBroadcast(intent);
        } else if (view == btnToBackstage) {
            finish();//转到后台
        } else if (view == btnDeleteData) {
            //查看历史数据
            SQLiteDatabase db = (SQLiteDatabase)
            openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null);
            db.delete(MySQLiteHelper.TABLE_NAME, null, null);
            db.close();
            wv.stepsInWeek = wv.getSQLData("7");
            wv.postInvalidate();
        }
    }

    在WalkingActivity 的onCreate 方法的最后调用了require-Data 方法向Service 发送Intent 请求今日走过的步数, 该方法的代码如下:

    public void requireData() {
        Intent intent = new Intent(); //创建Intent
        intent.setAction("wyf.wpf.WalkingService");
        intent.putExtra("cmd", WalkingService.CMD_UPDATAE);
        sendBroadcast(intent); //发出消息广播
    }

    完成了WalkingActivity 类的开发后就需要开发用于显示计步器的历史数据及绘制今日走过的步数及走步时动画的自定义View———WalkingView 了, 其代码框架如下:

    package wyf.wpf;
    import java.util.ArrayList;
    …//省略部分引入相关类的代码
    import android.view.View;
    
    public class WalkingView extends View {
        ArrayList<String> stepsInWeek=null;//存历史数据
        int stepsToday=0; //记录今天走的步数
        int gapY = 8; //屏幕最上面留出的空隙
        int distY = 10; //每一条的间距
        int cellHeight = 30; //每一条的高度
        float STEP_MAX = 1000.0f; //每天最大的步数
        int maxStepWidth = 280; //最大步数在屏幕中宽度
        Bitmap [] sprite; //运动小人的图片数组
        Bitmap [] digit; //数字图片数组
        Bitmap back_cell; //颜色渐变条
        boolean isMoving = false;
        int frameIndex; //记录运动小人的帧索引
        MySQLiteHelper mh; //操作数据库的辅助类
        SQLiteDatabase db; //数据库操作对象
        public WalkingView(Context context, AttributeSet attrs) {
            super(context, attrs);
            sprite = new Bitmap[5];
            digit = new Bitmap[10];
            //初始化图片
            Resources res = getResources();
            sprite[0] = BitmapFactory.decodeResource(res, R.drawable.act_1);
            …//省略部分Bitmap 的创建代码
            back_cell = BitmapFactory.decodeResource(res, R.drawable.back_cell);
            //获取数据库中最近7 天内的数据
            mh = new MySQLiteHelper(context, WalkingActivity.DB_NAME, null,1);
            stepsInWeek = getSQLData("7");
        }
    
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            drawPrevious(canvas);//画以前走过的步数
            drawToday(canvas); //画今天走过的步数
        }
    
        //画今天走的步数
        private void drawToday(Canvas canvas) {}
        //画之前走过的步数
        private void drawPrevious(Canvas canvas) {}
        //从数据库中获取历史数据
        public ArrayList<String> getSQLData(String limit){}

    上述为WalkingView 类的代码框架, 在WalkingView 类的构造器中将需要用到的图片资源初始化的同时, 调用getSQLData方法获取数据库中的历史数据。WalkingView 类重写了onDraw 方法, 该方法需要调用drawPrevious 和drawToday 方法分别对历史数据和今日走步情况进行绘制。这3 个方法以及在drawToday 方法中调用到的drawDigits 方法的详细代码如下:

    //画今天走的步数
    private void drawToday(Canvas canvas) {
        Paint paint = new Paint();
        paint.setColor(Color.CYAN);
        float strokewidth = paint.getStrokeWidth();
        Style s = paint.getStyle();
        paint.setStyle(Style.STROKE);
        paint.setStrokeWidth(2.0f);
        canvas.drawLine(0, 300, 320, 300, paint);
        paint.setStyle(s);
        paint.setStrokeWidth(strokewidth);//恢复画笔
        //把当前步数换算为在屏幕上绘制的条宽度
        int width = (int)(stepsToday/STEP_MAX*280);
        canvas.drawBitmap(back_cell, 0, 320, paint);
        paint.setColor(Color.BLACK);
        canvas.drawRect(width, 320, 320, 320+cellHeight, paint);
        //画出遮罩层
        if(isMoving) { //如果在运动,就切换帧序列
            canvas.drawBitmap(sprite[(++frameIndex)%5], width+20, 320,paint);
            isMoving = false;
        } else { //如果没在走步,就绘制静止的那张图片
            canvas.drawBitmap(sprite[4], width+20, 320, paint);
        }
        drawDigit(canvas,width); //绘制数字
    }
    
    //画之前走过的步数
    private void drawPrevious(Canvas canvas) {
        Paint paint = new Paint();
        for(int i=0; i < stepsInWeek.size(); i++) {
            String os = stepsInWeek.get(i);
            int s = Integer.valueOf(os).intValue();
            int width = (int) (s/STEP_MAX * maxStep-Width); //求出指定的步数在统计条中占得宽度
            int tempY = (cellHeight+distY)*i;
            canvas.drawBitmap(back_cell, 0, (cellHeight+distY)*i, paint); //画出渐变条
            paint.setColor(Color.BLACK);
            canvas.drawRect(width, tempY, 320, tempY+cell-Height, paint);
            paint.setTextAlign(Align.LEFT);
            paint.setColor(Color.CYAN);
            paint.setAntiAlias(true);
            canvas.drawText("走了"+stepsInWeek.get(i)+"步", width, tempY+cellHeight/2, paint);
        }
    }
    
    //从数据库中获取历史数据
    public ArrayList<String> getSQLData(String limit) {
        //获得SQLiteDatabase 对象
        db = mh.getReadableDatabase();
        String [] cols = {
            MySQLiteHelper.ID,MySQLiteHelper.STEP
        };
    
        Cursor c = db.query (MySQLiteHelper.TABLE_NAME, cols, null, null, null, null, MySQLiteHelper.ID+" DESC",limit);
        ArrayList<String> al = new ArrayList<String>();
        for (c.moveToFirst(); !(c.isAfterLast()); c.moveToNext()) {
            al.add(c.getString(1));
        }
        c.close();
        db.close();
        return al;
    }
    
    //将数字通过数字图片绘制到屏幕上
    public void drawDigit(Canvas canvas,int width) {
        String sStep = ""+stepsToday;
        int l = sStep.length();
        for (int i=0; i<l; i++) {
            int index = sStep.charAt(i) - '0';
            canvas.drawBitmap(digit[index], width+20+40+32*i, 320, null);//绘制数字图片
        }
    }

    完成了数据库辅助类及界面部分的开发后就可以开发后台的服务类———WalkingService 了。WalkingService 继承自Service类, 其主要实现的功能包括如下几个方面: 
    (1) 注册或注销传感器监听器。 
    (2) 在手机屏幕状态栏显示Notification。 
    (3) 定时将今日走过的步数写入数据库。 
    (4) 与WalkingActivity 进行通信。 
    下面将围绕这4 个功能对WalkingService 类的代码逐一进行介绍, 其类框架与成员声明的代码如下:

    package wyf.wpf;
    import java.util.Calendar;
    import android.app.Notification;
    …//省略部分引入相关类的代码
    import android.os.Message;
    import org.openintents.sensorsimulator.hardware.*;
    
    public class WalkingService extends Service {
        WalkingView wv;
        //SensorManager mySensorManager;
        SensorManagerSimulator mySensorManager;
        WalkingListener wl;
        int steps=0;
        boolean isActivityOn = false; //Activity 是否运行
        boolean isServiceOn = false;
        NotificationManager nm;//声明NotificationManager
        long timeInterval = 24*60*60*1000;
        //Handler 延迟发
        //送消息的时延
        final static int CMD_STOP = 0;
        final static int CMD_UPDATAE = 1;
        CommandReceiver receiver; //声明BroadcastReceiver
        Handler myHandler = new Handler() {//定时上传数据
            public void handleMessage(Message msg) {
            uploadData();
            super.handleMessage(msg);
        }
    };

    上述代码声明和创建了WalkingService 的成员变量。需要注意的是由于本案例将使用SensorSimulator 进行测试, 所以需要对正常代码进行修改。首先是成员变量mySensorManager 的声明, 代码中注释掉的部分为正常代码, 未被注释的是为了使用SensorSimulator 工具中相关类声明的引用。完成了类框架与成员声明代码的开发后就可以开发此Service的初始化方法onCreate 及其他相关功能方法了, 代码如下:

    public void onCreate() {
        super.onCreate();
        wl = new WalkingListener(this); //创建监听器类
        //初始化传感器
        // mySensorManager = (SensorManager)
        // getSystemService(SENSOR_SERVICE);
        mySensorManager = SensorManagerSimulator.getSystemService(this, SENSOR_SERVICE);
        mySensorManager.connectSimulator();
        //注册监听器
        mySensorManager.registerListener(wl, SensorManager.SENSOR_ACCELEROMETER, SensorManager.SENSOR_DELAY_UI);
        nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
        Calendar c = Calendar.getInstance();
        long militime = c.getTimeInMillis();
        //将Calendar 设置为第二天0 时
        c.set(Calendar.DAY_OF_MONTH, c.get(Calendar.DAY_OF_MONTH)+1);
        c.set(Calendar.HOUR_OF_DAY, 0);
        c.set(Calendar.MINUTE, 0);
        c.set(Calendar.SECOND, 0);
        long nextDay = c.getTimeInMillis();
        timeInterval = nextDay - militime;
    }
    
    public void onStart(Intent intent, int startId) {
        super.onStart(intent, startId);
        isServiceOn = true;
        showNotification();//添加Notification
        receiver = new CommandReceiver();
        IntentFilter filter1 = new IntentFilter();
        filter1.addAction("wyf.wpf.WalkingService");
        registerReceiver(receiver, filter1);
        //设定Message 并延迟到本日结束发送
        if (isServiceOn) {
            Message msg = myHandler.obtainMessage();
            myHandler.sendMessageDelayed(msg, timeInterval);
        }
    }
    
    //重写onDestroy 方法
    public void onDestroy() {
        mySensorManager.unregisterListener(wl);
        wl = null;
        mySensorManager = null;
        nm.cancel(0);
        unregisterReceiver(receiver);
        super.onDestroy();
    }
    
    //重写onBind 方法
    public IBinder onBind(Intent arg0) {
        return null;
    }
    
    //方法:显示Notification
    private void showNotification() {}
    
    //方法:向数据库中插入今日走过的步数
    public void uploadData(){}
    
    //开发继承自BroadcastReceiver 的子类接收广播消息
    class CommandReceiver extends BroadcastReceiver{}

    上述代码包括了WalkingService 的几个成员方法, 可以看到在onCreate 方法中, 同样为了使用SensorSimulator 工具测试案例而注释掉了正常代码并将其替换为使用SensorSimulator 的相关代码。同时在onCreate 方法中, 还计算了从Service 被启动时刻到这一天的结束之间的时间间隔, 并将该时间间隔作为Handler对象发送消息的延迟, 这样在本天过完之后, 会及时地向数据库中插入数据。在Service 的onStart 方法中调用了showNotification 方法,该方法将会在手机的状态栏(显示信号强度、电池等状态的区域) 显示本程序的Notification, 展开状态栏并点击Notification 后会启动WalkingActivity。showNotification 方法的代码如下:

    //方法:显示Notification
    private void showNotification() {
        Intent intent = new Intent(this,WalkingActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        Notification myNotification = new Notification();
        myNotification.icon = R.drawable.icon;
        myNotification.defaults = otification.DEFAULT_ALL;
        myNotification.setLatestEventInfo(this, "计步器运行中", "点击查看", pi);
        nm.notify(0,myNotification);
    }

    本程序使用Handler 来定时发送消息, 收到消息后会调用uploadData 方法将今日走过的步数插入到数据库, 该方法的代码如下:

    //方法:向数据库中插入今日走过的步数
    public void uploadData() {
        MySQLiteHelper mh = new MySQLiteHelper(this, WalkingActivity.DB_NAME, null, 1);
        SQLiteDatabase db = mh.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(MySQLiteHelper.STEP, this.steps);
        db.insert(MySQLiteHelper.TABLE_NAME, MySQLiteHelper.ID, values);
        Cursor c = db.query(MySQLiteHelper.TABLE_NAME, null, null, null, null, null, null);
        c.close();
        db.close(); //关闭数据库
        mh.close();
        if (isServiceOn) {//设置24 小时后再发同样的消息
            Message msg = myHandler.obtainMessage();
            myHandler.sendMessageDelayed(msg, 24*60*60*1000);
        }
    }

    WalkingService 和WalingActivity 进行通信是通过注册CommandReceiver 组件来实现的, CommandReceiver 继承自BroadcastReceiver, 负责接收WalingActivity 发来的Intent。 
    CommandReceiver 类的代码如下:

    //开发继承自BroadcastReceiver 的子类接收广播消息
    class CommandReceiver extends BroadcastReceiver {
        public void onReceive(Context context, Intent intent) {
            int cmd = intent.getIntExtra("cmd", -1);
            switch(cmd) {
            case WalkingService.CMD_STOP://停止服务
                stopSelf();
                break;
            case WalkingService.CMD_UPDATAE: //传数据
                isActivityOn = true;
                Intent i = new Intent();
                i.setAction("wyf.wpf.WalkingActivity");
                i.putExtra("step", steps);
                sendBroadcast(i);
                break;
            }
        }
    }

    WalkingService 和WalingActivity 在进行通信时, 被广播的Intent 对象中的action 必 须和另一方的IntentFilter 设置的action相同, 并且为了保证action 的惟一性, 一般以应 用程序包名后跟一个字符串来定义action。另外在开发了WalkingService 的代码之后, 还需要在AndroidManifest.xml 文件中声明该Service, 否则该Service 对Android系统是不可见的。 

    完成了前面的大部分工作后, 就可以开发传感器监听接口的实现类——— WalkingListener 了。WalkingListener 实现了SensorListener接口, 其主要的功能是监听加速度传感器的变化并进行相应的处理, 代码如下:

    package wyf.wpf;
    import android.content.Intent; //引入相关包
    import android.hardware.SensorListener;
    import android.hardware.SensorManager;
    
    public class WalkingListener implements SensorListener {
        WalkingService father; // WalkingService 引用
        float [] preCoordinate;
        double currentTime=0,lastTime=0; //记录时间
        float WALKING_THRESHOLD = 20;
        public WalkingListener(WalkingService father) {
            this.father = father;
        }
    
        public void onAccuracyChanged(int arg0, int arg1) {}
        //传感器发生变化后调用该方法
        public void onSensorChanged(int sensor, float[] values) {
            if(sensor == SensorManager.SENSOR_ACCELEROMETER) {
                analyseData(values);//调用方法分析数据
            }
        }
    
        //方法:分析参数进行计算
        public void analyseData(float[] values) {
            //获取当前时间
            currentTime=System.currentTimeMillis();
            //每隔200MS 取加速度力和前一个进行比较
            if (currentTime - lastTime > 200) {
                if (preCoordinate == null) {//还未存过数据
                    preCoordinate = new float[3];
                    for (int i=0; i<3; i++) {
                        preCoordinate = values;
                    }
                 } else { //记录了原始坐标的话,就进行比较
                     int angle = calculateAngle(values, preCoordinate);
                     if(angle >= WALKING_THRESHOLD) {
                         father.steps++; //步数增加
                         updateData(); //更新步数
                     }
                     for(int i=0; i<3; i++) {
                         preCoordinate = values;
                     }
                  }
                  lastTime = currentTime;//重新计时
              }
        }
        //方法:计算加速度矢量角度的方法
        public int calculateAngle(float[] newPoints,float[] oldPoints){}
    
        //方法:向Activity 更新步数
        public void updateData(){}
    }

    WalkingListener 类的代码中, 主要是对SensorListener 接口中的onSensorChanged 方法进行了重写, 在该方法中将读取到的传感器采样值传给analyseData 方法进行分析。在analyseData 方法中,调用了calculateAngle 方法来计算固定的时间间隔间手机加速度向量方向的夹角。calculateAngle方法的代码如下:

    //方法:计算两个加速度矢量夹角的方法
    public int calculateAngle(float[] newPoints, float[] oldPoints) {
        int angle=0;
        float vectorProduct=0; //向量积
        float newMold=0; //新向量的模
        float oldMold=0; //旧向量的模
        for (int i=0; i<3; i++) {
            vectorProduct += newPoints*oldPoints;
            newMold += newPoints*newPoints;
            oldMold += oldPoints*oldPoints;
        }
        newMold = (float)Math.sqrt(newMold);
        oldMold = (float)Math.sqrt(oldMold);
        //计算夹角的余弦
        float cosineAngle=(float)(vectorProduct/(newMold*oldMold));
        //通过余弦值求角度
        float fangle = (float)Math.toDegrees(Math.acos(cosineAngle));
        angle = (int)fangle;
        return angle; //返回向量的夹角
    }

    如果calculateAngle 方法返回的加速度向量角度变化超过了程序中设定的阈值, 应用程序将WalkingService 中已走步数计数器加1, 并调用updateData 方法将更新的步数传递给WalkingActivity 显示到界面, 该方法代码如下:

    public void updateData(){
        Intent intent = new Intent(); //创建Intent 对象
        intent.setAction("wyf.wpf.WalkingActivity");
        intent.putExtra("step", father.steps);//添加步数
        father.sendBroadcast(intent); //发出广播
    }

    完成了应用程序代码的开发之后, 就可以将应用程序打包安装调试了。在Eclipse 中构建本项目, 完成构建后本应用程序项目文件夹下bin 目录中的JBQ.apk 即为本计步器应用程序的发布apk 包。将此apk 包安装到手机模拟器, 然后启动SensorSimulator桌面端, 如图3 所示。 
     
    图3  测试传感器


    运行SensorSimulator 桌面端之后, 还需要在模拟器上安装SensorSimulator 的客户端, 根据桌面端显示的IP 地址和端口号进行响应的配置。配置好SensorSimulator 之后, 就可以运行已经安装过的计步器程序。在SensorSimulator 的桌面端可以模拟手机的动作变化从而达到调试传感器应用程序的目的。要特别注意的是, 在调试完成真正发布应用程序前, 需要将WalkingService 类中使用SensorSimulator 的代码注释掉, 将真正使用物理传感器的代码去掉注释。最后再次构建项目, 这样得到的apk 包就是最终真正的发布版了。


    通过开发计步器应用程序, 读者应该对Android 平台下开发传感器应用的流程有了一定的了解。传感器的特性和Android平台的开放性结合在一起, 使得在移动手机终端上开发各种新奇有趣的传感器应用成为可能, 同时也为开发人员开辟一个新的应用领域。可以预见, 在不久的将来, Android 嵌入式平台下的传感器应用必将大放光彩。

    4 在模拟器上模拟重力感应

    众所周知,Android 系统支持重力感应,通过这种技术,可以利用手机的移动、翻转来实现更为有趣的程序。但遗憾的是,在Android 模拟器上是无法进行重力感应测试的。既然Android 系统支持重力感应,但又在模拟器上无法测试,该怎么办呢?别着急,天无绝人之路,有一些第三方的工具可以帮助我们完成这个工作,本节将介绍一种在模拟器上模拟重力感应的工具(sensorsimulator )。这个工具分为服务端和客户端两部分。服务端是一个在PC 上运行的Java Swing GUI 程序,客户端是一个手机程序(apk 文件),在运行时需要通过客户端程序连接到服务端程序上才可以在模拟器上模拟重力感应。 

    读者可以从下面的地址下载这个工具: 
    http://code.google.com/p/openintents/downloads/list 
    进入下载页面后,下载如图1 所示的黑框中的zip 文件。 

     
    图1 sensorsimulator 下载页面 

    将zip 文件解压后,运行bin 目录中的sensorsimulator.jar 文件,会显示如图2 所示的界面。界面的左上角是一个模拟手机位置的三维图形,右上角可以通过滑杆来模拟手机的翻转、移动等操作。 

     
    图2 sensorsimulator 主界面 

    下面来安装客户端程序,先启动Android 模拟器,然后使用下面的命令安装bin 目录中的SensorSimulatorSettings.apk 文件。 

    adb install SensorSimulatorSettings.apk 

    如果安装成功,会在模拟器中看到如图3 所示黑框中的图标。运行这个程序,会进入如图4 所示的界面。在IP 地址中输入如图3 所示黑框中的IP (注意,每次启动服务端程序时这个IP 可能不一样,应以每次启动服务端程序时的IP 为准)。最后进入【Testing 】页,单击【Connect 】按钮,如果连接成功,会显示如图5 所示的效果。 

                                                       
    图3 安装客户端设置软件                                         

     
    图4 进行客户端设置 

    下面来测试一下SensorSimulator 自带的一个demo ,在这个demo 中输出了通过模拟重力感应获得的数据。 
    这个demo 就在samples 目录中,该目录有一个SensorDemo 子目录,是一个Eclipse 工程目录。读者可以直接使用Eclipse 导入这个目录,并运行程序,如果显示的结果如图5 所示,说明成功使用SensorSimulator 在Android模拟器上模拟了重力感应。 

                                                
    图5 测试连接状态                                               



     图6 测试重力感应demo 

    5. 手机翻转静音


    与手机来电一样,手机翻转状态(重力感应)也由系统服务提供。重力感应服务(android.hardware.SensorManager 对象)可以通过如下代码获得: 

    SensorManager sensorManager =(SensorManager)getSystemService(Context.SENSOR_SERVICE);

    本例需要在模拟器上模拟重力感应,因此,在本例中使用SensorSimulator 中的一个类(SensorManagerSimulator )来获得重力感应服务,这个类封装了SensorManager 对象,并负责与服务端进行通信,监听重力感应事件也需要一个监听器,该监听器需要实现SensorListener 接口,并通过该接口的onSensorChanged 事件方法获得重力感应数据。本例完整的代码如下: 

    package net.blogjava.mobile;
    
    import org.openintents.sensorsimulator.hardware.SensorManagerSimulator;
    import android.app.Activity;
    import android.content.Context;
    import android.hardware.SensorListener;
    import android.hardware.SensorManager;
    import android.media.AudioManager;
    import android.os.Bundle;
    import android.widget.TextView;
    
    public class Main extends Activity implements SensorListener {
        private TextView tvSensorState;
        private SensorManagerSimulator sensorManager;
        @Override
        public void onAccuracyChanged(int sensor, int accuracy) {
        }
        @Override
        public void onSensorChanged(int sensor, float[] values) {
            switch (sensor) {
                case SensorManager.SENSOR_ORIENTATION:
                    //  获得声音服务
                    AudioManager audioManager = (AudioManager) 
                                         getSystemService(Context.AUDIO_SERVICE);
                    //  在这里规定翻转角度小于-120度时静音,values[2]表示翻转角度,也可以设置其他角度
                    if (values[2] < -120) {
                        audioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT);
                    } else {
                        audioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL);
                    }
                    tvSensorState.setText("角度:" + String.valueOf(values[2]));
                    break;
            }
        }
        @Override
        protected void onResume() {
            //  注册重力感应监听事件
            sensorManager.registerListener(this, SensorManager.SENSOR_ORIENTATION);
            super.onResume();
        }
        @Override
        protected void onStop() {
            //  取消对重力感应的监听 
            sensorManager.unregisterListener(this);
            super.onStop();
        }
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            //  通过SensorManagerSimulator对象获得重力感应服务
            sensorManager = (SensorManagerSimulator) SensorManagerSimulator
                    .getSystemService(this, Context.SENSOR_SERVICE);
            //  连接到服务端程序(必须执行下面的代码)
            sensorManager.connectSimulator();
        }
    }

    在上面的代码中使用了一个SensorManagerSimulator 类,该类在SensorSimulator 工具包带的sensorsimulator-lib.jar 文件中,可以在lib 目录中找到这个jar 文件。在使用SensorManagerSimulator类之前,必须在相应的Eclipse 工程中引用这个jar 文件。 

    现在运行本例,并通过服务端主界面右侧的【Roll 】滑动杆移动到指定的角度,例如,-74.0 和-142.0 ,这时设置的角度会显示在屏幕上,如图1 和图2 所示。 

     
    图1 翻转角度大于-120 度


    图2 翻转角度小于-120 度

    读者可以在如图1 和图2 所示的翻转状态下拨入电话,会发现翻转角度在-74.0 度时来电仍然会响铃,而翻转角度在-142.0 度时就不再响铃了。 

    发布于 2年前, 阅读(282) | 评论(0) | 投票(1) | 收藏(1) 阅读全文...

    • 加速度传感器(Sensor.TYPE_ACCELEROMETER )

    • 磁场传感器(Sensor.TYPE_MAGNETIC_FIELD )

    • 光线传感器(Sensor.TYPE_LIGHT )

    • 方向传感器(TYPE_ORIENTATION )

    • SENSOR_DELAY_FASTEST :以最快的速度获得传感器数据。

    • SENSOR_DELAY_GAME :适合于在游戏中获得传感器数据。

    • SENSOR_DELAY_UI :适合于在UI 控件中获得传感器数据。

    • SENSOR_DELAY_NORMAL :以一般的速度获得传感器的数据。

    • 近程传感器(Sensor.TYPE_PROXIMITY)

    • 线性加速度传感器(Sensor.TYPE_LINEAR_ACCELERATION)

    • 旋转向量传感器(Sensor.TYPE_ROTATION_VECTOR)

    • 磁场传感器(Sensor.TYPE_MAGNETIC_FIELD)

    • 压力传感器(Sensor.TYPE_PRESSURE)

    • 温度传感器(Sensor.TYPE_TEMPERATURE)

    • values[0]:延X轴旋转的角速度。

    • values[1]:延Y轴旋转的角速度。

    • values[2]:延Z轴旋转的角速度。

    • X轴的方向是沿着屏幕的水平方向从左向右。如果手机不是正方形的话,较短的边需要水平放置,较长的边需要垂直放置。

    • Y轴的方向是从屏幕的左下角开始沿着屏幕的垂直方向指向屏幕的顶端。

    • 将手机平放在桌子上,Z轴的方向是从手机里指向天空。

  • 电子罗盘的工作原理及校准

    分类: 转贴移动开发文章

    112013-10

    电子罗盘是一种重要的导航工具,能实时提供移动物体的航向和姿态。随着半导体工艺的进步和手机操作系统的发展,集成了越来越多传感器智能手机变得功能强大,很多手机上都实现了电子罗盘的功能。而基于电子罗盘的应用(如Android的Skymap)在各个软件平台上也流行起来。

      要实现电子罗盘功能,需要一个检测磁场的三轴磁 力传感器和一个三轴加 速度传感器。随着微机械工艺的成熟,意法半导体推出将三轴磁力计和三轴加速计集成在一个封装里的二合一传感器模块LSM303DLH,方便用户在短时间内设计出成本低、性能高的电子罗盘。本文以LSM303DLH为例讨论该器件的工作原理、技术参数和电子罗盘的实现方法。 
       1.    地磁场和航向角的背景知识 
      如图1所示,地球的磁场象一个条形磁体一样由磁南极指向磁北极。在磁极点处磁场和当地的水平面垂直,在赤道磁场和当地的水平面平行,所以在北半球磁场 方向倾斜指向地面。用来衡量磁感应强度大小的单位是Tesla或者Gauss(1Tesla=10000Gauss)。随着地理位置的不同,通常地磁场的 强度是0.4-0.6 Gauss。需要注意的是,磁北极和地理上的北极并不重合,通常他们之间有11度左右的夹角。 

    图1 地磁场分布图

      地磁场是一个矢量,对于一个固定的地点来说,这个矢量可以被分解为两个与当地水平面平行的分量和一个与当地水平面垂直的分量。如果保持电子罗盘和当地的水平面平行,那么罗盘中磁力计的三个轴就和这三个分量对应起来,如图2所示。 

    图2 地磁场矢量分解示意图

      实际上对水平方向的两个分量来说,他们的矢量和总是指向磁北的。罗 盘中的航向角(Azimuth)就是当前方向和磁北的夹角。由于罗盘保持水平,只需要用磁力计水平方向两轴(通常为X轴和Y轴)的检测数据就可以用式1计 算出航向角。当罗盘水平旋转的时候,航向角在0?- 360?之间变化。 
       2.ST集成磁力计和加速计的传感器模块LSM303DLH 
      2.1  磁力计工作原理 
      在LSM303DLH中磁力计采用各向异性磁致 电阻(Anisotropic Magneto-Resistance)材料来检测空间中磁感应强度的大小。这种具有晶体结构的合金材料对外界的磁场很敏感,磁场的强弱变化会导致AMR自身电阻值发生变化。 
      在制造过程中,将一个 强磁场加在AMR上使其在某一方向上磁化,建立起一个主磁域,与主磁域垂直的轴被称为该AMR的敏感轴,如图3所示。为了使测量结果以线性的方式变化,AMR材料上的金属 导线呈45&ordm;角倾斜排列,电流从这些导线上流过,如图4所示。由初始的强磁场在AMR材料上建立起来的主磁域和电流的方向有45&ordm;的夹角。 

    图3 AMR材料示意图

    图4 45&ordm;角排列的导线

      当有外界磁场Ha时,AMR上主磁域方向就会发生变化而不再是初始的方向了,那么磁场方向和电流的夹角θ也会发生变化,如图5所示。对于AMR材料来说,θ角的变化会引起AMR自身阻值的变化,并且呈线性关系,如图6所示。 

    图5 磁场方向和电流方向的夹角

    图6 θ-R特性曲线

      ST利用惠斯通 电桥检测AMR阻值的变化,如图7所示。R1/R2/R3/R4是初始状态相同的AMR电阻,但是R1/R2和R3/R4具有相反的磁化特性。当检测到外界磁场的时候,R1/R2阻值增加&# 8710;R而R3/R4减少∆R。这样在没有外界磁场的情况下,电桥的输出为零;而在有外界磁场时电桥的输出为一个微小的电压∆V。 

    图7 惠斯通电桥

      当R1=R2=R3=R4=R,在外界磁场的作用下电阻变化为∆R时,电桥输出?V正比于?R。这就是磁力计的工作原理。 
       2.2  置位/复位(Set/Reset)电路 
      由于受到外界环境的影响,LSM303DLH中AMR上的主磁域方向不会永久保持不变。LSM303DLH内置有置位/复位电路,通过内部的金属 线圈周期性的产生电流脉冲,恢复初始的主磁域,如图8所示。需要注意的是,置位脉冲和复位脉冲产生的效果是一样的,只是方向不同而已。 

    图8 LSM303DLH置位/复位电路

      置位/复位电路给LSM303DLH带来很多优点: 
      1)    即使遇到外界强磁场的干扰,在干扰消失后LSM303DLH也能恢复正常工作而不需要用户再次进行校正。 
      2)    即使长时间工作也能保持初始磁化方向实现精确测量,不会因为芯片温度变化或内部噪音增大而影响测量精度。 
      3)    消除由于温漂引起的电桥偏差。 
       2.3  LSM303DLH的性能参数 
      LSM303DLH集成三轴磁力计和三轴加速计,采用数字接口。磁力计的测量范围从1.3 Gauss到8.1 Gauss共分7档,用户可以自由选择。并且在20 Gauss以内的磁场环境下都能够保持一致的测量效果和相同的敏感度。它的分辨率可以达到8 mGauss并且内部采用12位ADC,以保证对磁场强度的精确测量。和采用霍尔效应原理的磁力计相比,LSM303DLH的功耗低,精度高,线性度好, 并且不需要温度补偿。 
      LSM303DLH具有自动检测功能。当控制寄存器A被置位时,芯片内部的自测电路会产生一个约为地磁场大小的激励信号并输出。用户可以通过输出数据来判断芯片是否正常工作。 
      作为高集成度的传感器 模组,除了磁力计以外LSM303DLH还集成一颗高性能的加速计。加速计同样采用12位ADC,可以达到1mg的测量精度。加速计可运行于低功耗模式,并有睡眠/唤醒功能,可大大降低功耗。同时,加速计还集成了6轴方向检测,两路可编程中断接口。 
       3.   ST电子罗盘方案介绍 
      一个传统的电子罗盘系统至少需要一个三轴的磁力计以测量磁场数据,一个三轴加速计以测量罗盘倾角,通过信号条理和数据采集部分将三维空间中的重力分布 和磁场数据传送给处理器。处理器通过磁场数据计算出方位角,通过重力数据进行倾斜补偿。这样处理后输出的方位角不受电子罗盘空间姿态的影响,如图9所示。 

    图9 电子罗盘结构示意图

      LSM303DLH将上述的加速计、磁力计、A/D转化器及信号条理电路集成在一起,仍然通过I2C 总线和处理器通信。这样只用一颗芯片就实现了6轴的数据检测和输出,降低了客户的设计难度,减小了PCB板的占用面积,降低了器件成本。 
      LSM303DLH的典型应用如图10所示。它需要的周边器件很少,连接也很简单,磁力计和加速计各自有一条I2C总线和处理器通信。如果客户的I/O接口电平为 1.8V,Vdd_dig_M、 Vdd_IO_A和Vdd_I2C_Bus均可接1.8V供电,Vdd使用2.5V以上供电即可;如果客户接口电平为2.6V,除了Vdd_dig_M要 求1.8V以外,其他皆可以用2.6V。在上文中提到,LSM303DLH需要置位/复位电路以维持AMR的主磁域。C1和C2为置位/复位电路的外部匹 配电容,由于对置位脉冲和复位脉冲有一定的要求,建议用户不要随意修改C1和C2的大小。 

    图10 LSM303DLH典型应用电路图

      对于便携式设备而言,器件的功耗非常重要,直接影响 其待机的时间。LSM303DLH可以分别对磁力计和加速计的供电模式进行控制,使其进入睡眠或低功耗模式。并且用户可自行调整磁力计和加速计的数据更新 频率,以调整功耗水平。在磁力计数据更新频率为7.5Hz、加速计数据更新频率为50Hz时,消耗电流典型值为0.83mA。在待机模式时,消耗电流小于 3uA。 
       4.   铁磁场干扰及校准 
       电子指南针主 要是通过感知地球磁场的存在来计算磁北极的方向。然而由于地球磁场在一般情况下只有微弱的0.5高斯,而一个普通的手机喇叭当相距2厘米时仍会有大约4高 斯的磁场,一个手机马达在相距2厘米时会有大约6高斯的磁场,这一特点使得针对电子设备表面地球磁场的测量很容易受到电子设备本身的干扰。 
      磁场干扰是指由于具有磁性物质或者可以影响局部磁场强度的物质存在,使得 磁传感器所 放置位置上的地球磁场发生了偏差。如图11所示,在磁传感器的XYZ 坐标系中,绿色的圆表示地球磁场矢量绕z轴圆周转动过程中在XY平面内的投影轨迹,再没有外界任何磁场干扰的情况下,此轨迹将会是一个标准的以 O(0,0)为中心的圆。当存在外界磁场干扰的情况时,测量得到的磁场强度矢量α将为该点地球磁场β与干扰磁场γ的矢量和。记作: 


    图11 磁传感器XY坐标以及磁力线投影轨迹

      一般可以认为,干扰磁场γ在该点可以视为一个恒定的矢量。有很多因素可以造成磁场的干扰,如摆放在电路板上的马达和喇叭,还有含有铁镍钴等金属的材料如屏蔽罩,螺丝,电阻, LCD背板以及外壳等等。同样根据安培定律有电流通过的导线也会产生磁场,如图12。 

    图12 电流对磁场产生的影响

      为了校准这些来自电路板的磁场干扰,主要的工作就是通过计算将γ求出。 
      4.1  平面校准方法 
      针对XY轴的校准,将配备有磁传感器的设备在XY平面内自转,如图11,等价于将地球磁场矢量绕着过点O(γx,γy)垂直于XY平面的法线旋转, 而红色的圆为磁场矢量在旋转过程中在XY平面内投影的轨迹。这可以找到圆心的位置为((Xmax + Xmin)/2,  (Ymax + Ymin)/2).  同样将设备在XZ平面内旋转可以得到地球磁场在XZ平面上的轨迹圆,这可以求出三维空间中的磁场干扰矢量γ(γx, γy, γz). 
       4.2  立体8字校准方法 
      一般情况下,当带有传感器的设备在空中各个方向旋转时,测量值组成的空间几何结构实际上是一个圆球,所有的采样点都落在这个球的表面上,如图13所示,这一点同两维平面内投影得到的圆类似。 

    图13 地球磁场空间旋转后在传感器空间坐标内得到球体

      这种情况下,可以通过足够的样本点求出圆心O(γx, γy, γz), 即固定磁场干扰矢量的大小及方向。公式如下: 

      8字校准法要求用户使用需要校准的设备在空中做8字晃动,原则上尽量多的让设备法线方向指向空间的所有8个象限,如图14所示。 

    图14 设备的空中8字校准示意图

       4.2  十面校准方法 
      同样,通过以下10面校准方法,也可以达到校准的目的。 

    图15 10面交准法步骤

      如图16所示,经过10面校准方法之后,同样可以采样到以上所述球体表面的部分轨迹,从而推导出球心的位置,即固定磁场干扰矢量的大小及方向。 

    图16 10面校准后的空间轨迹

       5.倾斜补偿及航偏角计算 
      经过校准后电子指南针在水平面上已经可以正常使用了。但是更多的时候手机并不是保持水平的,通常它和水平面都有一个夹角。这个夹角会影响航向角的精度,需要通过 加速度传感器进行倾斜补偿。 
      对于一个物体在空中的姿态,导航系统里早已有定义,如图17所示,Android中也采用了这个定义。Pitch(Φ)定义为x轴和水平面的夹角,图 示方向为正方向;Roll(θ)定义为y轴和水平面的夹角,图示方向为正方向。由Pitch角引起的航向角的误差如图18所示。可以看出,在x轴方向10 度的倾斜角就可以引起航向角最大7-8度的误差。 

    图17 Pitch角和Roll角定义                图18 Pitch角引起的航向角误差

      手机在空中的倾斜姿态如图19所示,通过3轴加速度传感器检测出三个轴上重力加速度的分量,再通过式2可以计算出Pitch和Roll。 

    图19 手机在空中的倾斜姿态

      式3可以将磁力计测得的三轴数据(XM,YM ,ZM)通过Pitch和Roll转化为式1中计算航向角需要的Hy和Hx。之后再利用式1计算出航向角。

       6.Android平台指南针的实现 
      在当前流行的android 手机中,很多都配备有指南针的功能。为了实现这一功能,只需要配备有ST提供的二合一传感模块LSM303DLH,ST 提供整套解决方案。Android中的软件实现可以由以下框图表示: 

      其中包括: 
      BSP Reference 
      Linux Kernel Driver (LSM303DLH_ACC + LSM303DLH_MAG) 
       HAL Library(Sensors_lsm303dlh + Liblsm303DLH) for sensors.default.so 
      经过library 的计算,上层的应用可以很轻松的运用由Android定义由Library提供的航偏角信息进行应用程序的编写。 

    发布于 2年前, 阅读(169) | 评论(0) | 投票(0) | 收藏(0) 阅读全文...

  • Android操作系统11种传感器介绍

    分类: 转贴移动开发文章

    092013-10

    在Android2.3 gingerbread系统中,google提供了11种传感器供应用层使用。

     

    #define SENSOR_TYPE_ACCELEROMETER       1 //加速度

    #define SENSOR_TYPE_MAGNETIC_FIELD      2 //磁力

    #define SENSOR_TYPE_ORIENTATION         3 //方向

    #define SENSOR_TYPE_GYROSCOPE           4 //陀螺仪

    #define SENSOR_TYPE_LIGHT               5 //光线感应

    #define SENSOR_TYPE_PRESSURE            6 //压力

    #define SENSOR_TYPE_TEMPERATURE         7 //温度 

    #define SENSOR_TYPE_PROXIMITY           8 //接近

    #define SENSOR_TYPE_GRAVITY             9 //重力

    #define SENSOR_TYPE_LINEAR_ACCELERATION 10//线性加速度

    #define SENSOR_TYPE_ROTATION_VECTOR     11//旋转矢量

     

    我们依次看看这十一种传感器

     

    1 加速度传感器

    加速度传感器又叫G-sensor,返回x、y、z三轴的加速度数值。

    该数值包含地心引力的影响,单位是m/s^2。

    将手机平放在桌面上,x轴默认为0,y轴默认0,z轴默认9.81。

    将手机朝下放在桌面上,z轴为-9.81。

    将手机向左倾斜,x轴为正值。

    将手机向右倾斜,x轴为负值。

    将手机向上倾斜,y轴为负值。

    将手机向下倾斜,y轴为正值。

     

    加速度传感器可能是最为成熟的一种mems产品,市场上的加速度传感器种类很多。

    手机中常用的加速度传感器有BOSCH(博世)的BMA系列,AMK的897X系列,ST的LIS3X系列等。

    这些传感器一般提供±2G至±16G的加速度测量范围,采用I2C或SPI接口和MCU相连,数据精度小于16bit。

     

    2 磁力传感器

    磁力传感器简称为M-sensor,返回x、y、z三轴的环境磁场数据。

    该数值的单位是微特斯拉(micro-Tesla),用uT表示。

    单位也可以是高斯(Gauss),1Tesla=10000Gauss。

    硬件上一般没有独立的磁力传感器,磁力数据由电子罗盘传感器提供(E-compass)。

    电子罗盘传感器同时提供下文的方向传感器数据。

     

    3 方向传感器

    方向传感器简称为O-sensor,返回三轴的角度数据,方向数据的单位是角度。

    为了得到精确的角度数据,E-compass需要获取G-sensor的数据,

    经过计算生产O-sensor数据,否则只能获取水平方向的角度。

    方向传感器提供三个数据,分别为azimuth、pitch和roll。

    azimuth:方位,返回水平时磁北极和Y轴的夹角,范围为0°至360°。

    0°=北,90°=东,180°=南,270°=西。

    pitch:x轴和水平面的夹角,范围为-180°至180°。

    当z轴向y轴转动时,角度为正值。

    roll:y轴和水平面的夹角,由于历史原因,范围为-90°至90°。

    当x轴向z轴移动时,角度为正值。

     

    电子罗盘在获取正确的数据前需要进行校准,通常可用8字校准法。

    8字校准法要求用户使用需要校准的设备在空中做8字晃动,

    原则上尽量多的让设备法线方向指向空间的所有8个象限。

     

    手机中使用的电子罗盘芯片有AKM公司的897X系列,ST公司的LSM系列以及雅马哈公司等等。

    由于需要读取G-sensor数据并计算出M-sensor和O-sensor数据,

    因此厂商一般会提供一个后台daemon来完成工作,电子罗盘算法一般是公司私有产权。

     

    4 陀螺仪传感器

    陀螺仪传感器叫做Gyro-sensor,返回x、y、z三轴的角速度数据。

    角速度的单位是radians/second。

    根据Nexus S手机实测:

    水平逆时针旋转,Z轴为正。

    水平逆时针旋转,z轴为负。

    向左旋转,y轴为负。

    向右旋转,y轴为正。

    向上旋转,x轴为负。

    向下旋转,x轴为正。

     

    ST的L3G系列的陀螺仪传感器比较流行,iphone4和google的nexus s中使用该种传感器。

     

    5 光线感应传感器

    光线感应传感器检测实时的光线强度,光强单位是lux,其物理意义是照射到单位面积上的光通量。

    光线感应传感器主要用于Android系统的LCD自动亮度功能。

    可以根据采样到的光强数值实时调整LCD的亮度。

     

    6 压力传感器

    压力传感器返回当前的压强,单位是百帕斯卡hectopascal(hPa)。

     

    7 温度传感器

    温度传感器返回当前的温度。

     

    8 接近传感器

    接近传感器检测物体与手机的距离,单位是厘米。

    一些接近传感器只能返回远和近两个状态,

    因此,接近传感器将最大距离返回远状态,小于最大距离返回近状态。

    接近传感器可用于接听电话时自动关闭LCD屏幕以节省电量。

    一些芯片集成了接近传感器和光线传感器两者功能。


    下面三个传感器是Android2新提出的传感器类型,目前还不太清楚有哪些应用程序使用。

    9 重力传感器

    重力传感器简称GV-sensor,输出重力数据。

    在地球上,重力数值为9.8,单位是m/s^2。

    坐标系统与加速度传感器相同。

    当设备复位时,重力传感器的输出与加速度传感器相同。

     

    10 线性加速度传感器

    线性加速度传感器简称LA-sensor。

    线性加速度传感器是加速度传感器减去重力影响获取的数据。

    单位是m/s^2,坐标系统与加速度传感器相同。

    加速度传感器、重力传感器和线性加速度传感器的计算公式如下:

    加速度 = 重力 + 线性加速度

     

    11 旋转矢量传感器

    旋转矢量传感器简称RV-sensor。

    旋转矢量代表设备的方向,是一个将坐标轴和角度混合计算得到的数据。

    RV-sensor输出三个数据:

    x*sin(theta/2)

    y*sin(theta/2)

    z*sin(theta/2)

    sin(theta/2)是RV的数量级。

    RV的方向与轴旋转的方向相同。

    RV的三个数值,与cos(theta/2)组成一个四元组。

    RV的数据没有单位,使用的坐标系与加速度相同。

    举例:

    sensors_event_t.data[0] = x*sin(theta/2)

    sensors_event_t.data[1] = y*sin(theta/2)

    sensors_event_t.data[2] = z*sin(theta/2)

    sensors_event_t.data[3] =   cos(theta/2)


    GV、LA和RV的数值没有物理传感器可以直接给出,

    需要G-sensor、O-sensor和Gyro-sensor经过算法计算后得出。

    算法一般是传感器公司的私有产权。

     

    参考文献:

    android source code hardware\libhardware\include\hardwaresensor.h 

    http://www.dzsc.com/data/html/2010-11-29/87454.html

    发布于 2年前, 阅读(1044) | 评论(4) | 投票(2) | 收藏(18) 阅读全文...

  • Android内存泄漏简介

    分类: 转贴移动开发文章

    142013-08

    不少人认为 JAVA程序,因为有垃圾回收机制,应该没有内存泄露。 其实如果我们一个程序中,已经不再使用某个对象,但是因为仍然有引用指向它,垃圾回收器就无法回收它,当然该对象占用的内存就无法被使用,这就造成了内存泄露。如果我们的java运行很久,而这种内存泄露不断的发生,最后就没内存可用了。当然java的内存泄漏和C/C++是不一样的。如果java程序完全结束后,它所有的对象就都不可达了,系统就可以对他们进行垃圾回收,它的内存泄露仅仅限于它本身,而不会影响整个系统的。C/C++的内存泄露就比较糟糕了,它的内存泄露是系统级,即使该C/C++程序退出,它的泄露的内存也无法被系统回收,永远不可用了,除非重启机器。 

    Android 的一个应用程序的内存泄露对别的应用程序影响不大。 为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote 服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的 Android 为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被 kill 掉,这使得仅仅自己的进程被 kill 掉,而不会影响其他进程(如果是 system_process 等系统进程出问题的话,则会引起系统重启)。 

    一、引用没释放造成的内存泄露

    1.1、注册没取消造成的内存泄露

    这种Android的内存泄露比纯java的内存泄露还要严重,因为其他一些Android程序可能引用我们的Anroid程序的对象(比如注册机制)。即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄露的内存依然不能被垃圾回收。

    比如:

    假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。

    但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process进程挂掉。

    虽然有些系统程序,它本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在我们的程序中明确的取消注册,程序结束时应该把所有的注册都取消掉。

    1.2、集合容器对象没清理造成的内存泄露

    我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

    1.3、Context泄漏

    所谓的 Context泄漏,其实更多的是指 Activity泄露,这是一个很隐晦的OutOfMemoryError的情况。 

    先看一个Android官网提供的例子: 

    private static Drawable sBackground;  
    @Override  
    protected void onCreate(Bundle state) {  
      super.onCreate(state);  
      
      TextView label = new TextView(this);  
      label.setText("Leaks are bad");  
      
      if (sBackground == null) {  
        sBackground = getDrawable(R.drawable.large_bitmap);  
      }  
      label.setBackgroundDrawable(sBackground);  
      
      setContentView(label);  
    }

    这段代码效率很快,但同时又是极其错误的; 在第一次屏幕方向切换时它泄露了一开始创建的Activity。当一个Drawable附加到一个 View上时, View会将其作为一个 callback设定到Drawable上。上述的代码片段,意味着这个静态的Drawable拥有一个TextView的引用, 而TextView又拥有Activity(Context类型)的引用,换句话说,Drawable拥有了更多的对象引用。即使Activity被销毁,内存仍然不会被释放。 

    另外,对Context的引用超过它本身的生命周期,也会导致该Context无法回收,从而导致内存泄漏。所以尽量使用Application这种Context类型。 这种Context拥有和应用程序一样长的生命周期,并且不依赖Activity的生命周期。如果你打算保存一个长时间的对象, 并且其需要一个 Context,记得使用Application对象。你可以通过调用Context.getApplicationContext()或 Activity.getApplication()轻松得到Application对象。

    最近遇到一种情况引起了Context泄漏,就是在Activity销毁时,里面有其他线程没有停。 
    总结一下避免Context泄漏应该注意的问题: 

    1.4、static关键字的滥用

    当类的成员变量声明成static后,它是属于类的而不是属于对象的,如果我们将很大的资源对象(Bitmap,context等)声明成static,那么这些资源不会随着对象的回收而回收, 会一直存在,所以在使用static关键字定义成员变量的时候要慎重。

    1.5、WebView对象没有销毁

    当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露

    1.6、GridView的滥用

    GridView和ListView的实现方式不太一样。GridView的View不是即时创建的,而是全部保存在内存中的。比如一个GridView有100项,虽然我们只能看到10项,但是其实整个100项都是在内存中的。

    二、资源对象没关闭造成的内存泄露

    资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。 因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。

    程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。

    三、一些不良代码成内存压力

    有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存,对内存的回收和分配造成很大影响的,容易迫使虚拟机不得不给该应用进程分配更多的内存,造成不必要的内存开支。

    3.1、Bitmap没调用recycle()

    Bitmap对象在不使用时,我们应该先调用recycle(),然后才它设置为null.虽然Bitmap在被回收时可以通过BitmapFinalizer来回收内存。但是调用recycle()是一个良好的习惯在Android4.0之前,Bitmap的内存是分配在Native堆中,调用recycle()可以立即释放Native内存。从Android4.0开始,Bitmap的内存就是分配在dalvik堆中,即JAVA堆中的,调用recycle()并不能立即释放Native内存。但是调用recycle()也是一个良好的习惯。

    可以通过dumpsys meminfo命令查看一个进程的内存情况。

    示例:
    
    adb shell "dumpsys meminfo com.lenovo.robin"
    
    运行结果。
    
    Applications Memory Usage (kB):
    
    Uptime: 18696550 Realtime: 18696541
    
    
    
    ** MEMINFO in pid 7985 [com.lenovo.robin] **
    
                        native   dalvik    other    total
    
                size:     4828     5379      N/A    10207
    
           allocated:     4073     2852      N/A     6925
    
                free:       10     2527      N/A     2537
    
               (Pss):      608      317     1603     2528
    
      (shared dirty):     2240     1896     6056    10192
    
        (priv dirty):      548       36     1276     1860
    
    
    
     Objects
    
               Views:        0        ViewRoots:        0
    
         AppContexts:        0       Activities:        0
    
              Assets:        2    AssetManagers:        2
    
       Local Binders:        5    Proxy Binders:       11
    
    Death Recipients:        1
    
     OpenSSL Sockets:        0
    
    
    
     SQL
    
                   heap:        0         MEMORY_USED:        0
    
     PAGECACHE_OVERFLOW:        0         MALLOC_SIZE:        0

    关于内存统计的更多内容请参考《Android内存泄露利器(内存统计篇)》

    3.2、构造Adapter时,没有使用缓存的 convertView

    以构造ListView的BaseAdapter为例,在BaseAdapter中提共了方法:

    public View getView(int position, View convertView, ViewGroup parent)


    来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参 View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。 

    由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费时间,也造成内存垃圾,给垃圾回收增加压力,如果垃圾回收来不及的话,虚拟机将不得不给该应用进程分配更多的内存,造成不必要的内存开支。ListView回收list item的view对象的过程可以查看:

    android.widget.AbsListView.java --> void addScrapView(View scrap) 方法。

    示例代码:

    public View getView(int position, View convertView, ViewGroup parent) {
    
        View view = new Xxx(...);
    
        ... ...
    
        return view;
    
    }

    修正示例代码:

    public View getView(int position, View convertView, ViewGroup parent){
        View view = null;
        if (convertView != null) {
            view = convertView;
            populate(view, getItem(position));
            ...
    
        } else {
            view = new Xxx(...);
            ...
        }
    
        return view;
    }

    3.3、ThreadLocal使用不当

    如果我们粗暴的把ThreadLocal设置null,而不调用remove()方法或set(null),那么就可能造成ThreadLocal绑定的对象长期也不能被回收,因而产出内存泄露。

    关于此的更多内容请参考《ThreadLocal的内存泄露》

    四、JNI代码的内存泄露

    关于此的详细内容请参考《 JNI引用与垃圾回收》

    发布于 2年前, 阅读(1400) | 评论(4) | 投票(0) | 收藏(118) 阅读全文...

    • 尽量使用Application这种Context类型。

    • 注意对Context的引用不要超过它本身的生命周期。 

    • 慎重的对Context使用“static”关键字。 

    • Context里如果有线程,一定要在onDestroy()里及时停掉。 

  • Android内存溢出分析

    分类: 转贴移动开发文章

    012013-08

        内存溢出,是Android开发中常遇到的问题,解决起来总是摸不着头脑。今天爬爬就来讲讲如何定位内存溢出。

    OOM(内存溢出)和Memory Leak(内存泄露)有什么关系?

    OOM可能是因为Memory Leak,也可能是你的应用本身就比较耗内存(比如图片浏览型的,或者应用本身的设计有问题)。所以,出现OOM不一定是Memory Leak。

    同样,Memory Leak也不一定就会导致OOM,如果泄露的速度很慢,可能还没用完可用内存应用就被重启了,那就不会OOM咯。当然了,有bug解决了最好。

    什么是shallow heap与retained heap?

    shallow heap:你自身占了多少内存,比如你有一个int属性,就占4字节。不包括你引用的其他对象。
    retained heap:如果你被销毁,总共会释放多少内存。这些因你存在被占据的空间就是retained heap。
    更详细的解释请看这篇博客

    什么是GC roots?


    GC的时候,是从这些节点开始遍历,不停的寻找其子节点直到结束。然后把不能遍历到的节点释放。这些遍历的起点(注意,可不是一个哦)就叫做GC roots。

    那,对于java来说,谁是GC roots?简单点说(不是那么准确)包括以下几种:

    栈上面的局部变量
    栈上面的函数参数变量
    所有由Bootstrap Loader加载的类变量
    另外,JNI相关的也会有
    更多详细解释请看这篇博客
    其实到最后,谁是GC roots不是那么重要,因为一般来说,到最后就剩下一些系统框架类,以及jvm和class相关的东西。这里给大家说GC roots主要是因为使用mat需要了解它。

    怎样使用MAT定位内存泄露?

    看Histogram(类统计图)

    histogram视图显示了每个类有多少实例,并可以按照这些实例占据的Retained size和Shallow size排序。通过过滤包名,很容易发现有问题的类。

    这里有几个简单的原则,比如,activity的实例通常只应该有一个。已经关闭的activity不应该出现。实体类的Retained size应该是比较小的,也就几十KB。

    对于Android程序来说,内存泄露通常都会牵扯到activity。因此,dump之前,可以多旋转几次屏幕并反复的进出可能有问题的activity,让问题尽可能的凸现。
    通过Histogram我们可以看每个类有多少个实例,shallow和retained heap分别有多大。如果只是看java的基础类型和framework的类,没有什么意义,一定要过滤出自己的类型,如下图

    发现LeakInnerClassActivity产生了9个实例,一定是被hold住了。

    看Dominator Tree

    大家来看这个图,左侧是对象引用关系,右侧是dominator tree

    Note that A, B and C are dominated by a “virtual” root object.
    Note that the dominator relationship is transitive;C dominates E which dominates G therefore C also dominates G.
    这个视图非常强大,它把所有实例按Retained heap和Shallow heap列出来;并且,只要展开就可以看到这个实例所占有的实例(换句话说,如果该对象被释放,还会有哪些对象被释放)

    使用这个视图,可以很方便的追踪被泄露的内存到底是谁占用了,更多参考这篇博客

    对比heap dumps,可以更快的定位内存泄露的位置。操作步骤:

    打开一个HPROF文件,切换到histogram视图
    在Navigation View中右键点击histogram,选择Add to compare basket
    打开另一个HPROF文件,并重复上一个步骤
    对比两次heap dumps的内容,看下图,LeakInnerClassActivity的实例又增加了一个。而我仅仅是又启动了一次该Activity,所以问题显而易见。

    参考:Memory Analysis for Android Applications

    内部类怎样使用才会产生内存泄露,以及由此衍生的AsyncTask、Handler问题如何解决?

    如果非静态内部类的方法中,有生命周期大于其所在类的,那就有问题了。比如:AsyncTask、Handler,这两个类都是方便开发者执行异步任务的,但是,这两个都跳出了Activity/Fragment的生命周期。或许,是时候学习Loader了
    为什么?因为非静态内部类会自动持有一个所属类的实例,如果所属类的实例已经结束生命周期,但内部类的方法仍在执行,就会hold其主体。也就使主体不能被释放,亦即内存泄露。
    静态类呢?静态类编译后和非内部类是一样的,有自己独立的类名。不会悄悄引用所属类的实例,所以就不容易泄露。

    //首先,静态类
    static class IncomingHandler extends Handler {
     //其次,弱引用
        private final WeakReference mService;
            IncomingHandler(UDPListenerService service) {
            mService = new WeakReference<UDPListenerService>(service);
        }
        @Override
        public void handleMessage(Message msg) {
            UDPListenerService service = mService.get();
            if (service != null) {
                service.handleMessage(msg);
            }
        }
    }

    图片导致的OOM如何解决?

    加载时使用option,用多大,载入多大。
    res目录下的图片也是一样,及时清理过大的图片资源。
    如果还有问题,就想办法把不可见的资源释放掉,比如,TabActivity中不可见的Tab,ViewPager中的Fragment。
    如果activity的图片资源较多,需要考虑屏幕旋转时,销毁已有资源。请参考这篇文章

    需要context的时候用activity还是application?


    看使用的周期是否在activity周期内,如果超出,必须用application;常见的情景包括:AsyncTask,Thread,第三方库初始化等等。
    还有些情景,只能用activity:比如,对话框,各种View,需要startActivity的等。
    总之,尽可能使用Application。参考stackoverflow

    什么时候需要手动将变量设置为NULL?


    类变量,一旦用完,尽快释放。因为类的存活时间最长,所以,占用的资源越少越好;
    比较耗时且耗内存的方法内的局部变量,比如,图片处理的方法,每个bitmap对象用完就及时丢弃。尽可能让gc介入。
    参考:http://kohlerm.blogspot.com/

    发布于 2年前, 阅读(443) | 评论(0) | 投票(0) | 收藏(35) 阅读全文...

  • Handler和Runnable常见的问题

    分类: 转贴移动开发文章

    122013-07

    提问者的大意如下:

        在一个非UI线程中,如果连续给Handler对象post两个Runnable对象,那么第二个Runnable对象是不是需要等到第一个Runnable对象执行完之后才开始执行。


    答案:

        是的,第二个Runnable对象需要等到第一个Runnable对象执行完成之后才能开始执行。


    问题出处:

    http://stackoverflow.com/questions/8336579/does-handler-execute-runnables-simultaneously





    提问者的大意如下:

        在主UI线程中,使用handler.post(Runnable r)方法是否会创建一个新的线程?


    答案:

        不会,这个Runnable还是在主UI线程中。


    问题出处:
    http://stackoverflow.com/questions/9163411/does-handler-postrunnable-start-a-new-thread






    提问者的大意如下:

        怎么给Runnable传入参数?


    答案:

        · 在构造Runnable的时候把参数写成它的成员变量;

        · 把要传入的参数用final进行修饰;

        · 使用Callable


    问题出处:

    http://stackoverflow.com/questions/9123272/is-there-a-way-to-pass-parameters-to-a-runnable



    问题大意:

        Acticity.runOnUiThread(Runnable action) 和 Handler.post(Runnable r)的区别?


    问题出处:

    http://stackoverflow.com/questions/1839625/whats-the-difference-between-activity-runonuithreadrunnable-action-and-handler





    问题大意:

        Runnable是用来实现Thread中run()方法的代码,本身和Thread没有任何关系。


    问题出处:

    http://stackoverflow.com/questions/9029795/new-runnable-but-no-new-thread



    发布于 2年前, 阅读(791) | 评论(0) | 投票(0) | 收藏(6) 阅读全文...

  • Android性能调优

    分类: 转贴移动开发文章

    202013-06

    本文主要分享自己在appstore项目中的性能调优点,包括同步改异步、缓存、Layout优化、数据库优化、算法优化、延迟执行等。
    一、性能瓶颈点
    整个页面主要由6个Page的ViewPager,每个Page为一个GridView,GridView一屏大概显示4*4的item信息(本文最后有附图)。由于网络数据获取较多且随时需要保持页面内app下载进度及状态,所以出现以下性能问题
    a.  ViewPager左右滑动明显卡顿
    b.  GridView上下滚动明显卡顿
    c.  其他Activity返回ViewPager Activity较慢
    d.  网络获取到展现速度较慢


    二、性能调试及定位

    主要使用Traceview、monkey、monkey runner调试,traceview类似java web调优的visualvm,使用方法如下:
    在需要调优的activity onCreate函数中添加

    android.os.debug.startMethodTracing("Entertainment");

    onDestrory函数中添加

    android.os.debug.stopMethodTracing();

    程序退出后会在sd卡根目录下生成Entertainment.trace这个文件,cmd到android sdk的tools目录下运行traceview.bat Entertainment.trace即可,截图如下

    traceview

    从中可以看出各个函数的调用时间、调用次数、平均调用时间、时间占用百分比等从而定位到耗时的操作。monkey、monkey runner更详细的见后面博客介绍


    三、性能调优点

    主要包括同步改异步、缓存、Layout优化、数据库优化、算法优化、延迟执行。
    1. 同步改异步
    这个就不用多讲了,耗时操作放在线程中执行防止占用主线程,一定程度上解决anr。
    但需要注意线程和service结合(防止activity被回收后线程也被回收)以及线程的数量(后面优化介绍)
    PS:请使用java的线程池(后面介绍),少使用AsyncTask,因为AsyncTask存在性能问题(以后会单独博文介绍)


    2. 缓存

    java的对象创建需要分配资源较耗费时间,加上创建的对象越多会造成越频繁的gc影响系统响应。主要使用单例模式、缓存(图片缓存、线程池、View缓存、IO缓存、消息缓存、通知栏notification缓存)及其他方式减少对象创建。
    (1). 单例模式
    对于创建开销较大的类可使用此方法,保证全局一个实例,在程序运行过程中该类不会因新建额外对象产生开销。示例代码如下:

    单例模式:
    
    class Singleton {
        private static Singleton instance = null;
     
        private Singleton() {
        }
     
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }


    (2). 缓存
    程序中用到了图片缓存、线程池、View缓存、IO缓存、消息缓存、通知栏notification缓存等。
    a. 图片缓存:ImageCacheImageSdCache


    b. 线程池:
    使用Java的Executors类,通过newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool提供四种不同类型的线程池


    c. View缓存:

    listView的getView缓存:
    
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.type_item, null);
            holder = new ViewHolder();
            holder.imageView = (ImageView)convertView.findViewById(R.id.app_icon);
            holder.textView = (TextView)convertView.findViewById(R.id.app_name);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder)convertView.getTag();
        }
        holder.imageView.setImageResource(R.drawable.index_default_image);
        holder.textView.setText("");
        return convertView;
    }
     
    /**
    * ViewHolder
    */
    static class ViewHolder {
     
        ImageView imageView;
        TextView  textView;
    }

    通过convertView是否为null减少layout inflate次数,通过静态的ViewHolder减少findViewById的次数,这两个函数尤其是inflate是相当费时间的


    d. IO缓存:

    使用具有缓存策略的输入流,BufferedInputStream替代InputStream,BufferedReader替代Reader,BufferedReader替代BufferedInputStream.对文件、网络IO皆适用。


    e. 消息缓存:
    通过Handler的obtainMessage回收就的Message对象,减少Message对象的创建开销
    handler.sendMessage(handler.obtainMessage(1));


    f. 通知栏notification缓存:
    下载中需要不断改变通知栏进度条状态,如果不断新建Notification会导致通知栏很卡。这里我们可以使用最简单的缓存
    Map<String, Notification> notificationMap = new HashMap<String, Notification>();如果notificationMap中不存在,则新建notification并且put into map.


    (3). 其他

    能创建基类解决问题就不用具体子类:除需要设置优先级的线程使用new Thread创建外,其余线程创建使用new Runnable。因为子类会有自己的属性创建需要更多开销。
    控制最大并发数量:使用Java的Executors类,通过Executors.newFixedThreadPool(nThreads)控制线程池最大线程并发
    对于http请求增加timeout

    3. Layout优化
    性能优化相关的一些标签 <viewStub/>,<merge/>和<include/> 可见:http://hexen.blog.51cto.com/1110171/820197
    TextView属性优化:TextView的android:ellipsize=”marquee”跑马灯效果极耗性能,具体原因还在深入源码中
    对于layout中的布局实际效果可使用hierarchyviewer查看
    对于layout中多余的view以及不正确的标签可使用android lint查看

    4. 数据库优化
    主要包括sql优化、建立索引、使用事务、读写表区分
    (1). sql优化
    可参考http://database.51cto.com/art/200904/118526.htm

    (2). 建立索引
    使用CREATE INDEX mycolumn_index ON mytable (myclumn)语句在SQLiteOpenHelper子类的onCreate或onUpgrade函数创建索引,索引创建后对大数据量的查询性能提升效果较明显

    (3). 使用事务
    事务不仅能保证批量操作一起完成或回滚,而且在大量插入、更新、查询时减少程序和表的交互从而提高性能

    事务示例:
    
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    db.beginTransaction();
    try {
        // add to do
        db.setTransactionSuccessful();
    } catch (Exception e) {
        Log.e(TAG, e.toString());
    } finally {
        db.endTransaction();
    }

    (4). 读写表区分
    对于查询操作使用dbHelper.getReadableDatabase();读表代替写表。因为sqlite是表级锁,所以修改和插入等写操作的性能较差。

    5. 算法优化
    这个就是个博大精深的话题了,只介绍本应用中使用的。
    使用hashMap代替arrayList,时间复杂度降低一个数量级


    6. 延迟执行

    对于很多耗时逻辑没必要立即执行,这时候我们可以将其延迟执行。
    线程延迟执行 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
    消息延迟发送 handler.sendMessageDelayed(handler.obtainMessage(0), 1000);


    四、本程序性能调优结果

    1. ViewPager左右滑动明显卡顿
    2. GridView上下滚动明显卡顿

    (1). 去掉TextView的android:ellipsize=”marquee”
    (2). 修改图片缓存的最大线程数,增加http timeout
    (3). 修改设置app是否已安装的状态,具体代码修改如下:

    List<PackageInfo> installedPackageList = getPackageManager().getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);
    List<App> installedAppList = function(installedAppList)
    for (App app : appList) {
        for (App installedApp : installedAppList) {
     
        }
    }

    修改为

    for (App app : appList) {
        Pair<Integer, String> versionInfo = INSTALLED_APP_MAP.get(app.getPackageName());
        if (versionInfo != null) {
     
        } else {
     
        }
    }

    从每次获取List<PackageInfo> installedAppList = getPackageManager().getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES); 修改为只在有应用安装或卸载广播时获取应用列表,并且用hashMap代替installedAppList减少查询时间。

    将平均执行时间从201ms降低到1ms。


    3. 其他Activity返回ViewPager Activity较慢

    定位:在onStart函数
    解决:使用延迟策略,具体代码修改如下:

    @Override
    public void onStart() {
        super.onStart();
        appUpdateListAdapter.notifyDataSetChanged();
    }

    改为

    public void onStart() {
        super.onStart();
        // delay send message
        handler.sendMessageDelayed(handler.obtainMessage(MessageConstants.WHAT_NOTIFY_DATA_CHANGED), 100);
    }
     
    private class MyHandler extends Handler {
     
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
     
            switch (msg.what) {
                case MessageConstants.WHAT_NOTIFY_DATA_CHANGED:
                    if (appUpdateListAdapter != null) {
                        appUpdateListAdapter.notifyDataSetChanged();
                    }
                    break;
            }
        }
    }

    4. 网络获取到展现速度较慢

    定位:在HttpURLConnection.getInputStream()之后的处理
    解决:使用BufferedReader替代BufferedInputStream获取时间从100ms降低到3ms,具体代码修改如下:

    HttpURLConnection con = (HttpURLConnection)url.openConnection();
    InputStream input = con.getInputStream();
    while (input.read(buffer, 0, 1024) != -1) {
     
    }

    改为

    HttpURLConnection con = (HttpURLConnection)url.openConnection();
    BufferedReader input = new BufferedReader(new InputStreamReader(con.getInputStream()));
    String s;
    while ((s = input.readLine()) != null) {
     
    }

    附 娱乐精选截图:

    initpintu_副本

    发布于 2年前, 阅读(1071) | 评论(0) | 投票(0) | 收藏(40) 阅读全文...

  • Android应用性能优化之使用SparseArray替代HashMap

    分类: 转贴移动开发文章

    202013-06

    HashMap是java里比较常用的一个集合类,我比较习惯用来缓存一些处理后的结果。最近在做一个Android项目,在代码中定义这样一个变量,实例化时,Eclipse却给出了一个 performance 警告。

     

    意 思就是说用SparseArray<E>来替代,以获取更好性能。老实说,对SparseArray并不熟悉,第一感觉应该是Android 提供的一个类。按住Ctrl点击进入SparseArray的源码,果不其然,确定是Android提供的一个工具类。

    单纯从字面上来理解,SparseArray指的是稀疏数组(Sparse array),所谓稀疏数组就是数组中大部分的内容值都未被使用(或都为零),在数组中仅有少部分的空间使用。因此造成内存空间的浪费,为了节省内存空间,并且不影响数组中原有的内容值,我们可以采用一种压缩的方式来表示稀疏数组的内容。

    假设有一个9*7的数组,其内容如下:

     

     

    在此数组中,共有63个空间,但却只使用了5个元素,造成58个元素空间的浪费。以下我们就使用稀疏数组重新来定义这个数组:

     

     

    其中在稀疏数组中第一部分所记录的是原数组的列数和行数以及元素使用的个数、第二部分所记录的是原数组中元素的位置和内容。经过压缩之后,原来需要声明大小为63的数组,而使用压缩后,只需要声明大小为6*3的数组,仅需18个存储空间。

     

    继续阅读SparseArray的源码,从构造方法我们可以看出,它和一般的List一样,可以预先设置容器大小,默认的大小是10:

    public SparseArray() {
        this(10);
    }
     
    public SparseArray(int initialCapacity) {
        initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
     
        mKeys = new int[initialCapacity];
        mValues = new Object[initialCapacity];
        mSize = 0;
    }

    再来看看它对数据的“增删改查”。

    它有两个方法可以添加键值对:

    public void put(int key, E value) {}
    public void append(int key, E value){}


    有四个方法可以执行删除操作:

    public void delete(int key) {}
    public void remove(int key) {} //直接调用的delete(int key)
    public void removeAt(int index){}
    public void clear(){}


    修 改数据起初以为只有setValueAt(int index, E value)可以修改数据,但后来发现put(int key, E value)也可以修改数据,我们查看put(int key, E value)的源码可知,在put数据之前,会先查找要put的数据是否已经存在,如果存在就是修改,不存在就添加。

    public void put(int key, E value) {
        int i = binarySearch(mKeys, 0, mSize, key);
     
        if (i &gt;= 0) {
            mValues[i] = value;
        } else {
            i = ~i;
     
            if (i &lt; mSize &amp;&amp; mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
     
            if (mGarbage &amp;&amp; mSize &gt;= mKeys.length) {
                gc();
     
                // Search again because indices may have changed.
                i = ~binarySearch(mKeys, 0, mSize, key);
            }
            …………


    所以,修改数据实际也有两种方法:

    public void put(int key, E value)
    public void setValueAt(int index, E value)


    最后再来看看如何查找数据。有两个方法可以查询取值:

    public E get(int key)
    public E get(int key, E valueIfKeyNotFound)


    其中get(int key)也只是调用了 get(int key,E valueIfKeyNotFound),最后一个从传参的变量名就能看出,传入的是找不到的时候返回的值.get(int key)当找不到的时候,默认返回null。

    查看第几个位置的键:

    public int keyAt(int index)


    有一点需要注意的是,查看键所在位置,由于是采用二分法查找键的位置,所以找不到时返回小于0的数值,而不是返回-1。返回的负值是表示它在找不到时所在的位置。

    查看第几个位置的值:

    public E valueAt(int index)


    查看值所在位置,没有的话返回-1:

    public int indexOfValue(E value)


    最后,发现其核心就是折半查找函数(binarySearch),算法设计的很不错。

    private static int binarySearch(int[] a, int start, int len, int key) {
        int high = start + len, low = start - 1, guess;
     
        while (high - low &gt; 1) {
            guess = (high + low) / 2;
     
            if (a[guess] &lt; key)
                low = guess;
            else
                high = guess;
        }
     
        if (high == start + len)
            return ~(start + len);
        else if (a[high] == key)
            return high;
        else
            return ~high;
    }


    相应的也有SparseBooleanArray,用来取代HashMap<Integer, Boolean>,SparseIntArray用来取代HashMap<Integer, Integer>,大家有兴趣的可以研究。

    总结:SparseArray是android里为<Interger,Object>这样的Hashmap而专门写的类,目的是提高效率,其核心是折半查找函数(binarySearch)。在Android中,当我们需要定义

    HashMap<Integer, E> hashMap = new HashMap<Integer, E>()


    时,我们可以使用如下的方式来取得更好的性能.

    SparseArray<E> sparseArray = new SparseArray<E>();


    发布于 2年前, 阅读(324) | 评论(1) | 投票(0) | 收藏(16) 阅读全文...

  • More about keeping Android’s screen awake

    分类: 转贴移动开发文章

    192013-06

    In my previous post, I discussed the pros and cons of the various means of keeping the screen alive in Android, focusing mainly on the need for sandbox permissions in your application’sAndroidManifest.xmlfile.

    In this post, I’d like to further elaborate on the “shortcut” techniques to allow you to keep the screen alive, without explicitly managing a Wake Lock in your application.

    FLAG_KEEP_SCREEN_ON

    I’ve already covered the technique of setting a flag on your Activity’s main window to keep the screen active:

    getWindow().addFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON );

    There are two things to keep in mind with this approach:

    I won’t discuss how to create and manage a Wake Lock directly; instead, I’ll refer you to the SDK docs and to a short code example (I happen to like the anddev.org site for its nice, short and useful code snippets).

    android:keepScreenOn

    Although I haven’t tried this in my own dev work, you can also enforce “screen always on” behavior on a per-view basis.

    Every view has an android:keepScreenOnattribute that you can use to keep the screen active whenever the view is visible.  This should be as easy as simply setting the attribute in the XML layout you use to define your views, plus it needs no Java code at all.

    I suspectandroid:keepScreenOnworks by setting theFLAG_KEEP_SCREEN_ONon the window for you whenever the view is visible to the user. Anyway, this is handy to use if your need to keep the screen active is associated with a particular view in your app.


    发布于 2年前, 阅读(31) | 评论(0) | 投票(0) | 收藏(0) 阅读全文...

  1. FLAG_KEEP_SCREEN_ONonly works when the app window is onscreen.  If the user switches apps, Android automatically releases the wake lock.  That might be fine for your use case, but if you have a long-running background task that really needs to keep the device running until finished, this isn’t an airtight approach.

  2. FLAG_KEEP_SCREEN_ONdoes just that; it keeps the screen active along with the rest of the device.  If you directly create and manage a raw wake lock in your own code, you have more granularity, for example allowing the screen to go dim or go off completely whilst still allowing the CPU to do its thing.

浅谈ANR及如何分析解决ANR

分类: 转贴移动开发文章

132013-06

一:什么是ANR

ANR:Application Not Responding,即应用无响应

二:ANR的类型

ANR一般有三种类型:

三:KeyDispatchTimeout

Akey or touch event was not dispatched within the specified time(按键或触摸事件在特定时间内无响应)

具体的超时时间的定义在framework下的

ActivityManagerService.java

//How long we wait until we timeout on key dispatching.

staticfinal int KEY_DISPATCHING_TIMEOUT = 5*1000

四:为什么会超时呢?

超时时间的计数一般是从按键分发给app开始。超时的原因一般有两种

(1)当前的事件没有机会得到处理(即UI线程正在处理前一个事件,没有及时的完成或者looper被某种原因阻塞住了)

(2)当前的事件正在处理,但没有及时完成

五:如何避免KeyDispatchTimeout

六:UI线程

说了那么多的UI线程,那么哪些属于UI线程呢?

UI线程主要包括如下:

:如何去分析ANR

先看个LOG:

04-01 13:12:11.572 I/InputDispatcher( 220): Application is not responding:Window{2b263310com.android.email/com.android.email.activity.SplitScreenActivitypaused=false}.  5009.8ms since event, 5009.5ms since waitstarted

04-0113:12:11.572 I/WindowManager( 220): Input event dispatching timedout sending tocom.android.email/com.android.email.activity.SplitScreenActivity

04-01 13:12:14.123 I/Process(  220): Sending signal. PID: 21404 SIG: 3---发生ANR的时间和生成trace.txt的时间

04-01 13:12:14.123 I/dalvikvm(21404):threadid=4: reacting to signal 3 

……

04-0113:12:15.872 E/ActivityManager(  220): ANR in com.android.email(com.android.email/.activity.SplitScreenActivity)

04-0113:12:15.872 E/ActivityManager(  220): Reason:keyDispatchingTimedOut

04-0113:12:15.872 E/ActivityManager(  220): Load: 8.68 / 8.37 / 8.53

04-0113:12:15.872 E/ActivityManager(  220): CPUusage from 4361ms to 699ms ago ----CPUANR发生前的使用情况


04-0113:12:15.872 E/ActivityManager(  220):   5.5%21404/com.android.email: 1.3% user + 4.1% kernel / faults: 10 minor

04-0113:12:15.872 E/ActivityManager(  220):   4.3%220/system_server: 2.7% user + 1.5% kernel / faults: 11 minor 2 major

04-0113:12:15.872 E/ActivityManager(  220):   0.9%52/spi_qsd.0: 0% user + 0.9% kernel

04-0113:12:15.872 E/ActivityManager(  220):   0.5%65/irq/170-cyttsp-: 0% user + 0.5% kernel

04-0113:12:15.872 E/ActivityManager(  220):   0.5%296/com.android.systemui: 0.5% user + 0% kernel

04-0113:12:15.872 E/ActivityManager(  220): 100%TOTAL: 4.8% user + 7.6% kernel + 87% iowait

04-0113:12:15.872 E/ActivityManager(  220): CPUusage from 3697ms to 4223ms later:-- ANRCPU的使用量

04-0113:12:15.872 E/ActivityManager(  220):   25%21404/com.android.email: 25% user + 0% kernel / faults: 191 minor

04-0113:12:15.872 E/ActivityManager(  220):    16% 21603/__eas(par.hakan: 16% user + 0% kernel

04-0113:12:15.872 E/ActivityManager(  220):    7.2% 21406/GC: 7.2% user + 0% kernel

04-0113:12:15.872 E/ActivityManager(  220):    1.8% 21409/Compiler: 1.8% user + 0% kernel

04-0113:12:15.872 E/ActivityManager(  220):   5.5%220/system_server: 0% user + 5.5% kernel / faults: 1 minor

04-0113:12:15.872 E/ActivityManager(  220):    5.5% 263/InputDispatcher: 0% user + 5.5% kernel

04-0113:12:15.872 E/ActivityManager(  220): 32%TOTAL: 28% user + 3.7% kernel


LOG可以看出ANR的类型,CPU的使用情况,如果CPU使用量接近100%,说明当前设备很忙,有可能是CPU饥饿导致了ANR

如果CPU使用量很少,说明主线程被BLOCK

如果IOwait很高,说明ANR有可能是主线程在进行I/O操作造成的

除了看LOG,解决ANR还得需要trace.txt文件,

如何获取呢?可以用如下命令获取

trace.txt文件,看到最多的是如下的信息:

-----pid 21404 at 2011-04-01 13:12:14 -----  
Cmdline: com.android.email

DALVIK THREADS:
(mutexes: tll=0tsl=0 tscl=0 ghl=0 hwl=0 hwll=0)
"main" prio=5 tid=1NATIVE
  | group="main" sCount=1 dsCount=0obj=0x2aad2248 self=0xcf70
  | sysTid=21404 nice=0 sched=0/0cgrp=[fopen-error:2] handle=1876218976
  atandroid.os.MessageQueue.nativePollOnce(Native Method)
  atandroid.os.MessageQueue.next(MessageQueue.java:119)
  atandroid.os.Looper.loop(Looper.java:110
)
 at android.app.ActivityThread.main(ActivityThread.java:3688)
 at java.lang.reflect.Method.invokeNative(Native Method)
  atjava.lang.reflect.Method.invoke(Method.java:507)
  atcom.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:624)
 at dalvik.system.NativeStart.main(Native Method)

说明主线程在等待下条消息进入消息队列

八:Thread状态

ThreadState (defined at “dalvik/vm/thread.h “)

THREAD_UNDEFINED = -1, /* makes enum compatible with int32_t */

THREAD_ZOMBIE = 0, /* TERMINATED */

THREAD_RUNNING = 1, /* RUNNABLE or running now */

THREAD_TIMED_WAIT = 2, /* TIMED_WAITING in Object.wait() */

THREAD_MONITOR = 3, /* BLOCKED on a monitor */

THREAD_WAIT = 4, /* WAITING in Object.wait() */

THREAD_INITIALIZING= 5, /* allocated, not yet running */

THREAD_STARTING = 6, /* started, not yet on thread list */

THREAD_NATIVE = 7, /* off in a JNI native method */

THREAD_VMWAIT = 8, /* waiting on a VM resource */

THREAD_SUSPENDED = 9, /* suspended, usually by GC or debugger */

九:如何调查并解决ANR

十:案例

案例1关键词:ContentResolver in AsyncTask onPostExecute, high iowait

Process:com.android.email
Activity:com.android.email/.activity.MessageView
Subject:keyDispatchingTimedOut
CPU usage from 2550ms to -2814ms ago:
5%187/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20major
4.4% 1134/com.android.email: 0.7% user + 3.7% kernel /faults: 38 minor 19 major
4% 372/com.android.eventstream: 0.7%user + 3.3% kernel / faults: 6 minor
1.1% 272/com.android.phone:0.9% user + 0.1% kernel / faults: 33 minor
0.9%252/com.android.systemui: 0.9% user + 0% kernel
0%409/com.android.eventstream.telephonyplugin: 0% user + 0% kernel /faults: 2 minor
0.1% 632/com.android.devicemonitor: 0.1% user + 0%kernel
100%TOTAL: 6.9% user + 8.2% kernel +84%iowait


-----pid 1134 at 2010-12-17 17:46:51 -----
Cmd line:com.android.email

DALVIK THREADS:
(mutexes: tll=0 tsl=0tscl=0 ghl=0 hwl=0 hwll=0)
"main" prio=5 tid=1 WAIT
|group="main" sCount=1 dsCount=0 obj=0x2aaca180self=0xcf20
| sysTid=1134 nice=0 sched=0/0 cgrp=[fopen-error:2]handle=1876218976
at java.lang.Object.wait(Native Method)
-waiting on <0x2aaca218> (a java.lang.VMThread)
atjava.lang.Thread.parkFor(Thread.java:1424)
atjava.lang.LangAccessImpl.parkFor(LangAccessImpl.java:48)
atsun.misc.Unsafe.park(Unsafe.java:337)
atjava.util.concurrent.locks.LockSupport.park(LockSupport.java:157)
atjava.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:808)
atjava.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:841)
atjava.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1171)
atjava.util.concurrent.locks.ReentrantLock$FairSync.lock(ReentrantLock.java:200)
atjava.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:261)
atandroid.database.sqlite.SQLiteDatabase.lock(SQLiteDatabase.java:378)
atandroid.database.sqlite.SQLiteCursor.<init>(SQLiteCursor.java:222)
atandroid.database.sqlite.SQLiteDirectCursorDriver.query(SQLiteDirectCursorDriver.java:53)
atandroid.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1356)
atandroid.database.sqlite.SQLiteDatabase.queryWithFactory(SQLiteDatabase.java:1235)
atandroid.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1189)
atandroid.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1271)
atcom.android.email.provider.EmailProvider.query(EmailProvider.java:1098)
atandroid.content.ContentProvider$Transport.query(ContentProvider.java:187)
atandroid.content.
ContentResolver.query(ContentResolver.java:268)
atcom.android.email.provider.EmailContent$Message.restoreMessageWithId(EmailContent.java:648)
atcom.android.email.Controller.setMessageRead(Controller.java:658)
atcom.android.email.activity.MessageView.onMarkAsRead(MessageView.java:700)
atcom.android.email.activity.MessageView.access$2500(MessageView.java:98)
at
com.android.email.activity.MessageView$LoadBodyTask.onPostExecute(MessageView.java:1290)
atcom.android.email.activity.MessageView$LoadBodyTask.onPostExecute(MessageView.java:1255)
atandroid.os.AsyncTask.finish(AsyncTask.java:417)
atandroid.os.AsyncTask.access$300(AsyncTask.java:127)
at
android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:429)
atandroid.os.Handler.dispatchMessage(Handler.java:99)
atandroid.os.Looper.loop(Looper.java:123)
atandroid.app.ActivityThread.main(ActivityThread.java:3652)
atjava.lang.reflect.Method.invokeNative(Native Method)
atjava.lang.reflect.Method.invoke(Method.java:507)
atcom.android.internal.os.ZygoteIn

原因:IOWait很高,说明当前系统在忙于I/O,因此数据库操作被阻塞

原来:

final Message message = Message.restoreMessageWithId(mProviderContext, messageId);

if (message == null) {
    return;
}

Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
if (account == null) {
    return;//isMessagingController returns false for null, but let's make itclear.
}

if (isMessagingController(account)) {
    newThread() {
        @Override
        public void run() {         
            mLegacyController.processPendingActions(message.mAccountKey);
        }
    }.start();
}



解决后:

newThread() {        
    final Message message = Message.restoreMessageWithId(mProviderContext, messageId);

    if (message == null) {
        return;
    }

    Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);

    if (account == null) {
        return;//isMessagingController returns false for null, but let's make itclear.
    }

    if (isMessagingController(account)) {              
      mLegacyController.processPendingActions(message.mAccountKey);
    }
}.start();


关于AsyncTask:http://developer.android.com/reference/android/os/AsyncTask.html


案例2关键词:UI线程进行网络数据的读写

ANRin process: com.android.mediascape:PhotoViewer (last incom.android.mediascape:PhotoViewer)
Annotation:keyDispatchingTimedOut
CPU usage:
Load: 6.74 / 6.89 / 6.12
CPUusage from 8254ms to 3224ms ago:
ovider.webmedia: 4% = 4% user +0% kernel / faults: 68 minor
system_server: 2% = 1% user + 0%kernel / faults: 18 minor
re-initialized>: 0% = 0% user + 0%kernel / faults: 50 minor
events/0: 0% = 0% user + 0%kernel
TOTAL:7% = 6% user + 1% kernel

DALVIKTHREADS:
""main"" prio=5 tid=3 NATIVE
|group=""main"" sCount=1 dsCount=0 s=Yobj=0x4001b240 self=0xbda8
| sysTid=2579 nice=0 sched=0/0cgrp=unknown handle=-1343993184
atorg.apache.harmony.luni.platform.OSNetworkSystem.receiveStreamImpl(NativeMethod)
atorg.apache.harmony.luni.platform.
OSNetworkSystem.receiveStream(OSNetworkSystem.java:478)
atorg.apache.harmony.luni.net.PlainSocketImpl.read(PlainSocketImpl.java:565)
atorg.apache.harmony.luni.net.SocketInputStream.read(SocketInputStream.java:87)
atorg.apache.harmony.luni.internal.net.www.protocol.http.HttpURLConnection$LimitedInputStream.read(HttpURLConnection.java:303)
atjava.io.InputStream.read(InputStream.java:133)
atjava.io.BufferedInputStream.fillbuf(BufferedInputStream.java:157)
atjava.io.BufferedInputStream.read(BufferedInputStream.java:346)
atandroid.graphics.BitmapFactory.nativeDecodeStream(Native Method)
atandroid.graphics.
BitmapFactory.decodeStream(BitmapFactory.java:459)
atcom.android.mediascape.activity.PhotoViewerActivity.
getPreviewImage(PhotoViewerActivity.java:4465)
atcom.android.mediascape.activity.PhotoViewerActivity.
dispPreview(PhotoViewerActivity.java:4406)
atcom.android.mediascape.activity.PhotoViewerActivity.access$6500(PhotoViewerActivity.java:125)

atcom.android.mediascape.activity.PhotoViewerActivity$33$1.run(PhotoViewerActivity.java:4558)
atandroid.os.Handler.handleCallback(Handler.java:587)
atandroid.os.Handler.dispatchMessage(Handler.java:92)
atandroid.os.Looper.loop(Looper.java:123)
atandroid.app.ActivityThread.main(ActivityThread.java:4370)
atjava.lang.reflect.Method.invokeNative(Native Method)
atjava.lang.reflect.Method.invoke(Method.java:521)
atcom.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
atcom.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
atdalvik.system.NativeStart.main(Native Method)

关于网络连接,再设计的时候可以设置个timeout的时间或者放入独立的线程来处理。

关于Handler的问题,可以参考:http://developer.android.com/reference/android/os/Handler.html

案例3

关键词:Memoryleak/Thread leak

11-1621:41:42.560 I/ActivityManager( 1190): ANR in process:android.process.acore (last in android.process.acore)
11-1621:41:42.560 I/ActivityManager( 1190): Annotation:keyDispatchingTimedOut
11-16 21:41:42.560 I/ActivityManager(1190): CPU usage:
11-16 21:41:42.560 I/ActivityManager( 1190):Load: 11.5 / 11.1 / 11.09
11-16 21:41:42.560 I/ActivityManager(1190): CPU usage from 9046ms to 4018ms ago:
11-16 21:41:42.560I/ActivityManager( 1190): 
d.process.acore:98%= 97% user + 0% kernel / faults: 1134 minor
11-16 21:41:42.560I/ActivityManager( 1190): system_server: 0% = 0% user + 0% kernel /faults: 1 minor
11-16 21:41:42.560 I/ActivityManager( 1190): adbd:0% = 0% user + 0% kernel
11-16 21:41:42.560 I/ActivityManager(1190): logcat: 0% = 0% user + 0% kernel
11-16 21:41:42.560I/ActivityManager( 1190): 
TOTAL:100% = 98% user + 1% kernel

Cmdline: android.process.acore

DALVIK THREADS:
"main"prio=5 tid=3 
VMWAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8
| sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376
atdalvik.system.
VMRuntime.trackExternalAllocation(NativeMethod)
atandroid.graphics.Bitmap.nativeCreate(Native Method)
atandroid.graphics.
Bitmap.createBitmap(Bitmap.java:468)
atandroid.view.View.buildDrawingCache(View.java:6324)
atandroid.view.View.getDrawingCache(View.java:6178)
atandroid.view.ViewGroup.drawChild(ViewGroup.java:1541)
……
atcom.android.internal.policy.impl.PhoneWindow$DecorView.draw(PhoneWindow.java:1830)
atandroid.view.ViewRoot.draw(ViewRoot.java:1349)
atandroid.view.ViewRoot.performTraversals(ViewRoot.java:1114)
atandroid.view.ViewRoot.handleMessage(ViewRoot.java:1633)
atandroid.os.Handler.dispatchMessage(Handler.java:99)
atandroid.os.Looper.loop(Looper.java:123)
atandroid.app.ActivityThread.main(ActivityThread.java:4370)
atjava.lang.reflect.Method.invokeNative(Native Method)
atjava.lang.reflect.Method.invoke(Method.java:521)
atcom.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
atcom.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
atdalvik.system.NativeStart.main(Native Method)

"Thread-408"prio=5 tid=329 WAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x46910d40self=0xcd0548
| sysTid=10602 nice=0 sched=0/0 cgrp=unknownhandle=15470792
at java.lang.Object.wait(Native Method)
-waiting on <0x468cd420> (a java.lang.Object)
atjava.lang.Object.wait(Object.java:288)
atcom.android.dialer.CallLogContentHelper$UiUpdaterExecutor$1.run(CallLogContentHelper.java:289)
atjava.lang.Thread.run(Thread.java:1096)

分析:

atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod)内存不足导致block在创建bitmap

**MEMINFO in pid 1360 [android.process.acore] **
native dalvik other total
size: 17036 
23111 N/A 40147
allocated: 16484 20675 N/A 37159
free: 296 2436 N/A 2732

解决:如果机器的内存族,可以修改虚拟机的内存为36M或更大,不过最好是复查代码,查看哪些内存没有释放

  1. 首先分析log

  2. trace.txt文件查看调用stack.

  3. 看代码

  4. 仔细查看ANR的成因(iowait?block?memoryleak?

  1. $chmod 777 /data/anr

  2. $rm /data/anr/traces.txt

  3. $ps

  4. $kill -3 PID

  5. adbpull data/anr/traces.txt ./mytraces.txt

  1. Activity:onCreate(), onResume(), onDestroy(), onKeyDown(), onClick(),etc

  2. AsyncTask: onPreExecute(), onProgressUpdate(), onPostExecute(), onCancel,etc

  3. Mainthread handler: handleMessage(), post*(runnable r), etc

  4. other

  1. UI线程尽量只做跟UI相关的工作

  2. 耗时的工作(比如数据库操作,I/O,连接网络或者别的有可能阻碍UI线程的操作)把它放入单独的线程处理

  3. 尽量用Handler来处理UIthread和别的thread之间的交互

  1. KeyDispatchTimeout(5 seconds) --主要类型     按键或触摸事件在特定时间内无响应

  2. BroadcastTimeout(10 seconds)                      BroadcastReceiver在特定时间内无法处理完成

  3. ServiceTimeout(20 seconds) --小概率类型        Service在特定的时间内无法处理完成


转载于:https://my.oschina.net/yaly/blog/402973

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值