2018-06-19 19:59:56 Awenyini 阅读数 1363
  • Android核心组件 ListView 详解

    在Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。    ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。   另外ListView还有一个非常神奇的功能,我相信大家应该都体验过,即使在ListView中加载非常非常多的数据,比如达到成百上千条甚至更多, ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。那么ListView是怎么实现这么神奇的功能的呢?    下面我们来学习它,了解了它的基本工作原理,在感叹Google大神能够写出如此精妙代码的同时我也有所敬畏. 1,ListView 介绍; 2,原理讲解; 3,简单实现; 4,ListView扩展;

    16104 人正在学习 去看看 Frank Lee

最近在玩Android看到一篇文章一种极低成本的Android屏幕适配方式。细细阅读发现,其适配原理主要是根据dp/sp与px的转换,而dp/sp与px的转换又与DisplayMetrics的density相关,所以可以通过改变DisplayMetrics的density,scaledDensity和densityDpi的值来适配不同分辨率机型。这其中是怎么做到的呢,本篇博文将会从源码的角度来分析。

在开始分析之前,我们需要了解一些概念,如:

  • DisplayMetrics:是Android屏幕显示的信息描述,如尺寸size,密度desity,字体缩放值font scaling
  • Resources:是应用的资源管理类(High level api),可以调用应用中的color,string,drawable等资源
  • AssetManager:是可以直接访问应用的raw资源(Low level api)
  • BitmapFactory: Bitmap工厂类,可以根据多种来源创建Bitmap。

在说Android适配原理之前,我们先来了解一些基础概念。

一、基础概念

1. dip/dp

是Density independent pixel的缩写,指的是抽象意义上的像素。跟设备的屏幕密度有关系。它是Android里的一个单位,dip和dp是一样的。

2. sp

scale-independent pixel,安卓开发用的字体大小单位。

3.px

想像把屏幕放大再放大,对!看到的那一个个小点或者小方块就是像素了。

4.dpi

是dot per inch的缩写,就是每英寸的像素数,也叫做屏幕密度。这个值越大,屏幕就越清晰。iPhone5S的dpi是326; Samsung Note3 的dpi是386

5.分辨率

是指屏幕上垂直方向和水平方向上的像素个数。比如iPhone5S的分辨率是1136*640;Samsung Note3的分辨率是1920*1080;

6.屏幕尺寸(screen size)

就是我们平常讲的手机屏幕大小,是屏幕的对角线长度,一般讲的大小单位都是英寸。在api版本13之前(3.2),屏幕被分成四大组:small,normal,large,xlarge。但是在13往后,可以支持更加精确的屏幕区分:sw600dp,sw720dp,w600dp等。

二、dp/sp与px转换原理分析

转换公式

源码路径:frameworks/base/core/java/android/util/TypedValue.java

    public static float applyDimension(int unit, float value,
                                       DisplayMetrics metrics)
    {
        switch (unit) {
        case COMPLEX_UNIT_PX:
            return value;
        case COMPLEX_UNIT_DIP://1.dp转换为px
            return value * metrics.density;
        case COMPLEX_UNIT_SP://2.sp转换为px
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);
        case COMPLEX_UNIT_IN:
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM:
            return value * metrics.xdpi * (1.0f/25.4f);
        }
        return 0;
    }

由上知px = dp * metrics.density和px = sp * metrics.scaledDensity。根据google官方建议,我们主要都是用dp和sp,而这两个单位,最后都会转化为px(像素),在不同的设备中,决定px转化的大小是metrics.density和metrics.scaledDensity,所以这里我们具体来看看这两个变量,具体我们来看看DisplayMetrics源码:

源码路径:frameworks/base/core/java/android/util/DisplayMetrics.java


    public class DisplayMetrics {

    public static final int DENSITY_LOW = 120;

    public static final int DENSITY_MEDIUM = 160;

    public static final int DENSITY_TV = 213;

    public static final int DENSITY_HIGH = 240;

    public static final int DENSITY_280 = 280;

    public static final int DENSITY_XHIGH = 320;

    public static final int DENSITY_360 = 360;

    public static final int DENSITY_400 = 400;

    public static final int DENSITY_420 = 420;

    public static final int DENSITY_XXHIGH = 480;

    public static final int DENSITY_560 = 560;

    public static final int DENSITY_XXXHIGH = 640;

    public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;

    public static final float DENSITY_DEFAULT_SCALE = 1.0f / DENSITY_DEFAULT;

    /**
     * The device's density.
     * @hide because eventually this should be able to change while
     * running, so shouldn't be a constant.
     * @deprecated There is no longer a static density; you can find the
     * density for a display in {@link #densityDpi}.
     */
    @Deprecated
    public static int DENSITY_DEVICE = getDeviceDensity();

    /**
     * The absolute width of the display in pixels.
     */
    public int widthPixels;
    /**
     * The absolute height of the display in pixels.
     */
    public int heightPixels;
    /**
     * The logical density of the display.  This is a scaling factor for the
     * Density Independent Pixel unit, where one DIP is one pixel on an
     * approximately 160 dpi screen (for example a 240x320, 1.5"x2" screen), 
     * providing the baseline of the system's display. Thus on a 160dpi screen 
     * this density value will be 1; on a 120 dpi screen it would be .75; etc.
     *  
     * <p>This value does not exactly follow the real screen size (as given by 
     * {@link #xdpi} and {@link #ydpi}, but rather is used to scale the size of
     * the overall UI in steps based on gross changes in the display dpi.  For 
     * example, a 240x320 screen will have a density of 1 even if its width is 
     * 1.8", 1.3", etc. However, if the screen resolution is increased to 
     * 320x480 but the screen size remained 1.5"x2" then the density would be 
     * increased (probably to 1.5).
     *
     * @see #DENSITY_DEFAULT
     */
    public float density;
    /**
     * The screen density expressed as dots-per-inch.  May be either
     * {@link #DENSITY_LOW}, {@link #DENSITY_MEDIUM}, or {@link #DENSITY_HIGH}.
     */
    public int densityDpi;
    /**
     * A scaling factor for fonts displayed on the display.  This is the same
     * as {@link #density}, except that it may be adjusted in smaller
     * increments at runtime based on a user preference for the font size.
     */
    public float scaledDensity;

    ......

    public void setToDefaults() {
        widthPixels = 0;
        heightPixels = 0;
        density =  DENSITY_DEVICE / (float) DENSITY_DEFAULT;//1.desity的赋值
        densityDpi =  DENSITY_DEVICE;
        scaledDensity = density;
        xdpi = DENSITY_DEVICE;
        ydpi = DENSITY_DEVICE;
        ......
    }

    ......

    private static int getDeviceDensity() {
        // qemu.sf.lcd_density can be used to override ro.sf.lcd_density
        // when running in the emulator, allowing for dynamic configurations.
        // The reason for this is that ro.sf.lcd_density is write-once and is
        // set by the init process when it parses build.prop before anything else.
        return SystemProperties.getInt("qemu.sf.lcd_density",
                SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
    }
}

由上面的代码知,默认情况下,metrics.density和metrics.scaledDensity是相等的,并且有metrics.density = DENSITY_DEVICE / (float) DENSITY_DEFAULT,其中DENSITY_DEVICE = getDeviceDensity(),DENSITY_DEFAULT = DENSITY_MEDIUM = 160,我们来看看一下获取设备Density方法getDeviceDensity():

    private static int getDeviceDensity() {
        // qemu.sf.lcd_density can be used to override ro.sf.lcd_density
        // when running in the emulator, allowing for dynamic configurations.
        // The reason for this is that ro.sf.lcd_density is write-once and is
        // set by the init process when it parses build.prop before anything else.
        return SystemProperties.getInt("qemu.sf.lcd_density",
                SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
    }

此方法通过调用原生方法SystemProperties.getInt(“qemu.sf.lcd_density”,SystemProperties.getInt(“ro.sf.lcd_density”, DENSITY_DEFAULT))从而获得设备Density,通过研究分析知,这里是调用底层C的代码,我们继续来看:

源码路径:android\external\qemu\android文件夹下的hw-lcd.c和hw-lcd.h


void hwLcd_setBootProperty(int density)
{
    char  temp[8];

    /* Map density to one of our five bucket values.
       The TV density is a bit particular (and not actually a bucket
       value) so we do only exact match on it.
    */
    if (density != LCD_DENSITY_TVDPI) {
        if (density < (LCD_DENSITY_LDPI + LCD_DENSITY_MDPI)/2)
            density = LCD_DENSITY_LDPI;
        else if (density < (LCD_DENSITY_MDPI + LCD_DENSITY_HDPI)/2)
            density = LCD_DENSITY_MDPI;
        else if (density < (LCD_DENSITY_HDPI + LCD_DENSITY_XHDPI)/2)
            density = LCD_DENSITY_HDPI;
        else
            density = LCD_DENSITY_XHDPI;
    }
    snprintf(temp, sizeof temp, "%d", density);
    boot_property_add("qemu.sf.lcd_density", temp);
}

此方法主要就是向设备添加参数为”qemu.sf.lcd_density”的值,然后通过SystemProperties.getInt(“qemu.sf.lcd_density”,”“),就可以获取到此值。通过此方法,设备会返回系统规定好的值,其中LCD_DENSITY_LDPI为120,LCD_DENSITY_MDPI为160,LCD_DENSITY_HDPI为240,LCD_DENSITY_XHDPI为320等,通过ppi公式算出的值desityDpi不是最终的desityDpi,为了统一,为了drawable-ldpi,drawable-mdpi,drawable-hdpi,drawable-xhdpi,drawable-xxhdpi等图片资源获取,这里系统做了一下处理,以保证不同的设备返回的值在相对应的区间范围。

三、图片资源加载原理分析

加载本地资源图片方法有getDrawable()和decodeResource(Resources res, int id),我们先来分析第一个方法,我们知道getDrawable()是Resources类中的方法,所以我们来看看此类

1.getDrawable()方法
源码路径:frameworks/base/core/java/android/content/res/Resource.java

public class Resources {

    public Drawable getDrawable(int id) throws NotFoundException {
        synchronized (mTmpValue) {
            TypedValue value = mTmpValue;
            getValue(id, value, true);
            return loadDrawable(value, id);
        }
    }

 Drawable loadDrawable(TypedValue value, int id)
            throws NotFoundException {

                ......

                if (file.endsWith(".xml")) {//xml中获取图片
                    try {
                        XmlResourceParser rp = loadXmlResourceParser(
                                file, id, value.assetCookie, "drawable");
                        dr = Drawable.createFromXml(this, rp);
                        rp.close();
                    } catch (Exception e) {
                        NotFoundException rnf = new NotFoundException(
                            "File " + file + " from drawable resource ID #0x"
                            + Integer.toHexString(id));
                        rnf.initCause(e);
                        throw rnf;
                    }

                } else {//代码中获取图片
                    try {
                        InputStream is = mAssets.openNonAsset(
                                value.assetCookie, file, AssetManager.ACCESS_STREAMING);
        //                System.out.println("Opened file " + file + ": " + is);
                        dr = Drawable.createFromResourceStream(this, value, is,
                                file, null);//核心代码
                        is.close();
        //                System.out.println("Created stream: " + dr);
                    } catch (Exception e) {
                        NotFoundException rnf = new NotFoundException(
                            "File " + file + " from drawable resource ID #0x"
                            + Integer.toHexString(id));
                        rnf.initCause(e);
                        throw rnf;
                    }
                }
            }
        }
        ......

        return dr;
    }
}

我们主要来看从代码中获取图片,我们继续来看看核心代码Drawable.createFromResourceStream(this, value, is,file, null):

    public static Drawable createFromResourceStream(Resources res, TypedValue value,
            InputStream is, String srcName, BitmapFactory.Options opts) {
        .......
        if (opts == null) opts = new BitmapFactory.Options();
        opts.inScreenDensity = res != null
                ? res.getDisplayMetrics().noncompatDensityDpi : DisplayMetrics.DENSITY_DEVICE;
        Bitmap  bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);//核心代码
        if (bm != null) {
            byte[] np = bm.getNinePatchChunk();
            if (np == null || !NinePatch.isNinePatchChunk(np)) {
                np = null;
                pad = null;
            }

            final Rect opticalInsets = new Rect();
            bm.getOpticalInsets(opticalInsets);
            return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
        }
        return null;
    }

由上我们,继续来看看 BitmapFactory.decodeResourceStream(res, value, is, pad, opts)方法:

    public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options opts) {

        if (opts == null) {
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }

        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;//Android设备的densityDpi
        }

        return decodeStream(is, pad, opts);
    }

    public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
        // we don't throw in this case, thus allowing the caller to only check
        // the cache, and not force the image to be decoded.
        if (is == null) {
            return null;
        }

        Bitmap bm = null;

        Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
        try {
            if (is instanceof AssetManager.AssetInputStream) {
                final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
                bm = nativeDecodeAsset(asset, outPadding, opts);
            } else {
                bm = decodeStreamInternal(is, outPadding, opts);
            }

            if (bm == null && opts != null && opts.inBitmap != null) {
                throw new IllegalArgumentException("Problem decoding into existing bitmap");
            }
            setDensityFromOptions(bm, opts);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
        }

        return bm;
    }

由上知,当传入Android设备的相关密度后,最后调用了原生方法nativeDecodeAsset()从而获取本地相关资源图片。

2.decodeResource(Resources res, int id)方法
此方法主要是BitmapFactory中的方法,所以我们主要来看此类


    public static Bitmap decodeResource(Resources res, int id) {
        return decodeResource(res, id, null);
    }

    public static Bitmap decodeResource(Resources res, int id, Options opts) {
        Bitmap bm = null;
        InputStream is = null; 

        try {
            final TypedValue value = new TypedValue();
            is = res.openRawResource(id, value);

            bm = decodeResourceStream(res, value, is, null, opts);//核心代码
        } catch (Exception e) {
            /*  do nothing.
                If the exception happened on open, bm will be null.
                If it happened on close, bm is still valid.
            */
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                // Ignore
            }
        }

        if (bm == null && opts != null && opts.inBitmap != null) {
            throw new IllegalArgumentException("Problem decoding into existing bitmap");
        }

        return bm;
    }

   public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options opts) {

        if (opts == null) {
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }

        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }

        return decodeStream(is, pad, opts);
    }

由上易发现,最后也都是调用了原生方法nativeDecodeAsset()从而获取本地相关资源图片。

上面两种方法获取应用资源图片,其中都传入了Android的densityDpi密度值,然后再通过原生返回相关图片。为什么要传入Android设备的密度值,因为为了适配多个屏幕,这里就涉及到了图片资源的缩放。我们知道Android项目有多个图片文件夹,如drawable-ldpi,drawable-mdpi,drawable-hdpi,drawable-xhdpi,drawable-xxhdpi等,其对应的设备密度为120,160,240,320,480等。

通过实际Demo测试,一张分辨率为60x60的图片,如果放在drawable-xhdpi中,测试机的密度值为480,在其测试机上显示的图片分辨率为90x90,其缩放比值为480/320=1.5;如果在测试机密度为240,在其测试机上的显示图片分辨率为45x45,其缩放比值为240/320 = 0.75;

由此我们知道,不同文件夹下的图片,在高密度的手机上是放大,在低密度的手机上是缩小

最后附上一种极低成本的Android屏幕适配方式解决方式的源码:

/**
 * Describe: 屏幕适配方案
 *
 * 1.设计以1080*1920(px)为标准,换成dp为360*640(dp)
 * 2.其他分辨率按宽为360dp为标准,density = displayWidth/360,保证所有机型宽都能铺满屏幕
 *
 * Created by AwenZeng on 2018/6/14.
 */
public class AutoScreenUtils {

    private static float originalScaledDensity;

    private static final int DEFAULT_STANDARD = 360;//默认标准

    public static void AdjustDensity(final Application application) {
        final DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
        final float originalDensity = displayMetrics.density;
        originalScaledDensity = displayMetrics.scaledDensity;
        application.registerComponentCallbacks(new ComponentCallbacks() {
            @Override
            public void onConfigurationChanged(Configuration newConfig) {
                if (newConfig != null && newConfig.fontScale > 0) {
                    originalScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                }
            }
            @Override
            public void onLowMemory() {
            }
        });

        float targetDensity = (float)displayMetrics.widthPixels / DEFAULT_STANDARD;
        float targetScaledDensity = targetDensity * (originalScaledDensity / originalDensity);
        int targetDensityDpi = (int) (160 * targetDensity);
        displayMetrics.density = targetDensity;
        displayMetrics.scaledDensity = targetScaledDensity;
        displayMetrics.densityDpi = targetDensityDpi;

        DisplayMetrics activityDisplayMetrics = application.getResources().getDisplayMetrics();
        activityDisplayMetrics.density = targetDensity;
        activityDisplayMetrics.scaledDensity = targetScaledDensity;
        activityDisplayMetrics.densityDpi = targetDensityDpi;

        application.registerActivityLifecycleCallbacks(new CreateActivityLifecycle() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                float targetDensity = (float)displayMetrics.widthPixels / DEFAULT_STANDARD;
                float targetScaledDensity = targetDensity * (originalScaledDensity / originalDensity);
                int targetDensityDpi = (int) (160 * targetDensity);
                displayMetrics.density = targetDensity;
                displayMetrics.scaledDensity = targetScaledDensity;
                displayMetrics.densityDpi = targetDensityDpi;

                DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
                activityDisplayMetrics.density = targetDensity;
                activityDisplayMetrics.scaledDensity = targetScaledDensity;
                activityDisplayMetrics.densityDpi = targetDensityDpi;
            }
        });

    }

    private static abstract class CreateActivityLifecycle implements Application.ActivityLifecycleCallbacks {

        @Override
        public void onActivityStarted(Activity activity) {

        }

        @Override
        public void onActivityResumed(Activity activity) {

        }

        @Override
        public void onActivityPaused(Activity activity) {

        }

        @Override
        public void onActivityStopped(Activity activity) {

        }

        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

        }

        @Override
        public void onActivityDestroyed(Activity activity) {

        }
    }

}

注:源码采用android-4.1.1_r1版本,建议下载源码然后自己走一遍流程,这样更能加深理解。

四、参考文档

一种极低成本的Android屏幕适配方式

dpi 、 dip 、分辨率、屏幕尺寸、px、density 关系以及换算

UI之支持多屏幕

Android屏幕适配及DisplayMetrics解析

2016-05-05 11:00:09 crasheye 阅读数 620
  • Android核心组件 ListView 详解

    在Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。    ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。   另外ListView还有一个非常神奇的功能,我相信大家应该都体验过,即使在ListView中加载非常非常多的数据,比如达到成百上千条甚至更多, ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。那么ListView是怎么实现这么神奇的功能的呢?    下面我们来学习它,了解了它的基本工作原理,在感叹Google大神能够写出如此精妙代码的同时我也有所敬畏. 1,ListView 介绍; 2,原理讲解; 3,简单实现; 4,ListView扩展;

    16104 人正在学习 去看看 Frank Lee

给大家介绍下AndroidonTouchEven的实现原理以及调用流程

android Activity或View 里有 onTouchEvent() 事件,当我们对手机的屏幕进行操作,如点击、移动、抬起,都会触发 onTouchEvent 事件


一、onTouchEvent事件常用的几种操作

onTouchEvent事件常用的几种操作,可使用 switch(event.getAction() & MotionEvent.ACTION_MASK)进行区分:

@Override
public boolean onTouchEvent(MotionEvent event)
{
    switch(event.getAction() & MotionEvent.ACTION_MASK)
    {
        case MotionEvent.ACTION_DOWN: 
        {
            // 屏幕接到单点触碰事件
            // do someting
            break;
        }
        case MotionEvent.ACTION_POINTER_DOWN:
        {
            // 当屏幕接收到屏幕1点以上(多点)触碰,都会响应到此事件
            // do smoeting
            break;
        }
        case MotionEvent.ACTION_UP:
        {
            // 屏幕点击的最后一个点都抬起,就会触发此事件
            // do smoeting
            break;
        }
        case MotionEvent.ACTION_POINTER_UP:
        {
            // 抬起后还有多于1个点在屏幕的都会触发此事件
            // do smoeting
            break;
        }
        case MotionEvent.ACTION_MOVE:
        {
            // 所有的接触点移动都会触发此事件
            // do smoeting
            break;
        }
    }
}


二、多点触控使用 event.getAction() & MotionEvent.ACTION_MASK 原因:

我们首先来看看MotionEvent下的一些常量值:

ACTION_MASK = 0xff;

ACTION_DOWN = 0x0;

ACTION_UP = 0x1;

ACTION_MOVE = 0x2;

ACTION_POINTER_DOWN = 0x5;

ACTION_POINTER_UP = 0x6;

// 以下属性在api level 8(即android 2.2已经过时,不再使用
//
已改用上面的 ACTION_POINTER_DOWN ACTION_POINTER_UP

ACTION_POINTER_1_DOWN = ACTION_POINTER_DOWN |0x0000;  

ACTION_POINTER_2_DOWN = ACTION_POINTER_DOWN | 0x0100;

ACTION_POINTER_3_DOWN = ACTION_POINTER_DOWN | 0x0200;

ACTION_POINTER_1_UP  = ACTION_POINTER_UP | 0x0000;

ACTION_POINTER_2_UP  = ACTION_POINTER_UP | 0x0100;

ACTION_POINTER_3_UP  = ACTION_POINTER_UP | 0x0200;

例如:ACTION_MASK &ACTION_POINTER_2_DOWN 即: 0xff & 0x05 =5; 所以触发的是ACTION_POINTER_DOWN件事。

 

三、当我们触控屏幕的时候会发现什么事呢?

我们可以通过adb查看我们对屏幕所做的一切记录,下面我们就说说如何解读even的操作。

adb 查看event可以查看到所有关于此手机的一些操作,但我们只关注关注屏幕操作的一些事件,可以通过 adb shell getevent -p 查看信息,看到像下面的字样,这样你就可以找到屏幕操作在那个事件里:

// 当你看到有像以下这样的字段,那么就证明你的屏幕操作事件在event3

add device 5: /dev/input/event3

  name:     "mtk-tpd"

  events:

    KEY (0001):008b  009e  00ac 0145  0148  014a 014d  014e

               014f

    ABS (0003):0000  : value 0, min 0, max 1080, fuzz 0,flat 0, resolution 1080

               0001  : value 0, min 0, max 1920,fuzz 0, flat 0, resolution 1920

               0018  : value 0, min 0, max 255,fuzz 0, flat 0, resolution 0

               002f  : value 0, min 0, max 9,fuzz 0, flat 0, resolution 0

               0030  : value 0, min 0, max 100,fuzz 0, flat 0, resolution 0

               0031  : value 0, min 0, max 100,fuzz 0, flat 0, resolution 0

               0035  : value 0, min 0, max 1080,fuzz 0, flat 0, resolution 0

               0036  : value 0, min 0, max 1920,fuzz 0, flat 0, resolution 0

               0039  : value 0, min 0, max 65535,fuzz 0, flat 0, resolution 0

               003a  : value 0, min 0, max 255,fuzz 0, flat 0, resolution 0

  input props:

   INPUT_PROP_POINTER

   INPUT_PROP_DIRECT

那么我们就可以去查看对应的事件内容了。

使用adb shell getevent/dev/input/event3,这时候你试下点击一下手机屏幕,你就会看到会刷新出类型下面的内容:

130|shell@hermes:/ $ getevent /dev/input/event3

0003 0039 00000b25

0003 003a 00000052

0003 0030 00000003

0003 0035 00000385

0003 0036 00000437

0001 014a 00000001

0000 0000 00000000

0003 003a 00000051

0000 0000 00000000

0003 0039 ffffffff

0001 014a 00000000

0000 0000 00000000

是不是看着有点晕,下来我们更改一下命令:

130|shell@hermes:/ $ getevent -l /dev/input/event3

EV_ABS      ABS_MT_TRACKING_ID   00000b21

EV_ABS      ABS_MT_PRESSURE      0000007f

EV_ABS       ABS_MT_TOUCH_MAJOR   00000006

EV_ABS      ABS_MT_POSITION_X    0000038f   // x坐标

EV_ABS      ABS_MT_POSITION_Y    00000421  // y坐标

EV_KEY      BTN_TOUCH            DOWN    // MotionEvent.ACTION_DOWN事件触发

EV_SYN      SYN_REPORT           00000000    //同步事件

EV_ABS      ABS_MT_PRESSURE      0000007e

EV_SYN      SYN_REPORT           00000000//同步事件

EV_ABS      ABS_MT_TRACKING_ID   ffffffff

EV_KEY      BTN_TOUCH            UP   // MotionEvent.ACTION_UP事件触发

EV_SYN      SYN_REPORT           00000000 //同步事件

这次好看多了吧!

接着我们来说说上面的一些关键字是什么意思:

EV_SYN[0000] :同步事件

EV_KEY[0001] :按键事件

EV_ABS[0003] :绝对值事件

ABS_MT_TOUCH_MAJOR[0030] :接触面长轴值  {有效值范围[0, 255]}

ABS_MT_POSITION_X[0035]: (x轴坐标) {有效值范围[0, 1080]} (MAX根据屏幕长宽决定,在getevent -p 里能看到)

ABS_MT_POSITION_Y [0036]: (y轴坐标) {有效值范围[0, 1920]}

有了上面的了解,我们是否也可以通过adb进行屏幕的一些操作呢?答案是肯定的。

 

四、通过adb模拟滑动、按键、点击事件:

在进入adb模式下输入input,则可以看到以下命令的说明:

The commands and default sources are:

      text<string> (Default: touchscreen)

      keyevent[--longpress] <key code number or name> ... (Default: keyboard)

      tap <x><y> (Default: touchscreen)

      swipe<x1> <y1> <x2> <y2> [duration(ms)] (Default:touchscreen)

      press(Default: trackball)

      roll<dx> <dy> (Default: trackball)

 

1、 text 在手机的搜索输入框中输入关键字进行搜索,如:需要搜索aaa相关的所有内容:

    adb shell input text aaa 

2、keyevent 手机的按键操作,如:home键、返回键、菜单键、锁屏等。

    例如你需要点击一下home键,那么你可以这么输入:

    adb shell input keyevent 3  or adbshell input keyevent KEYCODE_HOME 

3、tap 是模拟touch屏幕的事件,以下命令就是点击屏幕300,300位置:

    adb shell input tap 300 300 

4、swipe 是滑动事件,以下命令表示从屏幕300,300 移动到  400,400

    adb shell input swipe 300 300 400400  

5、press 按下滚球(现在已经没用了,因为现在手机都没有那个滚球了)

    adb shell input trackball press 

6、roll 移动滚球(现在也已经没用了)

    adb shell input trackball roll <dx><dy>

2013-09-05 18:54:30 tanqiantot 阅读数 19484
  • Android核心组件 ListView 详解

    在Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。    ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。   另外ListView还有一个非常神奇的功能,我相信大家应该都体验过,即使在ListView中加载非常非常多的数据,比如达到成百上千条甚至更多, ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。那么ListView是怎么实现这么神奇的功能的呢?    下面我们来学习它,了解了它的基本工作原理,在感叹Google大神能够写出如此精妙代码的同时我也有所敬畏. 1,ListView 介绍; 2,原理讲解; 3,简单实现; 4,ListView扩展;

    16104 人正在学习 去看看 Frank Lee
大纲
1、android中res目录下的文件夹所对应屏幕尺寸
2、用dp表示尺寸的原理
3、对于720p尺寸的适配需要注意什么
4、ui切图按什么尺寸给最好
参考:http://www.360doc.com/content/12/0301/17/5087210_190881395.shtml

几个概念:
1) 屏幕密度(dpi) :dot per inch,即每英寸像素数。
ldpi(120),mdpi(160),hdpi(240),xhdpi(320)
计算方法:
以480x854,4.0inch手机为例,其对角线为4.0inch,对角线的像素数为:(480^2 + 854^2)开根号 = 979.
所以其dpi = 979 / 4 = 245,约为240
2)屏幕尺寸:对角线长度。有small,normal,large,extra large

3)方向:横屏和竖屏

4)分辨率:一个物理屏幕上总的像素点数,如480x800等。我们应用中并不使用分辨率这个概念,主要是dpi和尺寸

5)dp(density-independent pixel)独立像素单位。一个抽象概念,用来定位UI布局,包括尺寸和位置。



1、可以在android工程目录res下有四个文件夹,主要是为了支持多分辨率的图片
drawable-hdpi
drawable-mdpi
drawable-ldpi
drawable-xhdpi



当设计给出切图时,我们首先需要明确一点,设计给出的切图是在什么尺寸下给出的。如果是480x800的切图,则应该放入drawable-hdpi目录下,如果是320x480的切图,则应该放在mdpi目录下。如果是720x1280的切图,则应该放在xhdpi目录下。

当使用该图片时,系统会根据机器的分辨率到相应的文件夹下查找图片。

问题1:如果只放一套图片,系统如何对图片的分辨率进行转换?
android系统加载图片资源遵循下面的规则:首先判断手机的屏幕密度,然后在相应的密度下文件夹中查找图片资源。如果找到,则进行显示。如果找不到,则会从drawable或者其他drawable-*文件夹中寻找。找到后,将文件夹所表示的密度与手机实际密度进行比较,从而缩放图片。例如在drawable下找到资源(等价于从drawable-mdpi),而手机又是hdpi的,这样android在显示图片时会将图片进行放大,以满足hdpi手机显示需要。不难想象,图片肯定会变模糊。

系统加载图片前先将图片进行缩放,因此你通过getwidth得到的尺寸已经是经过缩放的尺寸了。缩放比例与密度之比保持一致。

问题2:如果切图放错文件夹会有什么问题?
如果放错文件夹,系统会进行相应的缩放,使图片进行错误的缩放。例如hdpi的图片放到mdpi中,在hdpi的手机显示,会比原图片大

问题3:720p图片相关问题?

设计给切图,最好根据那个dpi呢?是都可以吗?一般应该给480*800的hdpi的切图。这样对于mdpi,可以自动缩小。图像显示质量不受影响。对于xdpi,放大幅度不大,图像显示质量影响较小。

那么为什么不用xhdpi呢,这样都是缩小图像不是更好吗?有两个理由,一是增大了apk的尺寸,另一个就是容易引起切图的变形,影响显示。主要是对于.9的图片,有对某一方向进行了压缩,如果设置时,拉伸区域太小,再压缩时可能变形。当然这也不是绝对的,可以通过增大.9.png文件的拉伸区域来实现。
或许还有个理由,xhpi的机型比较不占多数。

2、dp工作原理

     最关键要记住一个公式:
     android在计算pixel值时会首先判断屏幕的密度。如果我们把mdpi设为1,则hdpi就等于1.5. 如果我们使用dp为单位,android在转换为pixel时会依据下面公式:

     px = dp * density;


    dp表示独立像素密度,所谓独立,就是不依赖与屏幕的密度。
     从本质上来看,dp其实表示是相同的物理长度,注:不是以像素为单位,而是以inch为单位。
     例如对于mdpi,长度为m inch的屏幕,其像素数为n1,dp数为n2. 对于hdpi,也是长度为m inch,包含的像素数为n1 * 1.5. 根据公式可以算出,其dp值为n1 * 1.5 / 1.5 = n1. 对于xhdpi的也是n1.

     即:相同的物理长度其dp值相同。


     
从表象上看,dp可以表示一个相同的比例
也就是在ldpi、mdpi、hdpi和xdpi的屏幕密度中,用同一个dp值,可以表示相同的比例。

     请注意:是比例而不是尺寸。因为UI设计中,我们最关心的是一个视图相对于整个屏幕的比例,例如:在mdpi中,某TextView的长度占屏幕宽度的1/2,假设屏幕的宽度像素为320pixel,那么TextView为160pixel。而在hdpi中,我们需要该TextView也占到屏幕的1/2,才能称之为适应了不同的屏幕密度。为了实现这个目标,Android提供了dp的概念。

     我们把dip为160(mdpi)的屏幕与dp一一对应起来,即mdpi屏幕中,1个pixel就等于1个dp。通常,mdpi的屏幕宽度为320pixel,所以其屏幕宽度为320dp。

     那么在hdpi屏幕中,其宽度通常为480pixel。其1/2为240pixel。如果用pixel表示,我们就需要使用两个值(mdpi:160px,hdpi:240px),而我们又只能输入1个值。这个问题该如何解决?



     这样对于mdpi,160dp对应的px为160*1 = 160,为屏幕宽度320px的一半。而对于hdpi,160dp对应的px为160*1.5 = 240px,也为屏幕宽度480px的一半。这样,我们用一个值:160dp,实际上表示了两个像素:160px和240dp。

     再进一步分析:由于相同长度下(1inch),hdpi和mdpi的像素数比值为一个定值(1.5),那么hdpi和mdpi都截取相同的比例(n/m),其比值还是1.5.这时,我们把mdpi的一个像素看成单位1,则hdpi就是1.5.也就是说要表示相同的比例,那么hdpi和mdpi的像素比值必须为1.5.现在,我们让android操作系统替我们记录下这个比值,我们就用以mdpi为单位1进行计数(重新创建一个单位:dp)。当使用时,我们只写出dp值,然后由操作系统自动乘上这个比值。这样,1个dp表示的数值可以表示一个标准的比例了。

     简单的说:其实dp表示的是一个比例。在mdpi中1dp表示1/320, 在hdpi中1dp也表示1/320.由android操作系统利用density在后台重新计算保证了相同的dp值能够表示相同的比例。

     注:上述说法也是有限制的。dp表示比例还必须要有一个前提,那就是屏幕的尺寸和密度必须满足下面的条件:
mdpi:3.5inch,320*480, hdpi:4.0inch,480*800或480*854,xhdpi:4.5inch,720*1280.即使是这样,也只能保证mdpi和hdpi有相同的比例,而xhdpi则不能。以满屏宽度为例,mdpi和hdpi都是320dp,而xhdpi为360dp。

     那么既然dp也仅仅是特殊情况才能保持相同的比例,那么是不是用处就大打折扣了呢?

     要记住,android屏幕适配终究是相对的,因为尺寸太多,不可能完全适配,所以我们只适配大众的,数量最多的。现在数量最多也就是上述尺寸,其他的都是小众。因此针对上述屏幕进行ui设计,可以满足大部分手机的ui正常。

     其他尺寸的手机怎么办?
     我们应该记住这个原则:尽量少用dp定义具体宽度。

     为什么是宽度?因为我们常见的ui元素,其宽度都是不固定的。高度可以固定。如各种button。我们尽量用dp来表示间隔,而用fill_parent、wrap_parent、weight等属性来使UI进行自动填充。我们需要的是一个适当的比例,而不是精确的尺寸。


2019-08-27 19:12:24 ChaoLiangBlog 阅读数 77
  • Android核心组件 ListView 详解

    在Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。    ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。   另外ListView还有一个非常神奇的功能,我相信大家应该都体验过,即使在ListView中加载非常非常多的数据,比如达到成百上千条甚至更多, ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。那么ListView是怎么实现这么神奇的功能的呢?    下面我们来学习它,了解了它的基本工作原理,在感叹Google大神能够写出如此精妙代码的同时我也有所敬畏. 1,ListView 介绍; 2,原理讲解; 3,简单实现; 4,ListView扩展;

    16104 人正在学习 去看看 Frank Lee

Android事件分发机制是android工程师必须掌握的基础知识,网上关于事件分发的文章有很多,这里我来阐述下自己对事件分发的看法。

MotionEvent

何为事件?事件就是用户手指触摸屏幕时,产生的的一系列触摸行为。Android中将事件定义为MotionEvent,其中事件的类型又分为四种:

  • ACTION_DOWN:按下
  • ACTION_MOVE:滑动
  • ACTION_UP:抬起,对应ACTION_DOWN
  • ACTION_CANCEL:取消,非人为因素造成的事件结束

正常用户一次从触摸屏幕到离开屏幕会产生一系列事件,根据情况不同又可分为两种:

  1. 点击屏幕后立即松开,事件序列为ACTION_DOWN—>ACTION_UP。
  2. 点击屏幕滑动一段距离后松开,事件序列为ACTION_DOWN—>ACTION_MOVE—>ACTION_UP。
    在这里插入图片描述
    事件产生必然会有接收者接收事件,那么接收者是谁呢?我们一起深入源码一探究竟。
    事件分发的入口是在Activity中的dispatchTouchEvent(MotionEvent ev),这里要解释下为什么事件分发入口是在Activity中的dispatchTouchEvent(MotionEvent ev)中,下面给出一张用户触发事件图:
    在这里插入图片描述

Activity#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
		//判断当前是否为ACTION_DOWN事件
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //调用了Window的superDispatchTouchEvent
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //如果Window的superDispatchTouchEvent返回false,则继续执行onTouchEvent(ev),否则不会执行
        return onTouchEvent(ev);
    }
		
	//该方法是个空实现,当activity处于栈顶时,触屏点击按home,back,menu键等都会触发此方法
	public void onUserInteraction() {
    }


Window是个抽象类,它的唯一子类是PhoneWindow。当调用PhoneWindow的superDispatchTouchEvent(ev)返回true时,Activity#dispatchTouchEvent方法结束并返回true,否则继续执行Activity#onTouchEvent(ev)。

Phonewindow#superDispatchTouchEvent(ev)

public boolean superDispatchTouchEvent(MotionEvent event) {
		//mDecor是DecorView的实例对象,调用了DecorView#superDispatchTouchEvent
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView#superDispatchTouchEvent(MotionEvent event)

public boolean superDispatchTouchEvent(MotionEvent event) {
		//DecorView的父类是FrameLayout,故这里会调用父类(ViewGroup)的dispatchTouchEvent(event)
        return super.dispatchTouchEvent(event);
    }

通过上述源码分析可知实际上事件从Activty传入到ViewGroup中。
ViewGroup#dispatchTouchEvent(MotionEvent ev)

public boolean dispatchTouchEvent(MotionEvent ev) {
	***
	boolean handled = false//在事件分发的过程中如果某个View消费了事件,则会将handled 置为true,最后会将handled 返回。
	if (actionMasked == MotionEvent.ACTION_DOWN) {
	//当前事件为ACTION_DOWN说明是新的事件序列,在开始新事件之前丢弃之前状态。
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
     
    //取消并且清空TouchTarget,TouchTarget是一个单项链表结构的数据类,其中保存了当前触摸的
    View以及下一个TouchTarget的实例引用。此处会将mFirstTouchTarget置null。
    cancelAndClearTouchTargets(ev);
    
    //重置触摸状态  将FLAG_DISALLOW_INTERCEPT标记为取反。
    resetTouchState();
 }
            
	// Check for interception.//检查是否拦截
       final boolean intercepted;
      if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                    
         //当事件为ACTION_DOWN时,disallowIntercept 一定为false,因为resetTouchState()
           将FLAG_DISALLOW_INTERCEPT标记位进行了取反操作。
           所以这里FLAG_DISALLOW_INTERCEPT标记位可以影响除了ACTION_DOWN事件以外
           的事件,子类可以通过getParent.requestDisallowInterceptTouchEvent(boolean disallowIntercept)
           改变FLAG_DISALLOW_INTERCEPT标记位,从而来决定是让父类否拦截事件。
                
           final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
          //当为ACTION_DOWN时disallowIntercept为false,必定会调用onInterceptTouchEvent(ev),
           该方法默认是返回false,也就是不拦截事件,子类(只能是ViewGroup,只有ViewGroup才能拦截事件)
           可重写该方法。
            if (!disallowIntercept) {
               intercepted = onInterceptTouchEvent(ev);
               ev.setAction(action); // restore action in case it was changed
              } else {
                	//如果disallowIntercept为true则表示不拦截
                    intercepted = false;
                }
          } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                //没有触摸目标且当前事件不为ACTION_DOWN,则继续拦截触摸事件
                intercepted = true;
            }
			***
			TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {//没有拦截事件
				***
			if (actionMasked == MotionEvent.ACTION_DOWN
               || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
               //这里会对ACTION_DOWN事件进行特殊处理
               
                 ***
                 
              final int childrenCount = mChildrenCount;
              if (newTouchTarget == null && childrenCount != 0) {
                 for (int i = childrenCount - 1; i >= 0; i--) {
                  //这里需要解释下为什么会采用倒叙遍历子View,这是一个概率问题,一般外层View
                  消费事件的概率较内层View大,所以采用倒叙遍历的方式,这样可以减少遍历子View
                  的时间。
                final int childIndex = getAndVerifyPreorderedIndex(
                                   childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                                    
			  		              ***		
			  		             
			    if (!canViewReceivePointerEvents(child) || 
						!isTransformedTouchPointInView(x, y, child, null)) {
						
					//这里对当前子View进行了条件判断,当view不可见、view在执行动画或者当前触摸
					位置不在子View范围内时,则跳过该View继续遍历子View。
                       ev.setTargetAccessibilityFocus(false);
                       continue;
											
				if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
					//该方法会将当前时间分发给子View,稍后去看源码,接着往下走。
				    //当上述条件成立时,会执行下面代码。
				    
					                             ***
					
				   newTouchTarget = addTouchTarget(child, idBitsToAssign);//将当前子View跟
				   Touchtarget对象绑定,并将绑定的Touchtarget对象赋值给mFirstTouchTarget,
				   这里也侧面说明了如果该子View能够消费事件,那么mFirstTouchTarget就指向
				   该View,否则mFirstTouchTarget为null。
					可能这里大家心里会有疑问,问什么要将当前子View与Touchtarget绑定?原因是
					为了后续事件能够快速响应,不需要在遍历全部子View。
									
                    alreadyDispatchedToNewTouchTarget = true;//该标记位标识已经将事件分发给了
                    新的View
                    break;//跳出子View遍历循环。
							  }
                            }
                    	}
					}
						// Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
            	//mFirstTouchTarget 为空则说明没有子View消费事件,会调用dispatchTransformedTouchEvent
            	并且第三个参数传入的为null
            	
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {	//代表有子View能够消费事件
				TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                
                	//alreadyDispatchedToNewTouchTarget 是在对ACTION_DOWN事件特殊处理时赋值为true,
                	也就是说当前事件为ACTION_DOWN时,handled会被赋值为true。
                	
                	if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {//这里也说明了如果子View消费了事件,ViewGroup不会对后续事件进行处理,全部交给
                    子View处理
                    
                    	***
                    	'
                     //一个事件序列中ACTION_DOWN事件后的其他事件都会执行
                    dispatchTransformedTouchEvent,该方法在我们上述分析中出现过多次,
                    接下来我们就看看该方法对子View做了什么。
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                             target.child, target.pointerIdBits)) {
                             
                        handled = true;
                        }
                    }
                }
			}
         }
	}	
	***
return handled
}

ViewGroup#dispatchTransformedTouchEvent


private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
		final boolean handled;
		                             ***
		if (child == null) {
				如果传入的子View不存在,则会调用父类(View)的dispatchTouchEvent(event)方法。
                handled = super.dispatchTouchEvent(event);
            } else {
            	//如果子View存在,则会调用子View的dispatchTouchEvent(event)。
                handled = child.dispatchTouchEvent(event);
            }
            return handled;
}

至此ViewGroup事件的分发就结束了,这里总结下整个ViewGroup事件分发流程。
先通过伪代码来整理下思路:

 public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;
        boolean intercepted = false;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            //子View可通过getParent.requestDisallowInterceptTouchEvent请求父容器拦截事件
            final boolean disallowIntercept = (mDroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {//如果允许父容器拦截事件,会调用父容器onInterceptTouchEvent方法
                intercepted = onInterceptTouchEvent(ev);
            } else {
                intercepted = false;//否则父容器不拦截事件
            }
        } else {
            intercepted = true; //没有子View消费事件或者当前不是ACTION_DOWN事件,事件就不会继续传递下去
        }
        //判断是否拦截事件
        if (intercepted ) {
            //如果拦截,则ViewGroup调用自身onTouchEvent方法
            consume = onTouchEvent(ev);
        } else {
            //如果不拦截,则调用子View的dispatchTouchEvent方法
            consume = child.dispatchTouchEvent(ev);
        }
        return consume;
    }

再来张流程图,加深理解。
在这里插入图片描述
ViewGroup中的dispatchTouchEvent负责将事件进行分发,并不会消费事件,如果有子View则会将事件传递下去,我们来看下子View中是如何处理事件。

View#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
		***
		boolean result = false;
         ListenerInfo li = mListenerInfo;//mListenerInfo包含View的很多监听,有onLongClickListener,
         onClickListener等
         if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                    //如果设置onTouchListener ,并且onTouchListener#onTouch返回true,则
                    View#dispatchTouchEvent返回true,表示子View消费事件。
            result = true;
          }
          //如果未设置onTouchListener,则会调用View#’onTouchEvent,如果该方法返回true, 
          View#dispatchTouchEvent返回true,表示子View消费事件。
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        ***
        return result;
    }

View#dispatchTouchEvent中不会再对事件进行分发,这里子View会决定是否消费事件,如果调用了View.setOnTouchListener(onTouchListener listener)且onTouchListener#onTouch
返回true,那么此次事件消费,否则会调用 onTouchEvent(event)。如果设置了onTouchListener,会先执行onTouchListener#onTouch方法,根据其返回值决定是否调用onTouchEvent。也就是onTouchListener#onTouch要先于onTouchEvent执行。

View#onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
	***
	final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //当前View是否为可点击的
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
		
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;//如果当前View状态是DISABLED,那么只要是可点击的,依然能够消费事件。
        }
		if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
		 switch (action) {
		 //当接触到屏幕时
		 case MotionEvent.ACTION_DOWN:
			***
			mHasPerformedLongPress = false;//将该变量赋值为false,表示还未处理长按事件。
			***
			// Walk up the hierarchy to determine if we're inside a scrolling container.
			//判断当前子View是否在滚动容器中
            boolean isInScrollingContainer = isInScrollingContainer();

             // For views inside a scrolling container, delay the pressed feedback for
             // a short period in case this is a scroll.
             //对于滚动视图中的View,短时间内延时按下的回调,以免当前正处于滚动状态。
              if (isInScrollingContainer) {//如果当前正处于滚动状态
                  mPrivateFlags |= PFLAG_PREPRESSED;
                  if (mPendingCheckForTap == null) {
                     mPendingCheckForTap = new CheckForTap();//CheckForTap实现了runnable接口
                   }
                   mPendingCheckForTap.x = event.getX();
                   mPendingCheckForTap.y = event.getY();
                   postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());//通过postDelayed
                   方法名称可知是延时一段时间执行runnable中run方法, ViewConfiguration.getTapTimeout()是
                   100毫秒。CheckForTap中run方法会执行checkForLongClick方法,检查长按事件。
                } else {//如果当前子View没有处于正在滚动的容器中
                  // Not inside a scrolling container, so show the feedback right away
                  setPressed(true, x, y);
                  checkForLongClick(0, x, y);//这里也会执行checkForLongClick 
               }
		break;
		//当离开屏幕时
		case MotionEvent.ACTION_UP:
		***
		//PFLAG_PRESSED标志位在ACTION_DOWN中调用setPressed(true, x, y)设置,此处prepressed 为true
		boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
        	***
        	//mHasPerformedLongPress 如果此时为false,说明长按事件没有触发,那么解除长按事件延时执行
        	事件,这里可能大家会有点蒙,什么叫解除长按事件延时执行呢?后面会回大家揭晓,这里先跟着流
        	程走完。mIgnoreNextUpEvent默认是false,那么所有条件都满足。
        	if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                  // This is a tap, so remove the longpress check
                   removeLongPressCallback();//移除长按事件延时
                   // Only perform take click actions if we were in the pressed state
                   //当只有处于按下状时,才会执行click行为,这里的click是不是就是View#onClickListener中的
                   onClick方法呢?我猜是的,接着往下走。
                    if (!focusTaken) {
                        // Use a Runnable and post this rather than calling
                        // performClick directly. This lets other visual state
                        // of the view update before click actions start.
                         if (mPerformClick == null) {
                            mPerformClick = new PerformClick();//PerformClick也实现了Runable,在其run方法中调用
                            了performClickInternal()。
                           }
                         if (!post(mPerformClick)) {
                               performClickInternal();//这里也调用了performClickInternal(),先说结果吧!
                               在performClickInternal中拖过一系列的调用最终会调用View#OnClickListener中的onClick方法,至此时间分发也就结束了。
                               }
                            }
                        }
        }
		break;
		}
		return true;//这里的返回值会返回至View#dispatchTouchEvent,View#dispatchTouchEvent的返回值会
		返回值ViewGroup#dispatchTransformedTouchEvent,ViewGroup#dispatchTransformedTouchEvent返回值会返回至ViewGroup#dispatchTouchEvent,这样事件消费情况一级一级网上传,直到回到Activity#dispatchTouchEvent。
 }
 retrun false;
}

在View#onTouchEvent中首先会执行ACTION_DOWN(事件的开始肯定是从ACTION_DOWN开始),在ACTION_DOWN中会对长按事件进行检测,那么具体是怎么检测呢?带着问题看看之前分析所遗漏的checkForLongClick方法。
View#checkForLongClick

 private void checkForLongClick(int delayOffset, float x, float y) {
 		//如果当前View是可长按的就会往下执行。
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
        	//将mHasPerformedLongPress 置为false,表示长按事件没有执行。
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();//
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            mPendingCheckForLongPress.rememberPressedState();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);//这里会发送一个延时任务,也就是在
                    ACTION_UP中判断是否已经执行了长按事件,如果没有执行则解除延时任务,这是同一个任
                    务。这里延迟时间 ViewConfiguration.getLongPressTimeout()是500毫秒。
        }
    }

如果当前View可处理长按事件,那么发送一个500毫秒的延时任务,这个延时任务具体执行内容是什么呢?CheckForLongPress中的run方法会告诉我们答案。

private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;

        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                    
				//1.会执行performLongClick方法,并将mHasPerformedLongPress
				
                if (performLongClick(mX, mY)) {
                 置为true
                    mHasPerformedLongPress = true;
                }
            }
        }


public boolean performLongClick(float x, float y) {
        mLongClickX = x;
        mLongClickY = y;
        
        //2. 这里又会执行performLongClick,跟在ACTION_UP中的performClickInternal
        
        final boolean handled = performLongClick();
        有点相似啊!我猜这里面就会调用View#onLongClickListener中的onLongClick方法。
        mLongClickX = Float.NaN;
        mLongClickY = Float.NaN;
        return handled;
    }


 public boolean performLongClick() {
 
 		//3.performLongClickInternal
 		
        return performLongClickInternal(mLongClickX, mLongClickY);
    }
    
private boolean performLongClickInternal(float x, float y) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
        
        	//4.执行View#onLongClickListener中的onLongClick方法,并将该返回值返回
        	
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
      	***
        return handled;
    }

直到此处,豁然明朗。在View#onTouchEvent方法中首先会执行ACTION_DOWN,在ACTION_DOWN中会对长按事件进行检测,并发送一个500毫秒的延时任务,在ACTION_UP中对mHasPerformedLongPress 判断是否执行了长按事。这里涉及到一个时间问题,如果ACTION_DOWN到ACTION_UP事件发生时间间隔不到500毫秒,长按事件还没执行,则会执行View#onClickListener中onClick。如果时间间隔大于500毫秒则会执行View#onLongClickListener中onLongClick,这里会出现两种情况:
1、onLongClick方法返回true时,表示消费了长按事件,则不会再执行View#onclickListener中onClick。
2、onLongClick方法返回false时,表示没有消费长按事件,会执行View#onclickListener中onClick。
通过上述分析可以得出结论:
1、如果View设置OnLongClickListener,从接触屏幕到离开屏幕至离开屏幕时间间隔不足500毫秒,不会执行OnLongClickListener#onLongClick。
2、如果View设置OnLongClickListener、OnClickListener,并且OnLongClickListener#onLongClick返回true,不会执行OnClickListener#onClick;如果返回false,则会执行OnClickListener#onClick。

View#dispatchTouchEvent事件处理结论

View中是对事件进行消费而不是分发,如果View事件没有消费,则会交给上层View处理。如果当前View设置OnTouchListener,且OnTouchListener#onTouch返回true,则事件被消费不会继续传递。返回false则将事件传递至onTouchEvent,如果View设置OnLongClickListener、OnClickListener,如果OnLongClickListener#onLongClick返回true,OnClickListener#onClick不会执行,反之则会执行。综上可知OnTouchListener、OnLongClickListener、OnClickListener优先级层层降低,OnTouchListener优先级最高。个人语言表达能力实在有限,那么画张图帮大家梳理梳理。我还是你的小佩琪.jpg
在这里插入图片描述

事件分发机制原理总结

  • Acttivity
    事件分发的入口是在Acttivity#dispatchTouchEvent。通过系列传递,Activity会将事件传递至ViewGroup中,如果事件在ViewGroup或其子类中被消费,那么事件传递结束;否则,事件将会被Activity#onTouchEvent处理。

  • ViewGroup
    ViewGroup中的事件是Activity传递而来,如果ViewGroup不对事件进行拦截,寻找当前ViewGroup中全部子View,将事件传递给当前手指触摸位置在其范围内的子View。如果拦截事件,当前序列事件的其他事件(MOVE,UP)都将不会再传递,全部交由ViewGroup#onTouchEvent处理,如果onTouchEvent消费事件,则事件传递结束,否则,将事件传回Activity#onTouchEvent。

  • View
    事件传递至View#dispatchTouchEvent,直接View#onTouchEvent。如果View消费事件,那么事件传递结束;如果没有消费事件,View将事件传回至ViewGroup中,交由ViewGroup#onTouchEvent处理。

具体流程图如下:
在这里插入图片描述
相信看到这里大家对事件分发应该有了全新的认识。在下水平有限,若文中有错,欢迎纠正。
本片文章就到此结束啦!咱们下期见!

2019-10-30 13:25:42 CodeFarmer__ 阅读数 169
  • Android核心组件 ListView 详解

    在Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。    ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。   另外ListView还有一个非常神奇的功能,我相信大家应该都体验过,即使在ListView中加载非常非常多的数据,比如达到成百上千条甚至更多, ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。那么ListView是怎么实现这么神奇的功能的呢?    下面我们来学习它,了解了它的基本工作原理,在感叹Google大神能够写出如此精妙代码的同时我也有所敬畏. 1,ListView 介绍; 2,原理讲解; 3,简单实现; 4,ListView扩展;

    16104 人正在学习 去看看 Frank Lee

概述

屏幕刷新包括三个步骤:

  • CPU计算屏幕数据,,把计算好数据交给GPU。
  • GPU会对图形数据进行渲染,渲染好后放到buffer里存起来。
  • 接下来display负责把buffer里的数据呈现到屏幕上。

显示过程,简单的说就是CPU/GPU准备好数据,存入buffer,display每隔一段时间去buffer里取数据,然后显示出来。display读取的频率是固定的,比如每个16ms读一次,但是CPU/GPU写数据是完全无规律的。

对于Android而言:CPU 计算屏幕数据指的也就是 View 树的绘制过程,也就是 Activity 对应的视图树从根布局 DecorView 开始层层遍历每个 View,分别执行测量、布局、绘制三个操作的过程。

也就是说,我们常说的 Android 每隔 16.6ms 刷新一次屏幕其实是指:底层以固定的频率,比如每 16.6ms 将 buffer 里的屏幕数据显示出来。

在这里插入图片描述

Display 这一行可以理解成屏幕,所以可以看到,底层是以固定的频率发出 VSync 信号的,而这个固定频率就是我们常说的每 16.6ms 发送一个 VSync 信号,至于什么叫 VSync 信号,我们可以不用深入去了解,只要清楚这个信号就是屏幕刷新的信号就可以了。

CPU 蓝色的这行,上面也说过了,CPU 这块的耗时其实就是我们 app 绘制当前 View 树的时间,而这段时间就跟我们自己写的代码有关系了,如果你的布局很复杂,层次嵌套很多,每一帧内需要刷新的 View 又很多时,那么每一帧的绘制耗时自然就会多一点。

  • 我们常说的 Android 每隔 16.6 ms 刷新一次屏幕其实是指底层会以这个固定频率来切换每一帧的画面。
  • 这个每一帧的画面也就是我们的 app 绘制视图树(View 树)计算而来的,这个工作是交由 CPU 处理,耗时的长短取决于我们写的代码:布局复不复杂,层次深不深,同一帧内刷新的 View 的数量多不多。
  • CPU 绘制视图树来计算下一帧画面数据的工作是在屏幕刷新信号来的时候才开始工作的,而当这个工作处理完毕后,也就是下一帧的画面数据已经全部计算完毕,也不会马上显示到屏幕上,而是会等下一个屏幕刷新信号来的时候再交由底层将计算完毕的屏幕画面数据显示出来。
  • 当我们的 app 界面不需要刷新时(用户无操作,界面无动画),app 就接收不到屏幕刷新信号所以也就不会让 CPU 再去绘制视图树计算画面数据工作,但是底层仍然会每隔 16.6 ms 切换下一帧的画面,只是这个下一帧画面一直是相同的内容。

为什么界面不刷新时 app 就接收不到屏幕刷新信号了?为什么绘制视图树计算下一帧画面的工作会是在屏幕刷新信号来的时候才开始的?

源码

ViewRootImpl 与 DecorView 的绑定.

View#invalidate() 是请求重绘的一个操作,所以我们切入点可以从这个方法开始一步步跟下去。

Android 设备呈现到界面上的大多数情况下都是一个 Activity,真正承载视图的是一个 Window,每个 Window 都有一个 DecorView,我们调用 setContentView() 其实是将我们自己写的布局文件添加到以 DecorView 为根布局的一个 ViewGroup 里,构成一颗 View 树。

每个 Activity 对应一颗以 DecorView 为根布局的 View 树,但其实 DecorView 还有 mParent,而且就是 ViewRootImpl,而且每个界面上的 View 的刷新,绘制,点击事件的分发其实都是由 ViewRootImpl 作为发起者的,由 ViewRootImpl 控制这些操作从 DecorView 开始遍历 View 树去分发处理。

ViewRootImpl 与 DecorView 的绑定

跟着 invalidate() 一步步往下走的时候,发现最后跟到了 ViewRootImpl#scheduleTraversals() 就停止了。

Android 设备呈现到界面上的大多数情况下都是一个 Activity,真正承载视图的是一个 Window,每个 Window 都有一个 DecorView,我们调用 setContentView() 其实是将我们自己写的布局文件添加到以 DecorView 为根布局的一个 ViewGroup 里,构成一颗 View 树。

每个 Activity 对应一颗以 DecorView 为根布局的 View 树,但其实 DecorView 还有 mParent,而且就是 ViewRootImpl,而且每个界面上的 View 的刷新,绘制,点击事件的分发其实都是由 ViewRootImpl 作为发起者的,由 ViewRootImpl 控制这些操作从 DecorView 开始遍历 View 树去分发处理。

View#invalidate() 时,也可以看到内部其实是有一个 do{}while() 循环来不断寻找 mParent,所以最终才会走到 ViewRootImpl 里去,那么可能大伙就会疑问了,为什么 DecorView 的 mParent 会是 ViewRootImpl 呢?换个问法也就是,在什么时候将 DevorView 和 ViewRootImpl 绑定起来?

Activity 的启动是在 ActivityThread 里完成的,handleLaunchActivity() 会依次间接的执行到 Activity 的 onCreate(), onStart(), onResume()。在执行完这些后 ActivityThread 会调用 WindowManager#addView(),而这个 addView() 最终其实是调用了 WindowManagerGlobal 的 addView() 方法,我们就从这里开始看:

//WindowManagerGlobal#addView
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    ...
    ViewRootImpl root;
    ...
    synchronized (mLock) {
        ...
        //1. 实例化一个 ViewRootImpl对象
        root = new ViewRootImpl(view.getContext(), display);
        ...
        mViews.add(view);
        mRoots.add(root);
        ...
    }
    try {
        //2. 调用ViewRootImpl的setView(),并将DecorView作为参数传递进去
        root.setView(view, wparams, panelParentView);
    }...  
}

WindowManager 维护着所有 Activity 的 DecorView 和 ViewRootImpl。这里初始化了一个 ViewRootImpl,然后调用了它的 setView() 方法,将 DevorView 作为参数传递了进去。所以看看 ViewRootImpl 中的 setView() 做了什么:

//ViewRootImpl#setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            //1. view 是 DecorView
            mView = view;
            ...
            //2.发起布局请求
            requestLayout();
            ...
            //3.将当前ViewRootImpl对象this,作为参数调用了DecorView的assignParent
            view.assignParent(this);
            ...
        }
    }
}

在 setView() 方法里调用了 DecorView 的 assignParent() 方法,所以去看看 View 的这个方法:

//View#assignParent
void assignParent(ViewParent parent) {
    if (mParent == null) {
        mParent = null;
    } else if (parent == null) {
        mParent = null;
    } else {
        throw new RunTimeException("view " + this + " is already has a parent")
    }
}

参数是 ViewParent,而 ViewRootImpl 是实现了 ViewParent 接口的,所以在这里就将 DecorView 和 ViewRootImpl 绑定起来了。每个Activity 的根布局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl,所以在子 View 里执行 invalidate() 之类的操作,循环找 parent 时,最后都会走到 ViewRootImpl 里来。

跟界面刷新相关的方法里应该都会有一个循环找 parent 的方法,或者是不断调用 parent 的方法,这样最终才都会走到 ViewRootImpl 里,也就是说实际上 View 的刷新都是由 ViewRootImpl 来控制的。

即使是界面上一个小小的 View 发起了重绘请求时,都要层层走到 ViewRootImpl,由它来发起重绘请求,然后再由它来开始遍历 View 树,一直遍历到这个需要重绘的 View 再调用它的 onDraw() 方法进行绘制。

重新看回 ViewRootImpl 的 setView() 这个方法,这个方法里还调用了一个 requestLayout() 方法:

//ViewRootImpl#requestLayout
@Override
public void requestLayout() {
    if (!mHandingLayoutInLayoutRequest) {
        //1.检查该操作是否是在主线程中执行
        checkThread();
        mLayoutRequested = true;
        //2.安排一次遍历绘制View树的任务
        scheduleTraversals();
    }
}

这里调用了一个 scheduleTraversals(),还记得当 View 发起重绘操作 invalidate() 时,最后也调用了 scheduleTraversals() 这个方法么。其实这个方法就是屏幕刷新的关键,它是安排一次绘制 View 树的任务等待执行,具体后面再说。

也就是说,其实打开一个 Activity,当它的 onCreate—onResume 生命周期都走完后,才将它的 DecoView 与新建的一个 ViewRootImpl 对象绑定起来,同时开始安排一次遍历 View 任务也就是绘制 View 树的操作等待执行,然后将 DecoView 的 parent 设置成 ViewRootImpl 对象。

这也就是为什么在 onCreate—onResume 里获取不到 View 宽高的原因,因为在这个时刻 ViewRootImpl 甚至都还没创建,更不用说是否已经执行过测量操作了。

还可以得到一点信息是,一个 Activity 界面的绘制,其实是在 onResume() 之后才开始的。

ViewRootImpl # scheduleTraversals

调用一个 View 的 invalidate() 请求重绘操作,内部原来是要层层通知到 ViewRootImpl 的 scheduleTraversals() 里去。

//ViewRootImpl#scheduleTraversals
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
            Choreograhper.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

mTraversalScheduled 这个 boolean 变量的作用等会再来看,先看看 mChoreographer.postCallback() 这个方法,传入了三个参数,第二个参数是一个 Runnable 对象,先来看看这个 Runnable:

//ViewRootImpl$TraversalRunnable
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
//ViewRootImpl成员变量
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

做的事很简单,就调用了一个方法,doTraversal():

//ViewRootImpl#doTraversal
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        ...

        //1. 遍历绘制View树
        performTraversals();
        ...
    }
}

看看这个方法做的事,跟 scheduleTraversals() 正好相反,一个将变量置成 true,这里置成 false,一个是 postSyncBarrier(),这里是 removeSyncBarrier(),具体作用等会再说,继续先看看 performTraversals(),这个方法也是屏幕刷新的关键:

//ViewRootImpl#performTraversals
private void performTraversals() {
    //该方法实在太过复杂,所以将无关代码全部都省略掉,只留下关键代码和代码结构
    ...
    if (...) {
        ...
        if (...) {
            if (...) {
                ...
                //1.测量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ...
                layoutRequested = true;
            }
        }
    } ...
    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    ...
    if (didLayout) {
        //2.布局
        performLayout(lp, mWidth, mHeight);
        ...
    }
    ...
    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    if (!cancelDraw && !newSurface) {
        ...
        //3.绘制
        performDraw();
    }...

    ...
}

View 的测量、布局、绘制三大流程都是交由 ViewRootImpl 发起,而且还都是在 performTraversals() 方法中发起的,所以这个方法的逻辑很复杂,因为每次都需要根据相应状态判断是否需要三个流程都走,有时可能只需要执行 performDraw() 绘制流程,有时可能只执行 performMeasure() 测量和 performLayout() 布局流程(一般测量和布局流程是一起执行的)。不管哪个流程都会遍历一次 View 树,所以其实界面的绘制是需要遍历很多次的,如果页面层次太过复杂,每一帧需要刷新的 View 又很多时,耗时就会长一点。

测量、布局、绘制这些流程在遍历时并不一定会把整颗 View 树都遍历一遍,ViewGroup 在传递这些流程时,还会再根据相应状态判断是否需要继续往下传递。

了解了 performTraversals() 是刷新界面的源头后,接下去就需要了解下它是什么时候执行的,和 scheduleTraversals() 又是什么关系?

performTraversals() 是在 doTraversal() 中被调用的,而 doTraversal() 又被封装到一个 Runnable 里,那么关键就是这个 Runnable 什么时候被执行了?

Choreographer

scheduleTraversals() 里调用了 Choreographer 的 postCallback() 将 Runnable 作为参数传了进去

//Choreograhper#postCallback
public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 0);
}
//Choreograhper#postCallbackDelayed
pubic void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis) {
    ...  

    postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}

//Choreograhper#postCallbackDelayedInternal
private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) {
    ...

    synchronized (mLock) {
        //1.获取当前时间戳
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        //2.根据时间戳将Runnable任务添加到指定的队列中
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        //3.因为postCallback默认传入delay = 0,所以代码会走进if里面
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {...}
    }
}

因为 postCallback() 调用 postCallbackDelayed() 时传了 delay = 0 进去,所以在 postCallbackDelayedInternal() 里面会先根据当前时间戳将这个 Runnable 保存到一个 mCallbackQueue 队列里,这个队列跟 MessageQueue 很相似,里面待执行的任务都是根据一个时间戳来排序。然后走了 scheduleFrameLocked() 方法这边,看看做了些什么:

//Choreograhper#scheduleFrameLocked
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        //1.系统4.0之后该变量默认为true,所以会走进if里
        if (USE_VSYNC) {
            ...

            if (isRunningOnLooperThreadLocked()) {
                scheduleVsyncLocked();
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } ...
    }
}

如果代码走了 else 这边来发送一个消息,那么这个消息做的事肯定很重要,因为对这个 Message 设置了异步的标志而且用了sendMessageAtFrontOfQueue() 方法,这个方法是将这个 Message 直接放到 MessageQueue 队列里的头部,可以理解成设置了这个 Message 为最高优先级,那么先看看这个 Message 做了些什么:

//Choreograhper#scheduleFrameLocked
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        //1.系统4.0之后该变量默认为true,所以会走进if里
        if (USE_VSYNC) {
            ...

            if (isRunningOnLooperThreadLocked()) {
                scheduleVsyncLocked();
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } ...
    }
}

这个 Message 设置了异步的标志而且用了sendMessageAtFrontOfQueue() 方法,这个方法是将这个 Message 直接放到 MessageQueue 队列里的头部,可以理解成设置了这个 Message 为最高优先级,先看看这个 Message :

//Choreograhper$FrameHandler#handleMessage
private final class FrameHandler extends Handler {
    public FrameHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            ...
            case MSG_DO_SCHEDULE_VSYNC:
                doScheduleVsync();
                break;
            ...
        }
    }
}

//Choreographer#doScheduleVsync
void doScheduleVsync() {
    synchronized (mLock) {
        if (mFrameScheduled) {
            scheduleVsyncLocked();
        }
    }
}

所以这个 Message 最后做的事就是 scheduleVsyncLocked()。我们回到 scheduleFrameLocked() 这个方法里,当走 if 里的代码时,直接调用了 scheduleVsyncLocked(),当走 else 里的代码时,发了一个最高优先级的 Message,这个 Message 也是执行 scheduleVsyncLocked()。既然两边最后调用的都是同一个方法,那么为什么这么做呢?

关键在于 if 条件里那个方法是用来判断当前是否是在主线程的,我们知道主线程也是一直在执行着一个个的 Message,那么如果在主线程的话,直接调用这个方法,那么这个方法就可以直接被执行了,如果不是在主线程,那么 post 一个最高优先级的 Message 到主线程去,保证这个方法可以第一时间得到处理。

那么这个方法是干嘛的呢,为什么需要在最短时间内被执行呢,而且只能在主线程?

//Choreographer#scheduleVsyncLocked
private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}
//DisplayEventReceiver#scheduleVsync
/**
 * Schedules a single vertical sync pulse to be delivered when the next
 * display frame begins.
 */
public void scheduleVsync() {
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event " + "receiver has already been disposed.");
    } else {
        nativeScheduleVsync(mReceiverPtr);
    }
}

调用了 native 层的一个方法。

到这里为止,我们知道一个 View 发起刷新的操作时,会层层通知到 ViewRootImpl 的 scheduleTraversals() 里去,然后这个方法会将遍历绘制 View 树的操作 performTraversals() 封装到 Runnable 里,传给 Choreographer,以当前的时间戳放进一个 mCallbackQueue 队列里,然后调用了 native 层的一个方法就跟不下去了。所以这个 Runnable 什么时候会被执行还不清楚。那么,下去的重点就是搞清楚它什么时候从队列里被拿出来执行了?

既然这个 Runnable 操作被放在一个 mCallbackQueue 队列里,那就从这个队列着手,看看这个队列的取操作在哪被执行了:

//Choreographer$CallbackQueue
private final class CallbackQueue {
    private CallbackRecord mHead;
    ...
    //1.取操作
    public CallbackRecord extractDueCallbacksLocked(long now){...}  
    //2.入队列操作
    public void addCallbackLocked(long dueTime, Object action, Object token) {...}
    ...  
}

//Choreographer#doCallbacks
void doCallbacks(int callbackType, long frameTimeNanos) {
    CallbackRecord callbacks;
    synchronized(mLock) {
        ...
        //1.这个队列跟MessageQueue很相似,所以取的时候需要传入一个时间戳,因为队头的任务可能还没到设定的执行时间
        callback = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
        ...
    }
}

//Choreographer#doFrame
void doFrame(long frameTimeNanos, int frame) {
    ...
    try {
        ...
        //1.这个参数跟 ViewRootImpl调用mChoreographer.postCallback()时传进的第一个参数是一致的
        doCallbacks(Choreograhper.CALLBACK_TRAVERSAL, frameTimeNanos);
        ...
    }...
}

我们说过在 ViewRootImpl 的 scheduleTraversals() 里会将遍历 View 树绘制的操作封装到 Runnable 里,然后调用 Choreographer 的 postCallback() 将这个 Runnable 放进队列里么,而当时调用 postCallback() 时传入了多个参数,这是因为 Choreographer 里有多个队列,而第一个参数 Choreographer.CALLBACK_TRAVERSAL 这个参数是用来区分队列的,可以理解成各个队列的 key 值。

那么这样一来,就找到关键的方法了:doFrame(),这个方法里会根据一个时间戳去队列里取任务出来执行,而这个任务就是 ViewRootImpl 封装起来的 doTraversal() 操作,而 doTraversal() 会去调用 performTraversals() 开始根据需要测量、布局、绘制整颗 View 树。所以剩下的问题就是 doFrame() 这个方法在哪里被调用了。

//Choreographer$FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
    ...
    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        ...
        //1.这个这里的this,该message做的事其实是下面的run()方法
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
        mHavePendingVsync = false;
        doFrame(mTimestampNanos, mFrame);
    }
}

这个继承自 DisplayEventReceiver 的 FrameDisplayEventReceiver 类的作用很重要。

FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,这里mTimestampNanos是信号到来的时间参数。

也就是说,onVsync() 是底层会回调的,可以理解成每隔 16.6ms 一个帧信号来的时候,底层就会回调这个方法,当然前提是我们得先注册,这样底层才能找到我们 app 并回调。当这个方法被回调时,内部发起了一个 Message,注意看代码对这个 Message 设置了 callback 为 this,Handler 在处理消息时会先查看 Message 是否有 callback,有则优先交由 Message 的 callback 处理消息,没有的话再去看看Handler 有没有 callback,如果也没有才会交由 handleMessage() 这个方法执行。

onVsync() 是由底层回调的,那么它就不是运行在我们 app 的主线程上,毕竟上层 app 对底层是隐藏的。但这个 doFrame() 是个 ui 操作,它需要在主线程中执行,所以才通过 Handler 切到主线程中。

前面分析 scheduleTraversals() 方法时,最后跟到了一个 native 层方法就跟不下去了,现在再回过来想想这个 native 层方法的作用是什么,应该就比较好猜测了。

//DisplayEventReceiver#scheduleVsync
/**
 * Schedules a single vertical sync pulse to be delivered when the next
 * display frame begins.
 */
public void scheduleVsync() {
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event " + "receiver has already been disposed.");
    } else {
        nativeScheduleVsync(mReceiverPtr);
    }
}

大体上是说安排接收一个 vsync 信号。而根据我们的分析,如果这个 vsync 信号发出的话,底层就会回调 DisplayEventReceiver 的 onVsync() 方法。

如果只是这样的话,就有一点说不通了,首先上层 app 对于这些发送 vsync 信号的底层来说肯定是隐藏的,也就是说底层它根本不知道上层 app 的存在,那么在它的每 16.6ms 的帧信号来的时候,它是怎么找到我们的 app,并回调它的方法呢?

这就有点类似于观察者模式,或者说发布-订阅模式。既然上层 app 需要知道底层每隔 16.6ms 的帧信号事件,那么它就需要先注册监听才对,这样底层在发信号的时候,直接去找这些观察者通知它们就行了。

这是我的理解,所以,这样一来,scheduleVsync() 这个调用到了 native 层方法的作用大体上就可以理解成注册监听了,这样底层也才找得到上层 app,并在每 16.6ms 刷新信号发出的时候回调上层 app 的 onVsync() 方法。这样一来,应该就说得通了。

还有一点,scheduleVsync() 注册的监听应该只是监听下一个屏幕刷新信号的事件而已,而不是监听所有的屏幕刷新信号。比如说当前监听了第一帧的刷新信号事件,那么当第一帧的刷新信号来的时候,上层 app 就能接收到事件并作出反应。但如果还想监听第二帧的刷新信号,那么只能等上层 app 接收到第一帧的刷新信号之后再去监听下一帧。

梳理一下目前的信息

  • 我们知道一个 View 发起刷新的操作时,最终是走到了 ViewRootImpl 的 scheduleTraversals() 里去,然后这个方法会将遍历绘制 View 树的操作 performTraversals() 封装到 Runnable 里,传给 Choreographer,以当前的时间戳放进一个 mCallbackQueue 队列里,然后调用了 native 层的方法向底层注册监听下一个屏幕刷新信号事件。
  • 当下一个屏幕刷新信号发出的时候,如果我们 app 有对这个事件进行监听,那么底层它就会回调我们 app 层的 onVsync() 方法来通知。当 onVsync() 被回调时,会发一个 Message 到主线程,将后续的工作切到主线程来执行。
  • 切到主线程的工作就是去 mCallbackQueue 队列里根据时间戳将之前放进去的 Runnable 取出来执行,而这些 Runnable 有一个就是遍历绘制 View 树的操作 performTraversals()。在这次的遍历操作中,就会去绘制那些需要刷新的 View。
  • 所以说,当我们调用了 invalidate(),requestLayout(),等之类刷新界面的操作时,并不是马上就会执行这些刷新的操作,而是通过 ViewRootImpl 的 scheduleTraversals() 先向底层注册监听下一个屏幕刷新信号事件,然后等下一个屏幕刷新信号来的时候,才会去通过 performTraversals() 遍历绘制 View 树来执行这些刷新操作。

过滤一帧内重复的刷新请求

整体上的流程我们已经梳理出来,但还有几点问题需要解决。

我们在一个 16.6ms 的一帧内,代码里可能会有多个 View 发起了刷新请求,这是非常常见的场景了,比如某个动画是有多个 View 一起完成,比如界面发生了滑动等等。

按照我们上面梳理的流程,只要 View 发起了刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals() 里去,是吧。而这个方法又会封装一个遍历绘制 View 树的操作 performTraversals() 到 Runnable 然后扔到队列里等刷新信号来的时候取出来执行。

那如果多个 View 发起了刷新请求,岂不是意味着会有多次遍历绘制 View 树的操作?

其实,这点不用担心,还记得我们在最开始分析 scheduleTraverslas() 的时候先跳过了一些代码么?现在我们回过来继续看看这些代码:

//ViewRootImpl#scheduleTraversals
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        //1.注意这个boolean类型的变量
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
            Choreograhper.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

我们上面分析的 scheduleTraversals() 干的那一串工作,前提是 mTraversalScheduled 这个 boolean 类型变量等于 false 才会去执行。那这个变量在什么时候被赋值被 false 了呢:

一个是上图的 doTraversal(),还有就是声明时默认为 false,剩下一个是在取消遍历绘制 View 操作 unscheduleTraversals() 里。

doTraversal()这个方法,就是在 scheduleTraversals() 中封装到 Runnable 里的那个方法。

当我们调用了一次 scheduleTraversals()之后,直到下一个屏幕刷新信号来的时候,doTraversal() 被取出来执行。在这期间重复调用 scheduleTraversals() 都会被过滤掉的。那么为什么需要这样呢?

View 就是在执行 performTraversals() 遍历绘制 View 树过程中层层遍历到需要刷新的 View,然后去绘制它的。既然是遍历,那么不管上一帧内有多少个 View 发起了刷新的请求,在这一次的遍历过程中全部都会去处理的。这也是我们从代码上看到的,每一个屏幕刷新信号来的时候,只会去执行一次 performTraversals(),因为只需遍历一遍,就能够刷新所有的 View 了。

同步屏障消息postSyncBarrier()

当我们的 app 接收到屏幕刷新信号时,来不及第一时间就去执行刷新屏幕的操作,这样一来,即使我们将布局优化得很彻底,保证绘制当前 View 树不会超过 16ms,但如果不能第一时间优先处理绘制 View 的工作,那等 16.6 ms 过了,底层需要去切换下一帧的画面了,我们 app 却还没处理完,这样也照样会出现丢帧了吧。而且这种场景是非常有可能出现的吧,毕竟主线程需要处理的事肯定不仅仅是刷新屏幕的事而已,那么这个问题是怎么处理的呢?

//ViewRootImpl#scheduleTraversals
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //1.注意这行代码,往主线程的消息队列里发送了一个同步屏障消息
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
            Choreograhper.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

//ViewRootImpl#doTraversal
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //1.注意这行代码,移除消息队列里的同步屏障消息
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        ...

        performTraversals();
        ...
    }
}

逻辑走进 Choreographer 前会先往队列里发送一个同步屏障,而当 doTraversal() 被调用时才将同步屏障移除。

这个同步屏障的作用可以理解成拦截同步消息的执行,主线程的 Looper 会一直循环调用 MessageQueue 的 next() 来取出队头的 Message 执行,当 Message 执行完后再去取下一个。

当 next() 方法在取 Message 时发现队头是一个同步屏障的消息时,就会去遍历整个队列,只寻找设置了异步标志的消息,如果有找到异步消息,那么就取出这个异步消息来执行,否则就让 next() 方法陷入阻塞状态。

如果 next() 方法陷入阻塞状态,那么主线程此时就是处于空闲状态的,也就是没在干任何事。所以,如果队头是一个同步屏障的消息的话,那么在它后面的所有同步消息就都被拦截住了,直到这个同步屏障消息被移除出队列,否则主线程就一直不会去处理同步屏幕后面的同步消息。

而所有消息默认都是同步消息,只有手动设置了异步标志,这个消息才会是异步消息。另外,同步屏障消息只能由内部来发送,这个接口并没有公开给我们使用。

最后,仔细看上面 Choreographer 里所有跟 message 有关的代码,你会发现,都手动设置了异步消息的标志,所以这些操作是不受到同步屏障影响的。这样做的原因可能就是为了尽可能保证上层 app 在接收到屏幕刷新信号时,可以在第一时间执行遍历绘制 View 树的工作。

刷新控制者 ViewRootImpl

所有跟界面刷新相关的操作,其实最终都会走到 ViewRootImpl 中的 scheduleTraversals() 去的。

跟界面刷新有关的操作大概就是下面几种场景吧:

  • invalidate(请求重绘)

  • requestLayout(重新布局)

  • requestFocus(请求焦点)

  • startActivity(打开新界面)

  • onRestart(重新打开界面)

  • KeyEvent(遥控器事件,本质上是焦点导致的刷新)

  • Animation(各种动画,本质上是请求重绘导致的刷新)

  • RecyclerView滑动(页面滑动,本质上是动画导致的刷新)

  • setAdapter(各种adapter的更新)

//ViewRootImpl#requestChildFocus
@Override
public void requestChildFocus(View child, View focused) {
    if (DEBUG_INPUT_RESIZE) {
        Log.v(mTag, "Request child focus: focus now " + focused);
    }
    checkThread();
    scheduleTraversals();
}

//ViewRootImpl#clearChildFocus
@Override
public void clearChildFocus(View child) {
    if (DEBUG_INPUT_RESIZE) {
        Log.v(mTag, "Clearing child focus");
    }
    checkThread();
    scheduleTraversals();
}

//ViewRootImpl#requestLayout
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

总结

  • 界面上任何一个 View 的刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals() 里来安排一次遍历绘制 View 树的任务;
  • scheduleTraversals() 会先过滤掉同一帧内的重复调用,在同一帧内只需要安排一次遍历绘制 View 树的任务即可,这个任务会在下一个屏幕刷新信号到来时调用 performTraversals() 遍历 View 树,遍历过程中会将所有需要刷新的 View 进行重绘;
  • 接着 scheduleTraversals() 会往主线程的消息队列中发送一个同步屏障,拦截这个时刻之后所有的同步消息的执行,但不会拦截异步消息,以此来尽可能的保证当接收到屏幕刷新信号时可以尽可能第一时间处理遍历绘制 View 树的工作;
  • 发完同步屏障后 scheduleTraversals() 才会开始安排一个遍历绘制 View 树的操作,作法是把 performTraversals() 封装到 Runnable 里面,然后调用 Choreographer 的 postCallback() 方法;
  • postCallback() 方法会先将这个 Runnable 任务以当前时间戳放进一个待执行的队列里,然后如果当前是在主线程就会直接调用一个native 层方法,如果不是在主线程,会发一个最高优先级的 message 到主线程,让主线程第一时间调用这个 native 层的方法;
  • native 层的这个方法是用来向底层注册监听下一个屏幕刷新信号,当下一个屏幕刷新信号发出时,底层就会回调 Choreographer 的onVsync() 方法来通知上层 app;
  • onVsync() 方法被回调时,会往主线程的消息队列中发送一个执行 doFrame() 方法的消息,这个消息是异步消息,所以不会被同步屏障拦截住;
  • doFrame() 方法会去取出之前放进待执行队列里的任务来执行,取出来的这个任务实际上是 ViewRootImpl 的 doTraversal() 操作;
  • 上述第4步到第8步涉及到的消息都手动设置成了异步消息,所以不会受到同步屏障的拦截;
  • doTraversal() 方法会先移除主线程的同步屏障,然后调用 performTraversals() 开始根据当前状态判断是否需要执行performMeasure() 测量、perfromLayout() 布局、performDraw() 绘制流程,在这几个流程中都会去遍历 View 树来刷新需要更新的View;

常见问题

Android 每隔 16.6 ms 刷新一次屏幕到底指的是什么意思?是指每隔 16.6ms 调用 onDraw() 绘制一次么?

如果界面一直保持没变的话,那么还会每隔 16.6ms 刷新一次屏幕么?

我们常说的 Android 每隔 16.6 ms 刷新一次屏幕其实是指底层会以这个固定频率来切换每一帧的画面,而这个每一帧的画面数据就是我们 app 在接收到屏幕刷新信号之后去执行遍历绘制 View 树工作所计算出来的屏幕数据。

而 app 并不是每隔 16.6ms 的屏幕刷新信号都可以接收到,只有当 app 向底层注册监听下一个屏幕刷新信号之后,才能接收到下一个屏幕刷新信号到来的通知。而只有当某个 View 发起了刷新请求时,app 才会去向底层注册监听下一个屏幕刷新信号。

也就是说,只有当界面有刷新的需要时,我们 app 才会在下一个屏幕刷新信号来时,遍历绘制 View 树来重新计算屏幕数据。如果界面没有刷新的需要,一直保持不变时,我们 app 就不会去接收每隔 16.6ms 的屏幕刷新信号事件了,但底层仍然会以这个固定频率来切换每一帧的画面,只是后面这些帧的画面都是相同的而已。

界面的显示其实就是一个 Activity 的 View 树里所有的 View 都进行测量、布局、绘制操作之后的结果呈现,那么如果这部分工作都完成后,屏幕会马上就刷新么?

我们 app 只负责计算屏幕数据而已,接收到屏幕刷新信号就去计算,计算完毕就计算完毕了。至于屏幕的刷新,这些是由底层以固定的频率来切换屏幕每一帧的画面。所以即使屏幕数据都计算完毕,屏幕会不会马上刷新就取决于底层是否到了要切换下一帧画面的时机了。

**网上都说避免丢帧的方法之一是保证每次绘制界面的操作要在 16.6ms 内完成,但如果这个 16.6ms 是一个固定的频率的话,请求绘制的操作在代码里被调用的时机是不确定的啊,那么如果某次用户点击屏幕导致的界面刷新操作是在某一个 16.6ms 帧快结束的时候,那么即使这次绘制操作小于 16.6 ms,按道理不也会造成丢帧么?这又该如何理解? **

代码里调用了某个 View 发起的刷新请求,这个重绘工作并不会马上就开始,而是需要等到下一个屏幕刷新信号来的时候才开始。

主线程耗时的操作会导致丢帧,但是耗时的操作为什么会导致丢帧?它是如何导致丢帧发生的?

造成丢帧大体上有两类原因,一是遍历绘制 View 树计算屏幕数据的时间超过了 16.6ms;

二是,主线程一直在处理其他耗时的消息,导致遍历绘制 View 树的工作迟迟不能开始,从而超过了 16.6 ms 底层切换下一帧画面的时机。

第一个原因就是我们写的布局有问题了,需要进行优化了。而第二个原因则是我们常说的避免在主线程中做耗时的任务。

针对第二个原因,系统已经引入了同步屏障消息的机制,尽可能的保证遍历绘制 View 树的工作能够及时进行,但仍没办法完全避免,所以我们还是得尽可能避免主线程耗时工作。

在这里插入图片描述

android屏幕适配原理

阅读数 8447

没有更多推荐了,返回首页