补:《Android面试题思考与解答》11月刊(三)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 又来更新啦,Android面试题《思考与解答》11月刊奉上。

具体说下这几个元注解都是怎么用的


  • @Target


target,表示注解对象的作用范围,比如Override注解所标示的就是ElementType.METHOD,即所作用的范围是方法范围,也就是只能在方法头上加这个注解。另外还有以下几个修饰范围参数:


  • TYPE:类、接口、枚举、注解类型。
  • FIELD:类成员(构造方法、方法、成员变量)。
  • METHOD:方法。
  • PARAMETER:参数。
  • CONSTRUCTOR:构造器。
  • LOCAL_VARIABLE:局部变量。
  • ANNOTATION_TYPE:注解。
  • PACKAGE:包声明。
  • TYPE_PARAMETER:类型参数。
  • TYPE_USE:类型使用声明。


比如ANNOTATION_TYPE就是表示该注解的作用范围就是注解,哈哈,有点绕吧,看看Target注解的代码:


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}


带了一个ElementType类型的参数,也就是上面说到的作用范围参数,另外还被Target注解修饰了,传的参数就是ANNOTATION_TYPE,也就是我注解我自己,我设置我自己的作用范围是注解。大家自己绕一下。。


  • @Retention


表示注解保留的生命周期,或者说表示该注解所保留的时长,主要有以下几个可选参数:


  • SOURCE:仅存在Java源文件,经过编译器后便丢弃相应的注解。适用于一些检查性的操作,比如@Override。
  • CLASS:编译class文件时生效,存在Java源文件,以及经编译器后生成的Class字节码文件,但在运行时VM不再保留注释。这个也是默认的参数。适用于在编译时进行一些预处理操作,比如ButterKnife的@BindView,可以在编译时生成一些辅助的代码或者完成一些功能。
  • RUNTIME:存在源文件、编译生成的Class字节码文件,以及保留在运行时VM中,可通过反射性地读取注解。适用于一些需要运行时动态获取注解信息,类似反射获取注解等。


  • @Inherited


表示注解类型能被类自动继承。这里需要注意两点:


  • 。也就是说只有在类集成关系中,子类才会集成父类使用的注解中被@Inherited所修饰的那个注解。其他的接口集成关系,类实现接口关系中,都不会存在自动继承注解。
  • 自动继承。也就是说如果父类有@Inherited所修饰的那个注解,那么子类不需要去写这个注解,就会自动有了这个注解。


还是看个例子:


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface MyInheritedAnnotation {
 //注解1,有Inherited注解修饰
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
 //注解2,没有Inherited注解修饰
}
@MyInheritedAnnotation
@MyAnnotation
public class BaseClass {
 //父类,有以上两个注解
}
public class ExtendClass extends BaseClass  {
 //子类会继承父类的MyInheritedAnnotation注解,
 //而不会继承MyAnnotation注解
}


  • @Documented


表示拥有该注解的元素可通过javadoc此类的工具进行文档化,也就是说生成JavaAPI文档的时候会被写进文档中。


注解可以用来做什么


主要有以下几个用处:


  • 降低项目的耦合度。
  • 自动完成一些规律性的代码
  • 自动生成java代码,减轻开发者的工作量。


序列化指的是什么?有什么用


序列化指的是讲对象变成有序的字节流,变成字节流之后才能进行传输存储等一系列操作。反序列化就是序列化的相反操作,也就是把序列化生成的字节流转为我们内存的对象。


介绍下Android中两种序列化接口


  • Serializable


Java提供的一个序列化接口,是一个空接口,专门为对象提供序列化和反序列化操作。具体使用如下:


public class User implements Serializable {
    private static final long serialVersionUID=519067123721561165l;
    private int id;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
}


实现Serializable接口,声明一个serialVersionUID


到这里可能有人就问了,不对啊,平时没有这个serialVersionUID啊。没错,serialVersionUID不是必须的,因为不写的话,系统会自动生成这个变量。它有什么用呢?当序列化的时候,系统会把当前类的serialVersionUID写入序列化的文件中,当反序列化的时候会去检测这个serialVersionUID,看他是否和当前类的serialVersionUID一致,一样则可以正常反序列化,如果不一样就会报错了。


所以这个serialVersionUID就是序列化和反序列化过程中的一个标识,代表一致性。不加的话会有什么影响?如果我们序列化后,改动了这个类的某些成员变量,那么serialVersionUID就会改变,这时候再拿之前序列化的数据来反序列化就会报错。所以如果我们手动指定serialVersionUID就能保证最大限度来恢复数据。


  • Parcelable


Android自带的接口,使用起来要麻烦很多:需要实现Parcelable接口,重写describeContents(),writeToParcel(Parcel dest, @WriteFlags int flags),并添加一个静态成员变量CREATOR并实现Parcelable.Creator接口


public class User implements Parcelable {
    private int id;
    protected User(Parcel in) {
        id = in.readInt();
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
    }
    @Override
    public int describeContents() {
        return 0;
    }
    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel in) {
            return new User(in);
        }
        @Override
        public User[] newArray(int size) {
            return new User[size];
        }
    };
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
}


  • createFromParcel,User(Parcel in) ,代表从序列化的对象中创建原始对象
  • newArray,代表创建指定长度的原始对象数组
  • writeToParcel,代表将当前对象写入到序列化结构中。
  • describeContents,代表返回当前对象的内容描述。如果还有文件描述符,返回1,否则返回0。


两者有什么区别,该怎么使用选择


Serializable是Java提供的序列化接口,使用简单但是开销很大,序列化和反序列化过程都需要大量I/O操作。Parcelable是Android中提供的,也是Android中推荐的序列化方式。虽然使用麻烦,但是效率很高。


所以,如果是内存序列化层面,那么还是建议Parcelable,因为他效率会比较高。如果是网络传输和存储磁盘情况,就推荐Serializable,因为序列化方式比较简单,而且Parcelable不能保证,当外部条件发生变化时数据的连续性。


  • Serializable


Serializable的实质其实是是把Java对象序列化为二进制文件,然后就能在进程之间传递,并且用于网络传输或者本地存储等一系列操作,因为他的本质就存储了文件。可以看看源码:


private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
    ...
    try {
        Object orig = obj;
        Class<?> cl = obj.getClass();
        ObjectStreamClass desc;
        desc = ObjectStreamClass.lookup(cl, true);
        if (obj instanceof Class) {
            writeClass((Class) obj, unshared);
        } else if (obj instanceof ObjectStreamClass) {
            writeClassDesc((ObjectStreamClass) obj, unshared);
        // END Android-changed:  Make Class and ObjectStreamClass replaceable.
        } else if (obj instanceof String) {
            writeString((String) obj, unshared);
        } else if (cl.isArray()) {
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum<?>) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
    } 
    ...
}
private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
        ...
        try {
            desc.checkSerialize();
            //写入二进制文件,普通对象开头的魔数0x73
            bout.writeByte(TC_OBJECT);
            //写入对应的类的描述符,见底下源码
            writeClassDesc(desc, false);
            handles.assign(unshared ? null : obj);
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }
    public long getSerialVersionUID() {
        // 如果没有定义serialVersionUID,序列化机制就会调用一个函数根据类内部的属性等计算出一个hash值
        if (suid == null) {
            suid = AccessController.doPrivileged(
                new PrivilegedAction<Long>() {
                    public Long run() {
                        return computeDefaultSUID(cl);
                    }
                }
            );
        }
        return suid.longValue();
    }


可以看到是通过反射获取对象以及对象属性的相关信息,然后将数据写到了一个二进制文件,并且写入了序列化协议版本等等。而获取·serialVersionUID·的逻辑也体现出来,如果id为空则会生成计算一个hash值。


  • Parcelable


Parcelable的存储是通过Parcel存储到内存的,简单地说,Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象。


这其中实际又是通过native方法实现的。具体逻辑我就没有去分析了,如果有大神朋友可以在评论区解析下。


当然,Parcelable也是可以持久化的,涉及到Parcel中的unmarshallmarshall方法。这里简单贴一下代码:


protected void saveParce() {
        FileOutputStream fos;
        try {
            fos = getApplicationContext().openFileOutput(TAG,
                    Context.MODE_PRIVATE);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            Parcel parcel = Parcel.obtain();
            parcel.writeParcelable(new ParceData(), 0);
            bos.write(parcel.marshall());
            bos.flush();
            bos.close();
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    protected void loadParce() {
        FileInputStream fis;
        try {
            fis = getApplicationContext().openFileInput(TAG);
            byte[] bytes = new byte[fis.available()];
            fis.read(bytes);
            Parcel parcel = Parcel.obtain();
            parcel.unmarshall(bytes, 0, bytes.length);
            parcel.setDataPosition(0);
            ParceData data = parcel.readParcelable(ParceData.class.getClassLoader());
            fis.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


序列化总结


1)对于内存序列化方面建议用Parcelable,为什么呢?


  • 因为Serializable是存储了一个二进制文件,所以会有频繁的IO操作,消耗也比较大,而且用到了大量反射,反射操作也是耗时的。相比之下Parcelable就要效率高很多。


2)对于数据持久化还是建议用Serializable,为什么呢?


  • 首先,Serializable本身就是存储到二进制文件,所以用于持久化比较方便。而Parcelable序列化是在内存中操作,如果进程关闭或者重启的时候,内存中的数据就会消失,那么Parcelable序列化用来持久化就有可能会失败,也就是数据不会连续完整。而且Parcelable还有一个问题是兼容性,每个Android版本可能内部实现都不一样,知识用于内存中也就是传递数据的话是不影响的,但是如果持久化可能就会有问题了,低版本的数据拿到高版本可能会出现兼容性问题。所以还是建议用Serializable进行持久化。


3)Parcelable一定比Serializable快吗?


  • 有个比较有趣的例子是:当序列化一个超级大的对象图表(表示通过一个对象,拥有通过某路径能访问到其他很多的对象),并且每个对象有10个以上属性时,并且Serializable实现了writeObject()以及readObject(),在平均每台安卓设备上,Serializable序列化速度大于Parcelable 3.6倍,反序列化速度大于1.6倍.


具体原因就是因为Serilazable的实现方式中,是有缓存的概念的,当一个对象被解析过后,将会缓存在HandleTable中,当下一次解析到同一种类型的对象后,便可以向二进制流中,写入对应的缓存索引即可。但是对于Parcel来说,没有这种概念,每一次的序列化都是独立的,每一个对象,都当作一种新的对象以及新的类型的方式来处理。


LruCache介绍


LruCache 是Android3.1提供的一个缓存类,用于数据缓存,一般用于图片的内存缓存。Lru的英文是Least Recently Used,也就是近期最少使用算法,核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。


当我们进行网络加载图片的时候,肯定要对图片进行缓存,这样下次加载图片就可以直接从缓存中取。三级缓存大家应该都比较熟悉,内存,硬盘和网络。所以一般要进行内存缓存和硬盘缓存,其中内存缓存就是用的LruCache。


LruCache使用


public class MyImageLoader {
    private LruCache<String, Bitmap> mLruCache;
    public MyImageLoader() {
        int maxMemory = (int) (Runtime.getRuntime().maxMemory())/1024;
        int cacheSize = maxMemory / 8;
        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes()*value.getHeight()/1024;
            }
        };
    }
    /**
     * 添加图片缓存
     */
    public void addBitmap(String key, Bitmap bitmap) {
            mLruCache.put(key, bitmap);
    }
    /**
     * 从缓存中获取图片
     *
     */
    public Bitmap getBitmap(String key) {
        return mLruCache.get(key);
    }
}


使用方法如上,只需要提供缓存的总容量大小并重写sizeOf方法计算缓存对象大小即可。这里总容量的大小也是通用方法,即进程可用内存的1/8,单位kb。然后就可以使用put方法来添加缓存对象,get方法来获取缓存对象。


LruCache原理


原理其实也很简单,就是用到了LRU算法,内部使用LinkedHashMap 进行存储。在缓存满了之后,会将最近最少使用的元素移除。怎么保证找到这个最近最少的元素呢?就是每次使用get方法访问了元素或者增加了一个元素,就把元素移动到LinkedHashMap的尾部,这样第一个元素就是最不经常使用的元素,在容量满了之后就可以将它移除。


简单看看源码:


public LruCache(int maxSize) {
       if (maxSize <= 0) {
           throw new IllegalArgumentException("maxSize <= 0");
       }
       this.maxSize = maxSize;
       this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
   }
   public final V put(K key, V value) {
       if (key == null || value == null) {
           throw new NullPointerException("key == null || value == null");
       }
       V previous; //查找是否已经存在key对应的元素
       synchronized (this) {
           putCount++;
           //计算entry的大小
           size += safeSizeOf(key, value); 
           previous = map.put(key, value);
           if (previous != null) {
             //如果之前存在,这先减去之前那个entry所占用的内存大小
               size -= safeSizeOf(key, previous);
           }
       }
       if (previous != null) {
       //如果之前存在则调用entryRemoved回调子类重写的此方法,做一些处理
           entryRemoved(false, key, previous, value);
       }
       //根据最大的容量,计算是否需要淘汰掉最不常使用的entry
       trimToSize(maxSize);
       return previous;
   }
    public final V get(K key) {
      if (key == null) {
          throw new NullPointerException("key == null");
      }
      V mapValue;
      //根据key来查询符合条件的etnry
      synchronized (this) {
          mapValue = map.get(key);
          if (mapValue != null) {
              hitCount++;
              return mapValue;
          }
          missCount++;
      }
      /*
       * Attempt to create a value. This may take a long time, and the map
       * may be different when create() returns. If a conflicting value was
       * added to the map while create() was working, we leave that value in
       * the map and release the created value.
       */
      V createdValue = create(key);
      if (createdValue == null) {
          return null;
      }
      synchronized (this) {
          createCount++;
          //mapValue返回的是已经存在相同key的entry
          mapValue = map.put(key, createdValue);
          if (mapValue != null) {
              // There was a conflict so undo that last put
              map.put(key, mapValue);
          } else {
              size += safeSizeOf(key, createdValue);
          }
      }
      if (mapValue != null) {
          entryRemoved(false, key, createdValue, mapValue);
          return mapValue;
      } else {
          trimToSize(maxSize);
          return createdValue;
      }
  }


其实可以看到,LruCache类本身做的事情不多,限定了缓存map的大小,然后利用了LinkHashMap完成了LRU的缓存策略。所以主要的实现LRU逻辑部分还是在LinkHashMap中。LinkedHashMap是hashmap和链表的结合体,通过链表来记录元素的顺序和链接关系,通过HashMap来存储数据,它可以控制元素的被遍历时候输出的顺序。他是一个双向链表,上面说过他会把最近访问的元素放到队列的尾部,有兴趣的可以看看LinkHashMap的源码。


Activity从创建到我们看到界面,发生了哪些事


  • 首先是通过setContentView加载布局,这其中创建了一个DecorView,然后根据然后根据activity设置的主题(theme)或者特征(Feature)加载不同的根布局文件,最后再通过inflate方法加载layoutResID资源文件,其实就是解析了xml文件,根据节点生成了View对象。流程图:


29.png


  • 其次就是进行view绘制到界面上,这个过程发生在handleResumeActivity方法中,也就是触发onResume的方法。在这里会创建一个ViewRootImpl对象,作为DecorView的parent然后对DecorView进行测量布局和绘制三大流程。流程图:


28.png


Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?


  • PhoneWindow是Window 的唯一子类,每个Activity都会创建一个PhoneWindow对象,你可以理解它为一个窗口,但不是真正的可视窗口,而是一个管理类,是Activity和整个View系统交互的接口,是Activity和View交互系统的中间层。
  • DecorView是PhoneWindow的一个内部类,是整个View层级的最顶层,一般包括标题栏和内容栏两部分,会根据不同的主题特性调整不同的布局。它是在setContentView方法中被创建,具体点来说是在PhoneWindow的installDecor方法中被创建。
  • ViewRootImpl是DecorView的parent,用来控制View的各种事件,在handleResumeActivity方法中被创建。


requestLayout和invalidate


  • requestLayout方法是用来触发绘制流程,他会会一层层调用 parent 的requestLayout,一直到最上层也就是ViewRootImpl的requestLayout,这里也就是判断线程的地方了,最后会执行到performMeasure -> performLayout -> performDraw 三个绘制流程,也就是测量——布局——绘制。


@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();//执行绘制流程
        }
    }


其中performMeasure方法会执行到View的measure方法,用来测量大小。performLayout方法会执行到view的layout方法,用来计算位置。performDraw方法需要注意下,他会执行到view的draw方法,但是并不一定会进行绘制,只有只有 flag 被设置为 PFLAG_DIRTY_OPAQUE 才会进行绘制。


  • invalidate方法也是用来触发绘制流程,主要表现就是会调用draw()方法。虽然他也会走到scheduleTraversals方法,也就是会走到三大流程,但是View会通过mPrivateFlags来判断是否进行onMeasure和onLayout操作。而在用invalidate方法时,更新了mPrivateFlags,所以不会进行measure和layout。同时他也会设置Flag为PFLAG_DIRTY_OPAQUE,所以肯定会执行onDraw方法。


private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        //...
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();//执行绘制流程
        }
    }


最后看一下scheduleTraversals方法中三大绘制流程逻辑,是不是我们之前说的那样,FORCE_LAYOUT标志才会onMeasure和onLayout,PFLAG_DIRTY_OPAQUE标志才会onDraw:


public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    // 只有mPrivateFlags为PFLAG_FORCE_LAYOUT的时候才会进行onMeasure方法
    if (forceLayout || needsLayout) {
      onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    // 设置 LAYOUT_REQUIRED flag
    mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
  }
  public void layout(int l, int t, int r, int b) {
    ...
    //判断标记位为PFLAG_LAYOUT_REQUIRED的时候才进行onLayout方法
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
     }
 }
public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    // flag 是 PFLAG_DIRTY_OPAQUE 则需要绘制
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    if (!dirtyOpaque) onDraw(canvas);
    // 绘制 Child
    dispatchDraw(canvas);
    // foreground 不管 dirtyOpaque 标志,每次都会绘制
    onDrawForeground(canvas);
}


参考文章中有一段总结挺好的:


虽然两者都是用来触发绘制流程,但是在measure和layout过程中,只会对 flag 设置为 FORCE_LAYOUT 的情况进行重新测量和布局,而draw方法中只会重绘flag为 dirty 的区域。requestLayout 是用来设置FORCE_LAYOUT标志,invalidate 用来设置 dirty 标志。所以 requestLayout 只会触发 measure 和 layout,invalidate 只会触发 draw。

目录
相关文章
|
3月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
127 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
3月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
49 8
|
3月前
|
Android开发 开发者
Android经典面试题之SurfaceView和TextureView有什么区别?
分享了`SurfaceView`和`TextureView`在Android中的角色。`SurfaceView`适于视频/游戏,独立窗口低延迟,但变换受限;`TextureView`支持复杂变换,视图层级中渲染,适合动画/视频特效,但性能略低。两者在性能、变换、使用和层级上有差异,开发者需按需选择。
60 1
|
3月前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
54 3
|
3月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
48 6
|
3月前
|
XML Android开发 数据格式
Android面试题之DialogFragment中隐藏导航栏
在Android中展示全屏`DialogFragment`并隐藏状态栏和导航栏,可通过设置系统UI标志实现。 记得在布局文件中添加内容,并使用`show()`方法显示`DialogFragment`。
57 2
|
3月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
48 1
|
3月前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
47 0
|
3月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式有哪些用法
Kotlin的Lambda表达式是匿名函数的简洁形式,常用于集合操作和高阶函数。基本语法是`{参数 -&gt; 表达式}`。例如,`{a, b -&gt; a + b}`是一个加法lambda。它们可在`map`、`filter`等函数中使用,也可作为参数传递。单参数时可使用`it`关键字,如`list.map { it * 2 }`。类型推断简化了类型声明。
22 0
|
3月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
31 0