Android进阶课学习收获 (13~18)

17 篇文章 1 订阅
10 篇文章 1 订阅

第13讲:Android是如何通过Activity进行交互的?

taskAffinity

        单纯使用taskAffinity不能导致Activity被创建在新的任务栈中,需要配合使用singleTask或者singleInstance。我们可以在命令行里输入 adb shell dumpsys activity activities  来输出日志 查看任务栈情况,其中一个TaskRecord代表一个任务栈,如下:

taskAffinity + allowTaskReparenting

     allowTaskReparenting 赋予Activity在各个Task中间转移的特性。一个在后台任务栈中的Activity A,当有其他任务进入前台,并且taskAffinity与A相同,则会自动将A添加到当前启动的任务栈中。

分别创建 2 个 Android 工程:First 和 TaskAffinityReparent:

  • 在 First 中有 3 个 Activity:FirstA、FirstB、FirstC。打开顺序依次是 FirstA -> FirstB -> FirstC。其中 FirstC 的 taskAffinity 为”lagou.affinity“,且 allowTaskReparenting 属性设置为true。FirstA 和 FirstB 为默认值;
  • TaskAffinityReparent 中只有一个 Activity--ReparentActivity,并且其 TaskAffinity 也等于”lagou.affinity“

     将这两个项目分别安装到手机上之后,打开 First App,并从 FirstA 开始跳转到 FirstB,再进入 FirstC 页面。然后按 Home 键,使其进入后台任务。

     接下来,打开 TaskAffinityReparent 项目,屏幕上本应显示 ReparentActivity 的页面内容,但是实际上显示的却是 FirstC 中的页面内容。

      可以看出,FirstC 被移动到与 ReparentActivity 处在一个任务栈中。此时 FirstC 位于栈顶位置,再次点击返回键,才会显示 ReparentActivity 页面。

通过Binder传递数据的限制

    Activity界面跳转的时候,使用Intent传递数据,偶尔会导致程序崩溃,比如如下代码:

public void startFirstB(){
    Intent intent = new Intent(this,FirstB.class);
    intent.putExtra('bean',new Bean());
    startActivity(intent);
}

static class Bean implements Serialiizable{
    private btye[] data = new byte[1024*1024];
    String str = "data String";
}

在跳转的时候就会报错:

上面log日志的意思是Intent传递数据量过大,最终原因是Android系统对使用Binder传数据进行了限制,通常为1M,但是根据不同版本、不同厂商,这个值是有区别的。

解决办法:

  1. 减少通过Intent传递的数据,将非必须字段使用transient关键字修饰,比如上述bean中的byte[] data并非必须使用的数据,则需要避免将其序列化,如图所示:
public void startFirstB(){
    Intent intent = new Intent(this,FirstB.class);
    intent.putExtra('bean',new Bean());
    startActivity(intent);
}

static class Bean implements Serialiizable{
    private transient btye[] data = new byte[1024*1024];
    String str = "data String";
}

2.将对象转为JSON字符串,减少数据体积 。因为JVM加载类通常会伴随额外的空间来保存类相关信息,将类中的信息转化为JSON字符串可以减少数据大小。比如使用使用Gson.toJson方法。大多数时候,转换之后,还是会超过Binder限制,说明实际需要传递的数据是很大的。这种情况则需要考虑本地持久化来实现数据共享,或者使用EventBus来实现数据传递。

process属性造成多个Application

      我们经常在自定义的Application中做一些初始化的操作,比如APP分包、推送初始化、图片加载库的全局配置等。实际上Activity可以在不同的进程中启动,而每一个不同的进程都会创建一个Application,因此可能造成Application的onCreate方法被执行多次。

解决办法:在onCreate方法中判断进程的名字,只有符合要求的进程里才执行初始化操作;

Android 10.0 后台启动Activity 失效

     从Android 10.0(API29)开始,Android系统对后台启动Activity做了一定的限制。主要目的就是尽可能避免用户前台的交互被打断,保证当前屏幕展示的内容不受影响。

     但这也造成了很多实际问题,比如项目中有Force Update功能,当用户选择升级之后会在后台进行新安装包的下载任务。正常情况下下载成功需要弹出apk安装界面,但是在10.0以后,无法弹出安装界面了。

 解决办法:Android官方建议我们使用通知来替代直接启动Activity操作。也就说后台任务执行完毕后,并不会调用startActivity()启动新页面,而是通过NotificationManager来发送Notification到状态栏。这样既不影响当前使用的交互操作,用户也能及时获取后台任务的进展情况,后续的操作由用户自己决定。

总结:本课时主要总结了几个使用startActivity时可能遇到的问题

  1. taskAffinity实现任务栈的调配;
  2. 通过Binder传递数据的显示;
  3. 多进程应用可能会造成的问题;
  4. 后台启动Activity的限制;

第14讲:Android Bitmap全面详解

Bitmap占用内存分析

     Bitmap用来描述一张图的长、宽、颜色等信息。通常情况下,我们可以使用BitmapFactory来将某一路径下的图片解析为Bitmap对象。

    当一张图加载到内存后,具体需要占用多大内存呢?我们可以通过Bitmap的实例方法getAllocationByteCount()方法获取Bitmap占用的字节大小,代码如下:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.rodman);

Log.e(TAG,"bitmap size is"+bitmap.getAllocationByteCount());

     上图中rodman是保存在res/drawable-xhdpi目录下的一张600*600,大小为65kb的图片。打印结果为:bitmap size is 1440000。

默认情况下,BitmapFactory使用Bitmap.Config.ARGB_8888的存储方式来加载图片内存,在这种模式下,每一个像素需要占用4个字节。因此图片rodman的内存大小可以使用的公式如下:宽*高*4 = 600*600*4 = 1440000;

屏幕自适应

    如果在保证不修改代码的前提下,将图片rodman移动到(注意是移动,不是拷贝)res/drawable-hdpi目录下,重新运行代码,则打印日志如下:bitmap size is 2560000。可以看出只是移动了图片位置,Bitmap所占用空间竟然上涨了77%。为什么呢?

    实际上BitmapFactory在解析图片的过程中,会根据当前设备屏幕密度和图片所在的drawable目录来做一个对比,根据这个对比来进行缩放操作。具体公式如下:

1.缩放比例   scale  = 当前设备屏幕密度/图片所在drawable目录对应的屏幕密度;

2.Bitmap实际大小 = 宽 * scale * 高* scale * Config对应存储方式所占的像素数

Android中各个drawable目录对应的屏幕密度如下图:

我运行的设备的屏幕密度为320,如果将rodman放到drawable-hdpi目录下,最终的计算公式即:实际占用的内存大小=600 * (320/240)* 600 * (320/240) *4 = 2560000。

注:也可以将图片放到assets目录下,解析这个目录下的图片并不会进行缩放操作。

BitmapRegionDecoder图片分片显示

     针对图片很大的情况,在不压缩的前提下,不建议一次性将整张图片加载到内存,而是采用分片加载的方式来显示内容,然后根据手势操作,放大缩小或者移动图片显示区域。

private void showRegionImage(){
    try{
        InputStream inputStream = getAssets().open("rodman.png");
        //设置显示图片的中心区域
        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(inputSream,false);
        BitmapFactory.Options options = new BitmapFactory.Options();
        Bitmap bitmap = decoder.decodeRegion(new Rect(0,0,200,200),options);
        regionImage.setImageBitmap(bitmap);
    }catch(IOException e){}
}

Bitmap加载优化

      从上面的例子也能看出,一张65kb大小的图片被加载到内存中,竟然占用了2560000个字节,也就是2.5M左右。因此适当时候,我们需要对需要加载的图片进行缩略优化。

  1. 修改图片加载的Config
    修改占用空间少的存储方式可以有效较低图片占用内存。比如通过BitmapFactory.Options的inPreferredConfig选项,将存储方式设置为BitmapFactory.Config.RGB_565。这种存储方式一个像素占用2个字节,所以最终占用内存直接减半。
    另外Options中还有一个inSampleSize参数,可以实现Bitmap采样压缩,这个参数的含义是宽高维度上每隔inSampleSize个像素进行一次采集。因为宽高会进行采样,所以最终图片被缩略4倍。
    BitmapFactory.Option options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    options.inSampleSize = 2;  //宽和高每隔2个像素进行一次采样
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.rodman,options);

     

  2. Bitmap复用
           比如有两张图片A和B,通过点击某一个按钮,需要在ImageView上切换显示这两张图片。每次切换图片,都需要通过BitmapFactory创建一个新的Bitmap对象,当方法执行完毕后,这个Bitmap又会被GC回收,这就造成不断地创建和销毁比较大的内存对象,从而导致频繁GC(或者叫内存抖动)。
          实际上经过第一次显示之后,内存中已经存在了一个Bitmap对象。每次切换图片只是显示的内容不一样,我们可以重复利用已经占用内存的Bitmap空间,修改之后,内存占用始终处于一个水平线状态,具体的做法就是使用Option.inBitmap参数。
    public class BitmapPoolActivity extends AppCompatActivity{
        ImageView poolImage;
        Bitmap reuseBitmap;
        int resIndex;
        int[] resIds = {R.drawable.rodman,R.drawable.rodman2};
        
        @Override
        protected void onCreate(Bundle savedInstanceState){
            setContentView(R.layout.activity_pool);
            poolImage = findViewById(R.id.image);
            final BitmapFactory.Options options = new BitmapFactory.Options();
            //inMutable属性不设置为true的话,不会重复利用Bitmap的内存。
            options.inMutable = true;
            resultBitmap =     BitmapFactory.decodeResource(getResource(),resIds[0],options);
        }
    
        public void switchImage(View view){
            poolImage.setImageBitmap(getBitmap());
        }
        
        private Bitmap getBitmap(){
            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeResource(getResource(),resIds[resIndex%2],options);
            //一定要先调用canUseForInBitmap来判断reuseBitmap是否可以被复用,因为在Android 4.4版本之前,只能重用相同大小的Bitmap,4.4之后,可以重用任何Bitmap的内存区域,只要这块内存比将要分配内存的bitmap大就可以。
            if(canUseForInBitmap(reuseBitmap,options)){
                options.inMutable = true;
                options.inBitmap = reuseBitmap;
            }
            options.inJustDecodeBounds = false;
            return BitmapFactory.decodeResource(getResource(),resIds[resIndex%2],options);
        }
    }

     

Bitmap缓存

      当需要在界面上同时展示一大堆图片的时候,比如ListView、RecyclerView等,由于用户不断地上下滑动,某个Bitmap可能会被短时间内加载并销毁多次。可以通过适当的缓存,可以有效地减缓GC频率保证图片加载效率,提高界面响应速度和流畅性。最常用的缓存方式就是LruCache,基本使用方法如下:

private LruCache<String,Bitmap> bitmapCache;

@Override
protected void onCreate(){
    setContent(R.layout.activity_img_cache);
    //指定LruCache的最大空间为20M,当超过20M,LruCache会根据内部缓存策略将多余Bitmap移出
    int cacheSize = 20*1024*1024;  
    bitmapCache = new Lrucache<String,Bitmap>(cacheSize){
        @Override
        protected int sizeOf(){
            //需要重写此方法,来定义每一Bitmap的大小
            return bitmap.getAllocationByteCount();
        }
    }
}


public void addBitmapTocache(String key ,Bitmap bitmap){
    if(getBitmapFromCache(key) == null){
        bitmapCache.put(key,bitmap);
    }
}

public Bitmap getBitmapFromCache(String key){
    return bitmapCache.get(key);
}

 总结:这节课讲了Bitmap开发中的几个常见问题:

  1. 一张图片被加载成Bitmap后,实际占用多大内存;
  2. 通过Options.inBitmap 可以实现Bitmap的复用,但是又一定的限制;
  3. 当界面要展示多张图片是,尤其是列表,可以考虑使用Bitmap缓存;
  4. 如果需要展示的图片过大,可以考虑使用分片加载的策略。

第15讲:彻底掌握Android touch事件分发时序

关于事件分发,主要有几个方向可以深入分析:

  1. touch事件是如何从驱动层传递给Framework层的InputManagerService;
  2. WMS是如何通过ViewRootImpl将事件传递到目标窗口;
  3. touch事件到达DecorView后,是如何一步步传递到内部的子view中的;

ViewGroup 

     它是一组View组合,在其内部可能包含多个子view,当手指触摸屏幕时,手指所在的区域既能在ViewGroup显示范围内,也可能在其内部子view 控件上。因此,它内部的事件分发的重心是处理当前Group和子View之间的逻辑关系:

  1. 当前Group是否需要拦截touch事件;
  2. 是否需要将touch事件继续分发给子View;
  3. 如何将touch事件分发给子view;

View

View是一个单纯的控件,内部也不会存在子View,所以它的事件分发的重点在于当前view如何处理touch事件,并根据相应的手势逻辑进行一些列的效果展示(比如滑动,放大,点击,长按等):

  1. 是否存在TouchListener;
  2. 是否自己接收处理touch事件(主要逻辑在onTouchEvent方法中);

事件分发核心dispatchTouchEvent

整个View之间的事件分发,实质上就是一个大的递归函数,而这个递归函数就是dispatchTouchEvent方法。在这个递归过程中会适时调用onInterceptTouchEvent来拦截事件,或者调用onTouchEvent方法来处理事件。从宏观角度,纵览整个dispatch的源码如下:

public boolean dispatchTouchEvent(){
    //步骤1:检查当前ViewGroup是否需要拦截
    ...
    //步骤2:将事件分发给子View
    ...
    //步骤3:根据mFirstTouchTarget,再次分发事件
    ...
}

dispatch主要分为3大步骤:

  • 步骤1:判断当前ViewGroup是否需要拦截此事件,如果需要则此touch事件不再会传递给子view(或者以CANCEL的方式通知子View);
  • 步骤2:如果没有拦截,则将事件分发给子View继续处理,如果子view将此事件捕获,则将mFirstTouchTarget赋值给捕获touch事件的View;如果子view一直没有拦截,则会调用super.dispatchTouchEvent方法,最终调用自身的onTouchEvent方法。
  • 步骤3:根据mFirstTouchTarget重新分发事件;

为什么Down事件特殊

      所有touch事件都是从DOWN事件开始的,而且DOWN事件的处理结果会直接影响后续MOVE、UP事件的逻辑。在步骤2中,只有DOWN事件会传递给子View进行捕获判断,一旦子View捕获成功,后续的MOVE和UP事件是通过遍历mFirstTouchTarget链表,查找之前接受ACTION_DOWN的子View,并将触摸事件分配给这些子View。也就是说,后续的MOVE、UP等事件的分发交给谁,取决于它们的起始事件DOWN是由谁捕获的。

mFirstTouchTarget

mFirstTouchTarget的部分源码如下所示:

private static final class TouchTarget{
    public static final int ALL_POINTER_IDS = -1;
    //The touched child view
    public View child;
    public int pointerIdBits;
    public TouchTarget next;
    
    private TouchTarget(){}
}

     可以看出,其实mFirstTouchTarget是一个TouchTarget类型的链表结构,而这个TouchTarget的作用就是用来记录捕获了DOWN事件的View,具体保存在上图中的child变量。可为什么是链表类型的结构呢?因为Android设备是支持多指操作的,每一个手指的DOWN事件都可以当做一个TouchTarget保存起来。在步骤3中判断mFirstTouchTarget不为null,则再次将事件分发给相应的TouchTarget。

容易遗漏的CANCEL事件

      当父视图的onInterceptTouchEvent先返回false,然后在子view的dispatchTouchEvent中返回true(表示子View捕获事件),关键步骤就是在接下来MOVE的过程中,父视图的onInterceptTouchEvent又返回true,intercepted被重置为true,子控件就会收到ACTION_CANCEL的touch事件。

     有个很经典的例子可以用来演示这种情况,当在ScrollView中添加自定义View时,ScrollView默认在DOWN事件中并不会进行拦截,事件会被传递到子控件。只有当手指进行滑动并达到一定的距离之后,onInterceptTouchEvent方法返回true,并触发ScrollView的滚动效果。当ScrollView进行滚动的瞬间,内部的子View会接收到一个CANCEL事件,并失去touch焦点。

    因此,我们平时自定义View时,尤其是有可能被ScrollView或者ViewPager嵌套使用的控件,不要遗漏对CANCEL事件的处理,否则有可能引起UI显示异常。

总结:

本课时重点分析了dispatchTouchEvent的事件流程机制,这一过程主要分为3部分:

  1. 判断是否需要拦截,主要是根据onInterceptTouchEvent方法的返回值来决定是否拦截;
  2. 在DOWN事件中将touch事件分发给子view,这一过程如果有子view捕获了touch事件,会对mFirstTouchTarget进行赋值;
  3. 最后,DOWN、MOVE、UP事件都会根据mFirstTouchTarget是否为null,决定自己处理touch事件,还是再次分发给子view。

然后介绍了事件分发中的几个特殊的点:

  1. DOWN事件的特殊之处:事件的起点;决定后续事件由谁来消费处理;
  2. MFirstTouchTarget的作用:记录捕获消费touch事件的view,是一个链表结构;
  3. CANCEL事件的触发场景:当父视图先不拦截,然后在MOVE事件中重新拦截,此时子view会接收到一个CANCEL事件。

第16讲:Android如何自定义View?

自定义UI控件有2种方式:

  1. 继承系统提供的成熟控件(比如LinearLayout、RelativeLayout、ImageView等);
  2. 直接继承自系统View或者ViewGroup,并自绘显示内容;

自定义属性的步骤:

  1. 在res的values目录下的attrs.xml文件中,使用标签自定义属性,如下所示:
    <declare-styleable name="CustomToolBar">
        <attr name="titleText" format="string | reference" />
        <attr name="myTitleTextColor" format="color | reference" />
        <attr name="titleTextSize" format="dimension | reference" />
        <attr name="leftImageSrc" format="reference" />
    </declare-styleable>
    
    //属性的类型有以下七种:reference(参考某一资源id,设置图片时用)、color、String、float、integer、boolean、dimension。

     

  2. 在Java类里,获取自定义属性的引用值,主要是通过obtainStyleAttributes方法获取到自定义属性的集合,然后从这个集合中取出相应的自定义属性。
    TypeArray ta = context.obtainStyledAttributes(attrs,R.styleable.CustomToolBar);
    String titleText = ta.getString(R.styleable.CustomToolBar_titleText);
    ta.getColor(R.styleable.CustomToolBar_myTitleTextColor,Color.BLACK);
    ta.getDimension();
    ta.getDrawable();

     

直接继承自View或者ViewGroup需要在对应方法里解决以下几个问题:

  1. onMeasure:如何控制自定义控件的大小,也就是宽和高分别设置多少;
  2. onLayout:如果是ViewGroup,如何合理安排其内部子View的摆放位置;
  3. onDraw:如何根据响应的属性,将UI元素绘制到界面上;

      自定义View的重点工作就是复写并合理的实现3个方法,并不是每一个自定义View都需要实现这3个方法,大多数情况下只需要实现其中的2个甚至1个方法也能满足需求。

onMeasure

        Android提供了wrap_content和match_parent属性来规范控件的显示规则,他们分别代表自适应大小和填充父视图的大小,但这两个属性并没有指定具体的大小,因此我们需要在onMeasure方法中过滤出这两种情况,真正测量出自定义View应该显示的宽高大小。

父视图传递给子View的两个参数,withMeasureSpec和heightMeasureSpec(1个int类型,二进制的高两位表示测量模式,低30位表示宽高具体大小),他们一共有3种测量模式:

  1. EXACTLY:表示XML布局文件中宽高使用match_parent或者固定大小的宽高;
  2. AT_MOST:   表示在XML布局中宽高使用wrap_content;
  3. UNSPECIFIED:父容器没有对当前View有任何限制,当前View可以取任意尺寸,比如ListView中的item。

自定义View的时候,如果没有重写onMeasure方法,则默认调用父类也就是View中的onMeasure方法,如下:

protected void onMeasure(int withMeasureSpec, int heightMeasureSpec){
  
 setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
}

//默认为父视图的剩余可用空间。

如果继承自ViewGroup,需要先确定内部子View所占大小,然后才能确定自己的大小。比如我们熟悉的FlowLayout的onMeasure方法如下:

protect void onMeasure(int widthMeasureSpec, int heightMeasureSpec){

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //获取宽高的测量模式、测量值
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int childCount = getChildCount();
    //记录每一行View的总宽度
    int totalLineWidth = 0;
    //记录每一行最高View的高度
    int perLineMaxHeight = 0;
    //记录当前ViewGroup的总高度
    int totalHeight = 0;
    for(int i= 0; i<childCount; i++){

        View childView = getChildAt(i);
        //对子View进行测量
        measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
        //获得子View的测量宽度
        int childWidth = childView.getMeasureWidth()+lp.leftMargin+lp.rightMarin;
        //获得子View的测量高度
        int childHeight = childView.getMeasureHeight()+ lp.topMargin +lp.bottomMargin;
        if(totalLineWidth + childWidth > widthSize){
            //统计总高度
            totalHeight += perLineMaxHeight;
            //开启新的一行
            totalLineWidth = childWidth;
            perLineMaxHeight = childHeight;
        }else{
            //记录每一行的总宽度
            totalLineWidth += childWidth;
            //比较每一行最高的View
            perLineMaxHeight = Math.max(perLineMaxHeight, childHeight);
        }
        //当该View已是最后一个View时,将该行最大高度添加到totalHeight中
        if(i==childCount-1){
            totalHeight += perLineMaxHeight;
        }
    }
    //如果高度测量模式是EXACTLY,则高度用测量值,否则用计算出来的总高度
    heightSize = heightMode== MeasureSpec.EXACTLY ? heightSize : totalHeight;
    setMeasureDimension(widthSize , heightSize);
}

onLayout

ViewGroup中的onLayout是一个抽象方法,也就是说每一个自定义ViewGroup都必须主动实现如何排布子View,具体就是遍历每一个子View,调用child(l,t,r,b)方法来为每一个子View设置具体的布局位置。一个简易的FlowLayout如下:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mAllViews.clear();
        mPerLineMaxHeight.clear();
        //存放每一行的子View
        List<View> lineViews = new ArrayList<>();
        //记录每一行已存放View的总宽度
        int totalLineWidth = 0;
        //记录每一行最高View的高度
        int lineMaxHeight = 0;
        /****遍历所有View,将View添加到List<List<View>>集合中**********/
        //获得子View的总个数
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (totalLineWidth + childWidth > getWidth()) {
                mAllViews.add(lineViews);
                mPerLineMaxHeight.add(lineMaxHeight);
                //开启新的一行
                totalLineWidth = 0;
                lineMaxHeight = 0;
                lineViews = new ArrayList<>();
            }
            totalLineWidth += childWidth;
            lineViews.add(childView);
            lineMaxHeight = Math.max(lineMaxHeight, childHeight);
        }
        //单独处理最后一行
        mAllViews.add(lineViews);
        mPerLineMaxHeight.add(lineMaxHeight);
        /************遍历集合中的所有View并显示出来************/
        //表示一个View和父容器左边的距离
        int mLeft = 0;
        //表示View和父容器顶部的距离
        int mTop = 0;
        for (int i = 0; i < mAllViews.size(); i++) {
            //获得每一行的所有View
            lineViews = mAllViews.get(i);
            lineMaxHeight = mPerLineMaxHeight.get(i);
            for (int j = 0; j < lineViews.size(); j++) {
                View childView = lineViews.get(j);
                MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
                int leftChild = mLeft + lp.leftMargin;
                int topChild = mTop + lp.topMargin;
                int rightChild = leftChild + childView.getMeasuredWidth();
                int bottomChild = topChild + childView.getMeasuredHeight();
                //四个参数分别表示View的左上角和右下角
                childView.layout(leftChild, topChild, rightChild, bottomChild);
                mLeft += lp.leftMargin + childView.getMeasuredWidth() + lp.rightMargin;
            }
            mLeft = 0;
            mTop += lineMaxHeight;
        }
    }

onDraw

      onDraw方法接收一个canvas类型的参数,canvas可以理解为一个画布,在这块画布上可以绘制各种类型的UI元素。系统提供了一系列Canvas操作方法,如下所示:

void drawRect(RectF rect ,Paint paint);  //绘制
void drawOval(RectF oval, Pinat paint);   //绘制椭圆
void drawCircle(float cx, float cy, float radius,Paint paint); //绘制圆
void drawPath(Path path , Paint paint); //绘制Path路径
void drawLine(float startX,float startY,float stopX,float stopY,Paint paint); //绘制连线
void drawPoint(float x,float y,Paint paint); //绘制点
void drawArc(RectF oval,float startAngle,float sweepAngle,booean useCenter,Paint paint); //绘制弧形

     从图中可以看出,Canvas中每一个绘制操作都需要传入一个Paint对象,Paint就相当于一个画笔,我们可以通过设置画笔的各种属性,来实现绘制不同效果:

setStyle(Style style)  //设置绘制模式
setColor(int color)    //设置颜色
setAlpha(int a)        //设置透明度
setShader(Shader shader) //设置Paint填充效果
setStrokeWidth(float width)  //设置线条宽度
setTextSize(float textSize)  //设置文字大小
setAntiAlias(boolean aa)   //设置抗锯齿开关
setDither(boolean dither)  //设置防抖动开关

总结:

  1. onMeasure:主要负责测量自定义控件的宽高;
  2. onLayout:主要在自定义ViewGroup中复写,并实现子View的显示位置,并在其中介绍了自定义属性的使用方法;
  3. onDraw:主要负责绘制UI元素;

第17讲:为什么RecyclerView能完美替代ListView?

RecyclerView常用方法:

  1. setLayoutManager:必选项,设置RV的布局管理器,决定RV的显示风格,常用的有LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager。
  2. setAdapter:必选项,设置数据适配器。
  3. addItemDecoration:非必选,设置RV中item的装饰器,经常用来设置item的分割线。
  4. setItemAnimator:非必选,设置RV中item的动画。

      本节课主要将RV是如何一步步将每个ItemView显示到屏幕上,然后再分析,在显示和滑动过程中,是如何通过缓存复用来提升整体性能的。RV也是自定义控件,所有也符合自定义控件的规则,因此我们也可以沿着其onMeasure、onLayout、onDraw这3个方法路线来深入研究。

onMeasure

       如果RV的宽高被设置为Match_parent或者具体值,那么直接调用传入的LayoutManager的onMeasure方法来测量自身宽高,如果设置为wrap_content,则会调用dispatchLayoutStep2()方法,其实就是测量RV子view的大小,最终确定RV的实际宽高。

onLayout

      很简单,其onLayout方法中其实就调用了一个dispatchLayout()方法,在这个方法里,如果onMeasure阶段没有执行dispatchLayoutStep2()方法去测量子View,则会在onLayout阶段重新执行。在dispatchLayoutStep2()方法中调用了一个主要的方法mLayout.onLayoutChildren(mRecycler,mState);这个方法是LayoutManager方法中的一个空方法,主要作用是测量RV内的子View的大小,并确定他们所在的位置,LinearLayoutManager、GridLayoutManager、StaggeredLayoutManager都分别复写了这个方法,并实现了不同方式的布局。

onDraw

public void onDraw(Canvas c){
    super.onDraw(c);
    final int count = mItemDecorations.size();
    for(int i=0; i<count; i++){
        mItemDecorations.get(i).onDraw(c,this,mState);
    }
}

这个方法也很简单,就是如果有ItemDecoration,就循环调用所有Decoration的onDraw方法,将其显示。至于所有的子ItemView则是通过Android渲染机制递归的调用子ItemView的draw方法显示到屏幕上。

小结:RV会将测量onMeasure和布局onLayout的工作委托为LayoutManager来执行,不容的LayoutManager会有不同的风格布局显示,这是一种策略模式。用一张图来描述这段过程如下:

缓存复用原理Recycler

      缓存复用是RV中另一个非常重要的机制,这套机制主要实现了ViewHolder的缓存以及复用。核心代码是在RV中的一个内部类Recycler中完成,主要用来缓存屏幕内ViewHolder以及部分屏幕外ViewHolder,部分代码如下:

public final class Recycler{
    
    //第一级缓存 mAttachedScrap、mChangedScrap
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;

    //第二级缓存 mCachedViews    
    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
    
    //第三级缓存 mViewCacheExtension
    private ViewCacheExtension mViewCacheExtension;
    
    //第四级缓存 mRecyclerPool
    RecycledViewPool mRecyclerPool;
}

RV之所以将缓存分为这么多块,是为了在功能上进行一些区分,并分别对应不同的场景。下面逐一分析:

  1. 第一级缓存mAttachedScrap & mChangedScrap :是两个名为Scrap的ArrayList,这两者主要用来缓存屏幕内的ViewHolder。为什么屏幕内ViewHolder也需要缓存?我们做下拉刷新,当刷新被触发时,只需要在原有ViewHolder基础上进行重新绑定新的数据data即可,而这些就的ViewHolder就是被保存在这两个集合中。实际上,当我们调用RV的notifyXXX方法时,就会向这两个列表填充,将旧ViewHolder缓存起来。
  2. 第二级缓存 mCachedViews  它用来缓存屏幕之外的ViewHolder,默认情况下缓存个数是2,不过可以通过setViewCacheSize方法来改变缓存的容量大小。如果mCachedViews的容量已满,则会根据FIFO的规则将旧ViewHolder抛弃,然后添加新的ViewHolder。
  3. 第三级缓存 ViewCacheExtension  这是RV预留给开发人员的一个抽象类,在这个类中只有一个抽象方法,开发者可以自己继承该类,复写抽象方法getViewForPositionAndType来实现自己的缓存机制。除非对RV源码很熟悉,否则不建议添加自己的缓存逻辑。
  4. 第四级缓存 RecycledViewPool  同样是用来缓存屏幕外的ViewHolder,当mCachedViews的个数已满,则从mCachedViews淘汰出来的ViewHolder会先缓存到RecycledViewPool中。ViewHolder在缓存到RecycledViewPool之前,会将内部数据清理,因此从RecycledViewPool中取出来的ViewHolder需要重新调用onBindViewHolder绑定数据。这就同ListView中使用ViewHolder复用convertView的道理是一致的,因此RV也算是将ListView的优点完美的继承过来了。

何时将ViewHolder存入缓存

下面是ViewHolder被存入各级缓存中的场景:

  1. 第一次layout:当调用setLayoutManager 和setAdapter 之后,RV会经历第一次Layout并显示到屏幕上。此时并不会有任何ViewHolder的缓存,都是通过createViewHolder创建的。
  2.  刷新列表:当我们通过手势下拉刷新,获取到新的数据之后,我们会调用notifyXXX方法取通知RV数据发生改变,这会RV会先将屏幕内的ViewHolder保存在Scrap中,然后通过相应的position从该缓存中获取旧的ViewHolder,然后将新数据设置上。

RV如何从缓存中获取ViewHolder

     上文中介绍onLayout阶段时,有讲到在layoutChunk方法中通过调用layoutState.next方法拿到某个子ItemView,然后添加到RV中。在该方法中最终调用的是tryGetViewHolderForPositionByDeadline()方法来查找相应位置上的ViewHolder,在这个方法中会从上面介绍的4级缓存中依次查找,最后如果还找不到,就调用createViewHolder方法创建一个新的ViewHolder。

总结:本课分析了RecyclerView源码中2块核心实现:

  1. RecyclerView是如何经过测量、布局并最终绘制到屏幕上的,其中大部分工作都是委托给LayoutManager来实现。
  2. RecyclerView的缓存复用机制,只要是通过内部类Recycler来实现。

第18讲:OKHTTP全面讲解

首先看下OKHTTP的基本使用:

两种写法:

第一种
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
client.newCall(request).enqueue(new Callback(){
    @Override
    public void onFailure(Call call ,IOException e){
    }
    @Override
    public void onResponse(Call call,Response response){
    }
});


第二种 还可以使用内部工厂类Builder来设置OkHttpClient。
OkHttpClient.Builer builder = new OkHttpClient.Builder();
builder.connectTimeout(60,TimeUnit.SECOND)
    .addInterceptor(interceptor)
    .proxy(proxy)
    .cache(cache);

OkHttpClient client = builder.build();

    其中,newCall()方法返回一个RealCall类型的对象,通过它将网络请求操作添加到请求队列中,也就是RealCall.enqueue()方法。这个方法内部最终是委托给Dispatcher的enqueue方法内部实现的。

    Dispatcher是OkHttpClient的调度器,是一种门户模式。主要用来实现执行、取消异步请求操作。本质上是内部维护了一个线程池去执行异步操作,并且在Dispatcher内部根据一定的策略,保证最大并发数、统一host主机允许执行请求的线程个数等。

拦截器调用链中重涉及的几个拦截器:

  • BridgeInterceptor:主要对Request中的Head设置默认值,比如Content-Type、Keep-Alive、Cookie等。
  • CacheInterceptor:负责HTTP请求的缓存处理。
  • ConnectInterceptor:负责与服务器地址之间连接,也就是TCP连接。
  • CallServerInterceptor:负责向服务器发起请求,并从服务器拿到远端数据结果。(最核心的网络请求部分)

总结:

  • 首先OkHttp内部是一个门户模式,所有的下发工作都是通过一个门户Dispatcher来进行分发。
  • 然后,在网络请求阶段通过责任链模式,链式的调用各个拦截器的intercept方法。其中比较重要的拦截器:CacheInterceptor、CallServerInterceptor,他们分别用来做请求缓存和执行网络请求操作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值