Android Lint扫描规则说明(二)

主要内容

对Android Studio支持的六类Android Lint规则, 本文主要对Performance包含的32个项的说明,主要内容都是文档翻译,适当加一些自己的感想。

分类详细说明

高效使用资源

UnusedIds

未被使用的资源id,在layout文件中定义了资源ID从未被使用过,但有时候它们可以让layout更容易阅读,没有必要删除未使用的资源id。

Overdraw

过度绘制:一个绘制区域被绘制的次数多于一次。

如果给一个root view设置了背景图,就要给它设置一个background=nulltheme,否则绘制过程会先绘制themebackground,然后再绘制设置的背景图,完全覆盖之前绘制的theme.background,这是 过度绘制

这个检测器依赖于根据扫描Java代码找出哪些布局文件与哪些Activity相关联,目前它使用的是一种不精确的模式匹配算法。因此,可能会因错误地推断布局与活动的关联而给出错误的提醒。

如果想把一个背景图应用在多个页面上,可以考虑自定义theme,并把背景图设置在theme里,在layout中设置theme代替设置background。如果背景图中有透明的部分,并且希望他和theme的背景有层叠效果,那么可以选择先把两个背景合并成一个背景图之后,在定义到theme里。

VectorPath

关于SVG的使用,给出一篇参考文章:Android vector标签 PathData 画图超详解,Android Studio可以创建使用SVG绘制出的drawable图像资源。

UselessLeaf

没有包含任何View,也没有设置背景的Layout是多余的,可以去掉。让界面更趋于扁平,嵌套更高效。

UselessParent

如果一个包含ViewLayout没有兄弟层级的Layout,而他的外部ViewGroup又不是ScrollView或者root级别,那么这个Layout可以移除,让他包含的View直接包含在它的父层级的Layout中。让界面更趋于扁平,嵌套更高效。

TooDeepLayout

Layout嵌套过深会影响性能,考虑使用平铺类型的Layout代替。默认最深的View嵌套是10层,也可以通过环境变量ANDROID_LINT_MAX_DEPTH进行设置。System.getenv("ANDROID_LINT_MAX_DEPTH");语句获取,如何设置还没找到。

TooManyViews

Layout内有太多的View:一个Layout文件内有过多的View会影响性能。考虑使用复合drawables或其他技巧来减少这个布局中的视图数量。默认最多的数量是80个,可以通过环境变量ANDROID_LINT_MAX_VIEW_COUNT进行设置。据说这个变量可以用System.getenv("ANDROID_LINT_MAX_DEPTH");语句获取,如何设置还没找到。

NestedWeights

Weight嵌套:使用非0layout-weight值,需要Layout被测量两次,如果一个包含非0值的LinearLayout被嵌套在另一个包含非0值的LinearLayout内部,那么,测量次数就会呈指数级增长。

DuplicateDivider

这个主要是讲RecyclerView的分割线,com.android.support:recyclerview-v7 提供了一个类DividerItemDecoration设置分割线样式,这个类在早期的版本内没有包含,所以在更新为新的版本后,可以使用这个类重新设置分割线。
具体使用,参考文章:Android RecyclerView 使用完全解析

MergeRootFrame

FrameLayout在一个layout文件中是root且没有使用background或者padding等属性,通常使用一个merge标签代替FrameLayout会更高效。但是这要看上下文设置,所以在替换之前要确认你已经理解了merge标签的工作原理

UnusedResources

未使用的资源:多指的是drawable类型的资源。多余的drawable资源会让APP变大,编译过程变长。

InefficientWeight

当LinearLayout只有一个Widget且使用了android:layout_weight时,定义对应的width/height的值为0dp,Widget就会自动占满剩余空间。因为不需要预先计算自己的尺寸,这种方式更高效。

高效的设置

DisableBaselineAlignment

在使用LinearLayout实现空间的按比例分割时,LinearLayout的空间用layout_weight属性在所包含的几个layout中间分割,那么应该设置被分割LinearLayoutbaseLineAligned="false",这样可以加快分割空间所做的运算。

LogConditional

LogConditional:使用android.util.Log打印调试日志,一般只会在DEBUG模式下使用,在release是不需要打印调试日志的,在buildToolsVersion大于等于17时, BuildConfig提供两个一个DEBUG常量来标记是否处于DEBUG模式,我们可以用if(BuildConfig.DEBUG){}包裹调试日志语句,这样编译器会在编译生成release包时,删除这些语句。如果真的需要在release模式下打印调试日志,可以使用@SuppressLint("LogConditional")注解告诉编译器在release包中保留这些日志信息。

UnpackedNativeCode

APP使用System.loadLibrary()加载Native库时,android 6.0或者更新的版本可以在Manifest文件中application标签中添加属性android:extractNativeLibs="false",这样可以提交加载速度,降低APP占用的存储空间。

更高效的替代方案

FloatMath

不要使用FloatMath类进行数学计算,推荐使用Math类。

Android早期版本因为浮点运算性能的原因,推荐使用FloatMath代替Math类进行数学计算。随着硬件和系统的发展,这个问题已经不复存在,甚至经过JIT优化之后的Math类运算速度会比FloatMath更快,所以,在Android F以上版本的系统上,可以直接使用Math类,而不是FloatMath。

UseValueOf

某些类构造新对象时,建议使用工厂方法,而不是new关键字声明新的对象。例如,new Intger(0)就可以使用Integer.valueOf(0)替代,工厂方法会使用更少的内存,因为它会让值相等的对象使用同一个实例。

ViewHolder

在给ListViewGradView之类的列表实现Adapter时,不能每次getView调用都去inflate一个新的layout,如果接口参数中给出了一个可以复用的View对象,就可以使用这个对象而不是重新生成。这个应该都很熟悉,也很简单基础了。

UseSparseArrays

KeyInteger类型的HashMap可以使用SparseArray代替,性能更好。可以使用替代HashMap的有SparseBooleanArray、SparseIntArray、SparseLongArray和泛型类SparseArray,每个对应的类型代表Value的类型。如果在某些情况一定要用HashMap实现,则可以用@SuppressLint注解抑制Lint检查。

WakelockTimeout

关于week lock的使用,这里提供一篇博客文章:Android 功耗分析之wakelock

UseCompoundDrawables

在一个TextView的四周有只具有展示作用的ImageView时,建议删除ImageView改用compound drawables:drawableTop, drawableLeft, drawableRight,drawableBottom,drawablePadding替代方案实现。

有关泄漏的提醒

Recycle

缺少recycle()调用:许多资源例如:TypedArrays, VelocityTrackers在使用完之后需要调用recycle()方法回收资源。

ViewTag

4.0版本系统之前,View.setTag(int, Object)的实现方式中,会把Object存储在一个静态的map里并且使用的是强引用。这就意味着如果这个Object包含了对Context对象的引用,这个Context就是泄漏了。

传递一个View做参数,这个View就能提供一个对创建它的Context的引用。类似的,View holders内包含View,也会有Context与这个View相关联。

HandlerLeak

Handler引用泄漏:声明Handler的子类如MyHandler为内部类,如果MyHandler类对象关联Looper.getMainLooper()或者Looper.getMainLooper().getQueue()时,会阻止无用的外部类对象被垃圾回收,导致泄漏。如果对应main thread 的关联,就不会有这个问题。

应对方法,声明MyHandler为静态内部类,并用WeakReference的方式持有一个外部类对象,MyHandler使用这个对象操作外部类的属性和方法。

DrawAllocation

绘制过程中的内存分配:避免在布局绘制过程中分配内存给新的对象。因为这些操作调用频率比较高,频繁分配内存会唤起垃圾回收,中断UI绘制,导致卡顿。

StaticFieldLeak

非静态内部类具有对其外部类对象的隐式引用。

如果外部类Fragment或者Activity,那么这个引用意味着长时间运行的处理程序/加载器/任务(handler/loader/task)将持外部类对象的引用,从而防止外部类对象被回收。

同理,长时间运行的处理程序/加载器/任务(handler/loader/task)对Fragment或者Activity的直接引用,也会造成泄漏。

ViewModel类应该禁止引用View或者non-application类型的Context对象。

代码提醒

AnimatorKeep

属性动画默认支持的属性如下面列表。如果超出这些范围,会通过反射调用本地定义的函数。声明一个属性动画对象例如:ObjectAnimator.ofFloat(view, "rotation", 0, 360) 中的“rotation”就是要操作的属性,如果属性不在下面的列表中例如ObjectAnimator.ofFloat(view, "position", 0, 360),就需要本地定义一个对应的方法setPosition(float position),并且这个方法需要加上@keep注解,防止被当做无用方法清理掉。

    static {
        PROXY_PROPERTIES.put("alpha", PreHoneycombCompat.ALPHA);
        PROXY_PROPERTIES.put("pivotX", PreHoneycombCompat.PIVOT_X);
        PROXY_PROPERTIES.put("pivotY", PreHoneycombCompat.PIVOT_Y);
        PROXY_PROPERTIES.put("translationX", PreHoneycombCompat.TRANSLATION_X);
        PROXY_PROPERTIES.put("translationY", PreHoneycombCompat.TRANSLATION_Y);
        PROXY_PROPERTIES.put("rotation", PreHoneycombCompat.ROTATION);
        PROXY_PROPERTIES.put("rotationX", PreHoneycombCompat.ROTATION_X);
        PROXY_PROPERTIES.put("rotationY", PreHoneycombCompat.ROTATION_Y);
        PROXY_PROPERTIES.put("scaleX", PreHoneycombCompat.SCALE_X);
        PROXY_PROPERTIES.put("scaleY", PreHoneycombCompat.SCALE_Y);
        PROXY_PROPERTIES.put("scrollX", PreHoneycombCompat.SCROLL_X);
        PROXY_PROPERTIES.put("scrollY", PreHoneycombCompat.SCROLL_Y);
        PROXY_PROPERTIES.put("x", PreHoneycombCompat.X);
        PROXY_PROPERTIES.put("y", PreHoneycombCompat.Y);
    }
ObsoleteSdkInt

无用的SDK版本检查:Android SDK的版本更新比较快,许多API的使用都需要通过检查SDK版本防止出现not found之类的崩溃。在APP迭代的过程中提升了minSdkVersion的值就会导致部分SDK版本检查不再需要。

这种SDK版本检查会引起不必要的资源搜索。

DevModeObsolete

以前,文档中建议在productFlavors中创建一个dev product。设定minSdkVersion 21,在开发过程中激活multidexing加速构建过程。现在已经不需要这么做了,在新版的IDE和Gradle插件中,会自动地识别所连接设备的API level,如果链接的设备API level大于等于21,就会自动打开multindexing,就跟之前设置了dev product的效果一样。

参考:Enable Android MultiDex

ObsoleteLayoutParam

无用的LayoutParam:当给Widget使用了所在Layout没有提供的LayouParam时,会有这个提示。这种情况一般出现在修改Layout类型时没有同时修改内部Widget的LayoutParam设置或者把一个Widget从一个Layout拷贝到另一个不同类型的Layout内部。

这种无用的LayoutParam在运行时会引起无效的属性解析,也会误导阅读这些代码的人。所以应该把这些无用的属性删除掉。

其他

WearableBindListener
UseOfBundledGooglePlayServices

安卓自定义注解支持和示例实现

开头

编码时使用注解,可以提高编码效率、简化代码增强可读性等优点;使用注解还是代码静态扫描的一部分,促进代码规范。安卓注解使用介绍一文中介绍了JDK/SDK提供的注解和support/ButterKnife等第三方提供的注解库,还有其他的一些库,这些基本已经能够满足需求。

support/ButterKnife是应用很广的注解库,它们也是属于“自定义注解”的范畴,只是有因为使用的多了,实际上成为了一个“标准”。

本文从“造库”的角度介绍自定义注解的相关支持,并提供一个示例实现。但是,本文不提供自定义注解相关的静态检查,这需要lint的支持,本文不做介绍,希望后面的文章有机会介绍一下,这里先占个坑

第三方注解库

引入一个注解库,以ButterKnife为例:
– 添加注解库

implementation 'com.jakewharton:butterknife:8.4.0'
  • 添加注解处理器
annotationProcessor 'com.jakewharton:butterknife:8.4.0'

添加了这两个库之后,就可以使用这个注解库了。

如果是library项目】,还需要引入butterknife-gradle-plugin插件,在安卓注解使用介绍中有具体介绍。

定义注解

所有的注解都默认继承自java.lang.annotation.Annotation

定义注解时可以声明0..N个成员,例如下面的定义,可以用default为成员指定默认值;成员名称可以按照程序语言的变量命名规则任意给定,成员的类型也是有限制的。在使用时需要指定参数名:@StringAnnotation(value = “data”),当成员只有一个且命名为value时,可省略。

8中基本数据类型,String,Class,Annotation及子类,枚举;

上面列举类型的数组,例如:String[]

public @interface StringAnnotation /*extends Annotation*/{
    String value() default "";
}

动态注解和静态注解

注解要在解析后才能最终发挥作用,解析过程有上面提到的 注解处理器 完成。依据注解处理器解析过程执行的时机,注解可以分为动态注解和静态注解。

动态注解

动态注解又叫运行时注解,注解的解析过程在执行期间进行,使用反射机制完成解析过程,会影响性能;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicIntentKey {
    String value() default "";
}
public class DynamicUtil {
    public static void inject(Activity activity) {
        Intent intent = activity.getIntent();
        // 反射
        for (Field field : activity.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(DynamicIntentKey.class)) {
                // 获取注解
                DynamicIntentKey annotation = field.getAnnotation(DynamicIntentKey.class);
                String intentKey = annotation.value();
                // 读取实际的IntentExtra值
                Serializable serializable = intent.getSerializableExtra(intentKey);
                if (serializable == null) {
                    if (field.getType().isAssignableFrom(String.class)) {
                        serializable = "";
                    }
                }
                try {
                    // 插入值
                    boolean accessible = field.isAccessible();
                    field.setAccessible(true);
                    field.set(activity, serializable);
                    field.setAccessible(accessible);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

静态注解

静态注解出现在动态注解之后,并取代动态注解。静态注解相对于动态注解,把注解的解释过程放在编译阶段,在运行时不再需要解释,而是直接使用编译的结果。

因此,编译阶段需要使用相应的工具生成所需的代码。

  • 先定义一个注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface StaticIntentKey {
    String value();
}
  • 然后为这个注解定义一个处理器

注解解释器需要继承自AbstractProcessor基类,并使用@AutoService(Processor.class)声明这个类是一个注解处理器。


import com.google.auto.service.AutoService; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Processor; @AutoService(Processor.class) public class StaticIntentProcessor extends AbstractProcessor { }

public abstract class AbstractProcessor implements Processor { }
  • 注解处理器基类AbstractProcessor实现自Processor接口,其中init()和getSupportedOptions()在抽象类AbstractProcessor给出了实现,StaticIntentProcessor的主体功能是实现process()方法,完成类生成。
public interface Processor {
    Set<String> getSupportedOptions();
    // 支持的注解类的类名集合
    Set<String> getSupportedAnnotationTypes();
    // 支持的Java版本
    SourceVersion getSupportedSourceVersion();

    void init(ProcessingEnvironment var1);

    boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);

    Iterable<? extends Completion> getCompletions(Element var1, AnnotationMirror var2, ExecutableElement var3, String var4);
}
  • 通过下面的注解处理器,为所有使用了这个注解的类生成处理代码,不再需要运行时通过反射获得。

因为这个实现没有专门实现一个对应的android-library类型的工程,所以在使用这个注解时,需要先编译完成,编译完成之后有了对应的注解处理器,才可以在Android工程中使用。

@AutoService(Processor.class)
public class StaticIntentProcessor extends AbstractProcessor {

    private TypeName activityClassName = ClassName.get("android.app", "Activity").withoutAnnotations();
    private TypeName intentClassName = ClassName.get("android.content", "Intent").withoutAnnotations();

    @Override
    public SourceVersion getSupportedSourceVersion() {
        // 支持java1.7
        return SourceVersion.RELEASE_7;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 只处理 StaticIntentKey 注解
        return Collections.singleton(StaticIntentKey.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) {
        // StaticMapper的bind方法
        MethodSpec.Builder method = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                .addParameter(activityClassName, "activity");

        // 查找所有的需要注入的类描述
        List<InjectDesc> injectDescs = findInjectDesc(set, re);

        for (int i1 = 0; i1 < injectDescs.size(); i1++) {
            InjectDesc injectDesc = injectDescs.get(i1);

            // 创建需要注解的类的Java文件,如上面所述的 IntentActivity$Binder
            TypeName injectedType = createInjectClassFile(injectDesc);
            TypeName activityName = typeName(injectDesc.activityName);

            // $T导入类型
            // 生成绑定分发的代码
            method.addCode((i1 == 0 ? "" : " else ") + "if (activity instanceof $T) {\n", activityName);
            method.addCode("\t$T binder = new $T();\n", injectedType, injectedType);
            method.addCode("\tbinder.bind((" + activityName + ") activity);\n", activityName, activityName);
            method.addCode("}");
        }
        // 创建StaticMapper类
        createJavaFile("com.campusboy.annotationtest", "StaticMapper", method.build());

        return false;
    }

    private List<InjectDesc> findInjectDesc(Set<? extends TypeElement> set, RoundEnvironment re) {

        Map<TypeElement, List<String[]>> targetClassMap = new HashMap<>();

        // 先获取所有被StaticIntentKey标示的元素
        Set<? extends Element> elements = re.getElementsAnnotatedWith(StaticIntentKey.class);
        for (Element element : elements) {
            // 只关心类别是属性的元素
            if (element.getKind() != ElementKind.FIELD) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field");
                continue;
            }

            // 此处找到的是类的描述类型
            // 因为我们的StaticIntentKey的注解描述是field,所以closingElement元素是类
            TypeElement classType = (TypeElement) element.getEnclosingElement();

            System.out.println(classType);

            // 对类做缓存,避免重复
            List<String[]> nameList = targetClassMap.get(classType);
            if (nameList == null) {
                nameList = new ArrayList<>();
                targetClassMap.put(classType, nameList);
            }

            // 被注解的值,如staticName
            String fieldName = element.getSimpleName().toString();
            // 被注解的值的类型,如String,int
            String fieldTypeName = element.asType().toString();
            // 注解本身的值,如key_name
            String intentName = element.getAnnotation(StaticIntentKey.class).value();

            String[] names = new String[]{fieldName, fieldTypeName, intentName};
            nameList.add(names);
        }

        List<InjectDesc> injectDescList = new ArrayList<>(targetClassMap.size());
        for (Map.Entry<TypeElement, List<String[]>> entry : targetClassMap.entrySet()) {
            String className = entry.getKey().getQualifiedName().toString();
            System.out.println(className);

            // 封装成自定义的描述符
            InjectDesc injectDesc = new InjectDesc();
            injectDesc.activityName = className;
            List<String[]> value = entry.getValue();
            injectDesc.fieldNames = new String[value.size()];
            injectDesc.fieldTypeNames = new String[value.size()];
            injectDesc.intentNames = new String[value.size()];
            for (int i = 0; i < value.size(); i++) {
                String[] names = value.get(i);
                injectDesc.fieldNames[i] = names[0];
                injectDesc.fieldTypeNames[i] = names[1];
                injectDesc.intentNames[i] = names[2];
            }
            injectDescList.add(injectDesc);
        }

        return injectDescList;
    }

    private void createJavaFile(String pkg, String classShortName, MethodSpec... method) {
        TypeSpec.Builder builder = TypeSpec.classBuilder(classShortName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
        for (MethodSpec spec : method) {
            builder.addMethod(spec);
        }
        TypeSpec clazzType = builder.build();

        try {
            JavaFile javaFile = JavaFile.builder(pkg, clazzType)
                    .addFileComment(" This codes are generated automatically. Do not modify!")
                    .indent("    ")
                    .build();
            // write to file
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private TypeName createInjectClassFile(InjectDesc injectDesc) {

        ClassName activityName = className(injectDesc.activityName);
        ClassName injectedClass = ClassName.get(activityName.packageName(), activityName.simpleName() + "$Binder");

        MethodSpec.Builder method = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                .addParameter(activityName, "activity");

        // $T导入作为类,$N导入作为纯值,$S导入作为字符串
        method.addStatement("$T intent = activity.getIntent()", intentClassName);
        for (int i = 0; i < injectDesc.fieldNames.length; i++) {
            TypeName fieldTypeName = typeName(injectDesc.fieldTypeNames[i]);
            method.addCode("if (intent.hasExtra($S)) {\n", injectDesc.intentNames[i]);
            method.addCode("\tactivity.$N = ($T) intent.getSerializableExtra($S);\n", injectDesc.fieldNames[i], fieldTypeName, injectDesc.intentNames[i]);
            method.addCode("}\n");
        }

        // 生成最终的XXX$Binder文件
        createJavaFile(injectedClass.packageName(), injectedClass.simpleName(), method.build());

        return injectedClass;
    }

    private TypeName typeName(String className) {
        return className(className).withoutAnnotations();
    }

    private ClassName className(String className) {

        // 基础类型描述符
        if (className.indexOf(".") <= 0) {
            switch (className) {
                case "byte":
                    return ClassName.get("java.lang", "Byte");
                case "short":
                    return ClassName.get("java.lang", "Short");
                case "int":
                    return ClassName.get("java.lang", "Integer");
                case "long":
                    return ClassName.get("java.lang", "Long");
                case "float":
                    return ClassName.get("java.lang", "Float");
                case "double":
                    return ClassName.get("java.lang", "Double");
                case "boolean":
                    return ClassName.get("java.lang", "Boolean");
                case "char":
                    return ClassName.get("java.lang", "Character");
                default:
            }
        }

        // 手动解析 java.lang.String,分成java.lang的包名和String的类名
        String packageD = className.substring(0, className.lastIndexOf('.'));
        String name = className.substring(className.lastIndexOf('.') + 1);
        return ClassName.get(packageD, name);
    }

    private static class InjectDesc {
        private String activityName;
        private String[] fieldNames;
        private String[] fieldTypeNames;
        private String[] intentNames;

        @Override
        public String toString() {
            return "InjectDesc{" +
                    "activityName='" + activityName + '\'' +
                    ", fieldNames=" + Arrays.toString(fieldNames) +
                    ", intentNames=" + Arrays.toString(intentNames) +
                    '}';
        }
    }
}

示例工程

示例工程:customize-annotation

代码生成库:javaPoet 使用这个库可以更方便地生成代码。

参考文章

OkHttp源码解析(三)–io操作

在前边文章分析中,我们已经知道CallServerInterceptor中实现了从服务端读/写数据, 因此分析IO操作,我们就从这个类入手。
查看这个类的Intercept方法(摘取关键部分):

  @Override public Response intercept(Chain chain) throws IOException {
   ///此处省略若干行...
    httpCodec.writeRequestHeaders(request); //写请求头
    ///省略
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();//把缓存的请求flush出去
        responseBuilder = httpCodec.readResponseHeaders(true);//读响应
      }

      if (responseBuilder == null) {
        long contentLength = request.body().contentLength();
        CountingSink requestBodyOut =
            new CountingSink(httpCodec.createRequestBody(request, contentLength));
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);//请求体Buffer
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
          //省略...
    httpCodec.finishRequest(); //结束请求

    if (responseBuilder == null) {
      realChain.eventListener().responseHeadersStart(realChain.call());
      responseBuilder = httpCodec.readResponseHeaders(false);//读响应头
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (code == 100) {//处理100-continue
      // try again to read the actual response
      responseBuilder = httpCodec.readResponseHeaders(false);//读响应头
    //省略
    }
    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))//处理Http响应
          .build();
    }
//省略...
    return response;
  }

代码比较长,我们只看重要的部分httpCodec.writeRequestHeaders(request);, 根据之前文章OkHttp分别通过Http1Codec 和 Http2Codec实现了http1.x和2.0协议,简单起见,我们先只关注Http1Codec, 查看Http1Codec的writeRequestHeaders方法如下:

  @Override public void writeRequestHeaders(Request request) throws IOException {
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
    writeRequest(request.headers(), requestLine);
  }

  public void writeRequest(Headers headers, String requestLine) throws IOException {
    if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
    sink.writeUtf8(requestLine).writeUtf8("\r\n");
    for (int i = 0, size = headers.size(); i < size; i++) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n");
    }
    sink.writeUtf8("\r\n");
    state = STATE_OPEN_REQUEST_BODY;
  }

我们看到是把具体的http请求行和请求头信息写到了一个sink里边,sink的声明final BufferedSink sink;BufferedSink 接口声明: interface BufferedSink : Sink, WritableByteChannel, 查看Sink接口中的说明:

* ### Comparison with OutputStream
 *
 * This interface is functionally equivalent to [java.io.OutputStream].
 ```
 Sink的设计跟OutputStream一样,因此Sink是Okio中对于输出流的抽象,但跟OutputStream不一样的是,为了效率Sink没有直接的write方法,而只有` fun write(source: Buffer, byteCount: Long)`方法。

 看完写数据,我们接着来看一个从服务器读取数据的环节: `responseBuilder = httpCodec.readResponseHeaders(true);`, 同样查看Http1Codec的 readResponseHeaders方法如下:
 ```java
 @Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    try {
      StatusLine statusLine = StatusLine.parse(readHeaderLine());//读取状态行

      Response.Builder responseBuilder = new Response.Builder()
          .protocol(statusLine.protocol)
          .code(statusLine.code)
          .message(statusLine.message)
          .headers(readHeaders());//读取header
        ///省略部分代码...
  }
   private String readHeaderLine() throws IOException {
      String line = source.readUtf8LineStrict(headerLimit);//关键的读取代码
      headerLimit -= line.length();
    return line;
  }

查看source的声明: final BufferedSource source;, BufferedSource的接口声明: interface BufferedSource : Source, ReadableByteChannel ,查看Source接口的说明:

* ### Comparison with InputStream
 * This interface is functionally equivalent to [java.io.InputStream].

Source的设计和Java中的InputStream功能一致,跟Sink类似,为了效率考虑没有InputStream中的read方法,而是有一个: fun read(sink: Buffer, byteCount: Long): Long

输入(BufferedSource)输出(BufferedSink)都只是接口定义,但是不难找到真正的实现类Buffer, 类声明class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel, 查看类说明:

/**
 * A collection of bytes in memory.
 *
 * **Moving data from one buffer to another is fast.** Instead of copying bytes from one place in
 * memory to another, this class just changes ownership of the underlying byte arrays.
 *
 * **This buffer grows with your data.** Just like ArrayList, each buffer starts small. It consumes
 * only the memory it needs to.
 *
 * **This buffer pools its byte arrays.** When you allocate a byte array in Java, the runtime must
 * zero-fill the requested array before returning it to you. Even if you're going to write over that
 * space anyway. This class avoids zero-fill and GC churn by pooling byte arrays.

简单来说Buffer同时实现了BufferedSource和BufferedSink接口,做了如下改进: 在不同的Buffer直接移动数据避免了内存拷贝,而只是将字节数组的所有权叫做交换;通过缓存字节数组,避免了Java的运行时在分配内存时的zero-fill以及释放内存时的GC,因此对于频繁的io操作或者数据移动是很高效的。

Buffer的实现很长,我们还是从读取Http头的代码查看读取的流程,通过追踪读取http头的的代码String line = source.readUtf8LineStrict(headerLimit);(中间经过很多步骤,但不是很重要就忽略了), 我们追踪到最终的读取实现在Buffer这个类的如下方法:
“`java
override fun read(sink: ByteArray, offset: Int, byteCount: Int): Int {
checkOffsetAndCount(sink.size.toLong(), offset.toLong(), byteCount.toLong())

<pre><code>val s = head ?: return -1//head是一个Segment类型
val toCopy = minOf(byteCount, s.limit – s.pos)
System.arraycopy(s.data, s.pos, sink, offset, toCopy)

s.pos += toCopy
size -= toCopy.toLong()

if (s.pos == s.limit) {
head = s.pop()
SegmentPool.recycle(s)//回收Segment
}

return toCopy
</code></pre>

}
<code>我们看到了Segment以及SegmentPool, Segment的类描述重要部分如下:</code>java
/**
* A segment of a buffer.
*
* Each segment in a buffer is a circularly-linked list node referencing the following and
* preceding segments in the buffer.
*
* Each segment in the pool is a singly-linked list node referencing the rest of segments in the
* pool.
*
<code>Segment要么在buffer中,表示一个循环双链表节点,连接其他Segment,要么在Pool中,连接其他Pool中的Segment。
查看SegmentPool类描述:</code>java
/**
* A collection of unused segments, necessary to avoid GC churn and zero-fill.
* This pool is a thread-safe static singleton.
*/
“`
可以看到SegmentPool是一个全局唯一的单例,用来存储缓存的Segment,通过查看源码,能看懂SegmentPool中设置一个缓存数据的最大值,如果这个池子满了,则Segment放不进去了。

我们通过这个流程简单梳理了OkHttp请求和响应过程中的io处理,当然,我们只是抓住主要的输入输出做了简单分析,了解了Okio主要的几个类,Source, Sink, Buffer, Segment, SegmentPool等,其中还有很多细节并没有涉及,例如: Buffer是如何尽量的避免内存数据拷贝的? Http2的交互细节,等等。感兴趣的同学可以再自己查看下源码梳理下吧。

OkHttp源码解析(二) —线程管理

跟上篇文章一样,我们还是按照请求和响应的处理流程梳理OkHttp的源码,不过这次重点放到线程管理这一部分。

入口仍然是两种调用方式,同步调用:

Response execute() throws IOException;

异步调用:

void enqueue(Callback responseCallback);

具体的实现类RealCall, 同步调用全部在当前线程处理,得到响应,并不会使用OkHttp内部创建的线程,因此我们重点查看异步调用的实现:

@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    client.dispatcher().enqueue(new AsyncCall(responseCallback));//生成一个AsyncCall对象,并调用dispatcher的enqueue方法
  }

最关键的只有一行代码,生成一个AsyncCall对象,并调用dispatcher的enqueue方法,因此我们查看enqueue方法:

  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

我们看到OkHttp在这里做了并发控制,如果总并发数小于允许的最大并发数,且对于单一域名的并发数小于允许的最大并发数,则调用excutorService的execute去执行,否则加入准备队列。多线程控制肯定在excutorService无疑了。

executorService实现如下:

  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

我们看到如果没有设置自定义的executorService, 就会创建一个线程池, 查看OkHttpClient类的代码, 默认的HttpClient.Bulder 创建了一个dispatcher:dispatcher = new Dispatcher(); 默认的构造函数并没有对executorService赋值,因此可以确定OkHttpClient默认即是采用的这个ThreadPoolExecutor。

于是关键的代码就在于这个ThreadPoolExecutor的构造函数了,相关文档说明如下:

    /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters and default rejected execution handler.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }

根据传入的参数和文档说明, 这个线程池的配置如下: 核心线程为0, 最大线程数为2^32-1, 非核心线程的活跃时间为60秒。单纯考虑这个线程池并未限制线程数,综合考虑dispatcher自己的限制:

private int maxRequests = 64;
private int maxRequestsPerHost = 5;

因此就只有最多64个并发请求,如果App内只有1个域名的话,最多只能有5个并发请求。

再来看一下ThreadPoolExecutor的workQueue 参数: new SynchronousQueue(), 字面意义是同步队列,扩展了AbstractQueue 并实现了BlockingQueue , BlockingQueue定义了阻塞队列的接口,阻塞队列在获取元素时如果队列为空则等待,同样插入队列元素时,如果队列满了也等待。

同步队列的说明如下:

/**
 * A {@linkplain BlockingQueue blocking queue} in which each insert
 * operation must wait for a corresponding remove operation by another
 * thread, and vice versa.  A synchronous queue does not have any
 * internal capacity, not even a capacity of one.  You cannot
 * {@code peek} at a synchronous queue because an element is only
 * present when you try to remove it; you cannot insert an element
 * (using any method) unless another thread is trying to remove it;
 * you cannot iterate as there is nothing to iterate.  The
 * <em>head</em> of the queue is the element that the first queued
 * inserting thread is trying to add to the queue; if there is no such
 * queued thread then no element is available for removal and
 * {@code poll()} will return {@code null}.  For purposes of other
 * {@code Collection} methods (for example {@code contains}), a
 * {@code SynchronousQueue} acts as an empty collection.  This queue
 * does not permit {@code null} elements.
 *
 * <p>Synchronous queues are similar to rendezvous channels used in
 * CSP and Ada. They are well suited for handoff designs, in which an
 * object running in one thread must sync up with an object running
 * in another thread in order to hand it some information, event, or
 * task.
 *
 * <p>This class supports an optional fairness policy for ordering
 * waiting producer and consumer threads.  By default, this ordering
 * is not guaranteed. However, a queue constructed with fairness set
 * to {@code true} grants threads access in FIFO order.
 *
 * <p>This class and its iterator implement all of the
 * <em>optional</em> methods of the {@link Collection} and {@link
 * Iterator} interfaces.
 ```
简要总结下是说在插入同步队列元素时,需要等待其他线程移除元素,在取元素时需要等待其他线程插入元素, 同步队列内部并不存储元素,因此不能peek。  同步队列也实现了一个可选的公平策略,允许等待线程排序,默认不采用公平策略。

最后看一下threadFactory参数: `Util.threadFactory("OkHttp Dispatcher", false)`, 实现如下:

```java
public static ThreadFactory threadFactory(final String name, final boolean daemon) {
    return new ThreadFactory() {
      @Override public Thread newThread(Runnable runnable) {
        Thread result = new Thread(runnable, name);
        result.setDaemon(daemon);
        return result;
      }
    };
  }

生成了一个Thread,同时设置成非守护线程(用户线程),守护线程和非守护线程的唯一区别在于: 如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

至此,我们搞明白了OkHttp对于异步和线程这块的管理。

安卓注解使用介绍

在Java中,注解(Annotation)引入始于Java5,用来描述Java代码的元信息,通常情况下注解不会直接影响代码的执行,尽管有些注解可以用来做到影响代码执行。

在代码文件中使用‘@’字符告诉编译器接下来的是一个注解。注解可以用在类,构造方法,成员变量,方法,参数等的声明中。作用主要是对编译器警告等辅助工具产生影响,如果传递了错误的类型那么编译器就会发出警告,这样就可以在编码和维护的过程中辅助发现问题,提高开发效率,提升代码质量,也促进形成编码规范。

安卓开发中用到的注解主要有四个方面:JDK内置注解、JDK自定义注解包、android sdk内置注解、android.support.annotation注解。

JDK内置注解

如下图,左侧是JDK提供的三个标准注解,在java.lang包内。右侧JDK提供的四个是对自定义注解的支持,在java.lang.annotation包内。

@Deprecated是一个标记注解,表示被标记的成员变量或者成员方法已经不建议使用,原因可能是这个方法/变量有缺陷或者在新的SDK中已经不被支持。

@Override注解在继承过程中,标识对父类方法的覆盖关系。这个在Java中不是必须的,但是建议在需要的地方强制使用。防止在子类或者父类中误操作修改方法签名或者遗漏相关的代码(kotlin中对于子类覆盖父类方法强制使用override关键字,不再需要注解标记)。

@SuppressWarnings用来抑制编译器生成警告信息,对指定类型的警告保持静默。可以修饰类、方法、方法参数、属性和局部变量,采用就近原则,尽量放在被需要静默的警告语句附近。接收一个字符串或者一个字符集作为参数(参数详细介绍参考文章),它指示将取消的警告。

JDK提供了四种元注解,支持用户对注解进行自定义扩展。自定义注解后面会详细介绍,这里先略过。

Android SDK内置注解

Android SDK注解有两个@SuppressLint和@TargetApi。


这两个注解是使用Lint静态检测对应的标记。如果禁用了Lint,用或者不用这些注解都没有太大关系。

@TargetApi:Android工程需要设置所支持的最小的系统版本,Android Studio是在Gradle中设置minSdkVersion的值。如果某个方法或者类被声明需要在某个版本和更高版本的系统上运行,可以使用@RequiresApi(requires)声明支持的最小系统版本。当在声明minSdkVersion的工程中使用了requires大于minSdkVersion的类或者方法时,Lint就会报错误提醒,这时候可以使用@TargetApi使Lint保持静默,但是要添加代码为低版本的系统提供对应的备选方案,否则在低版本系统上运行会产生崩溃。

@SuppressLint:上面的@TargetApi注解只针对API版本进行注解,使Lint对版本错误保持静默。@SuppressLint针对的范围更广,通过设置一个参数(identified by the lint issue id)通知Lint对相应的警告⚠和错误❎️保持静默。

@SuppressLint(“NewApi”),这个注解可以实现@TargetApi(version)相同的作用,只是没有指定特定的API版本,导致工程师和Lint都不知道响应的范围,容易导致错误,不建议使用。

android support注解

Android Support Library提供com.android.support:support-annotations对Android的注解进行了拓展。下图以v25为例列出了所有定义的46个注解(到v27增加了5个,分别为ColorLong、FontRes、GuardedBy、HalfFloat、NavigationRes)。

  • 其中22个资源类注解,如下:
  • 取值范围类3个注解,IntRange/FloatRange对响应类型的变量或参数规定取值范围,参数有from和to两个;Size对数组的长度约束,参数min/max(含)组合声明数组的长度范围,参数value指定数据的具体长度,参数multiple指定数组长度必须是某个数字的倍数。
  • Android中新引入的替代枚举的注解有IntDef和StringDef,他们唯一的区别一个是int类型,一个是string类型,下面我们列一下官方API文档中给出的使用方法。

枚举类型的注解的使用,先定义一系列可用的取值,然后定义一个注解使用@IntDef或者@StringDef指定新的注解可用的取值列表。

使用 Enum 的缺点:每一个枚举值都是一个对象,在使用它时会增加额外的内存消耗,所以枚举相比于Integer和String会占用更多的内存,较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的开销,使我们的应用需要更多的空间。特别是分dex的大APP,枚举的初始化很容易导致ANR。

@Retention(SOURCE)
@StringDef({
    POWER_SERVICE,
    WINDOW_SERVICE,
    LAYOUT_INFLATER_SERVICE
})
public @interface ServiceName {}
public static final String POWER_SERVICE = "power";
public static final String WINDOW_SERVICE = "window";
public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
@Retention(SOURCE)
@IntDef({
    NAVIGATION_MODE_STANDARD, 
    NAVIGATION_MODE_LIST, 
    NAVIGATION_MODE_TABS
})
public @interface NavigationMode {}
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;
  ...
public abstract void setNavigationMode(@NavigationMode int mode);
@NavigationMode
public abstract int getNavigationMode();

自定义注解

上面的枚举注解,就是自定义注解的例子。自定义注解使用关键字@interface声明,然后用相应的修饰。

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Entity {
    String value();
    String name();
}
  • @Documented表示拥有该注解的元素可通过javadoc此类的工具进行文档化。该类型应用于注解那些影响客户使用带注释(comment)的元素声明的类型。如果类型声明是用Documented来注解的,这种类型的注解被作为被标注的程序成员的公共API。
  • @Inherited:表示该注解类型被自动继承
  • @Retention:表示该注解类型的注解保留的时长。可用的参数都在枚举类型RetentionPolicy中给出了定义。当注解类型声明中没有@Retention元注解,则默认保留策略为RetentionPolicy.CLASS。
  • @Target:表示该注解类型的所使用的程序元素类型。可用的参数都在枚举类型ElementType中给出了定义。当注解类型声明中没有@Target元注解,则默认为可适用所有的程序元素。

附录:ButterKnife

使用ButterKnife只需要在module的gradle文件中加入下面代码:

implementation 'com.jakewharton:butterknife:8.4.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'

在library工程中,直接使用R.xxx.xxx会报“元素值必须为常量表达式”的错误提示,处理步骤如下:

  • 在根gradle文件中添加(版本根据需要变化):
classpath 'com.jakewharton:butterknife-gradle-plugin:8.4.0'
  • 在module的gradle中添加:
apply plugin: 'com.jakewharton.butterknife'
  • R.xxx.xxx替换成R2.xxx.xxx

参考文章

从源码的角度深入理解Java的动态代理机制(下)

        在上篇文章中我们讲解了如何通过动态代理解决在HuaWei手机Android 9.0版本中注册BroadcastReceiver超过1000引发应用crash的问题,但这种解决方式并不太合理,正确的做法是在注册完BroadcastReceiver后当不需要使用BroadcastReceiver时应及时反注册从而保证应用注册的BroadcastReceiver数量没有超过阀值。这篇文章我们就从源码的角度深入学习一下Java的动态代理机制。

        通过上篇文章我们知道使用Java的动态代理功能有两个前提条件:一个是有接口的存在;另一个是必须有接口的实现类和实例。只有满足了这两个条件,我们才能使用Java的动态代理功能对接口中的方法做代理。使用Java的动态代理功能就是使用Proxy和InvocationHandler这两个API,入口就是使用Proxy的newProxyInstance()方法,newProxyInstance()方法源码如下:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException {
    // h必须参数,为null就抛异常
    if (h == null) {
        throw new NullPointerException();
    }
    // 备份传递进来的interfaces数组
    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    // 权限检查,可忽略
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * getProxyClass0()方法是重点,功能是从缓存中查找或者生成目标类,
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    try {
        // 获取指定参数constructorParams的构造方法类
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        // 权限检查,最终也是调用newInstance()方法
        if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) {
            return AccessController.doPrivileged(new PrivilegedAction<Object>() {
                public Object run() {
                    // 调用newInstance()方法,注意ih参数最终传递给了cons
                    return newInstance(cons, ih);
                }
            });
        } else {
            // 调用newInstance()方法,注意ih参数最终传递给了cons
            return newInstance(cons, ih);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString());
    }
}

        Proxy的newProxyInstance()方法需要三个参数,分别表示如下:

  • loader 待生成代理类的类加载器
  • interfaces 待生成代理类需要实现的接口
  • h 执行代理类中的方式时对外提供的回调

        newProxyInstance()方法的流程先校验参数h是否为null,如果为null就直接抛异常,接着对参数interfaces做备份,最后调用getProxyClass0()方法获取一个Class类型的cl对象,获取到cl后调用cl含有一个InvocationHandler参数的构造方法并赋值给cons,最后调用newInstance(cons, ih)方法通过反射生成一个代理类对象并返回,根据以上流程我们可以得出生成代理对象的一些简单结论:该代理对象有一个带有InvocationHandler参数的构造方法,在生成代理对象的时候我们把外界的InvocationHandler实例ih传递给了该代理类。

        Proxy的newProxyInstance()方法流程很简单,重点在其内部的getProxyClass()方法,源码如下:

private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    // 接口类数量不能超过65535,否则抛异常
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // 从缓存中查找,如果缓存中没有则调用ProxyClassFactory创建
    return proxyClassCache.get(loader, interfaces);
}

        getProxyClass0()方法先对参数interfaces数组做了长度校验,如果超过65535就抛异常(也就是说使用Java的动态代理功能一次性最多只能代理65535个接口),最后通过proxyClassCache的get()方法获取到代理Class,proxyClassCache是Proxy的内部静态属性,它是WeakCache类型,核心功能是提供缓存来提高效率,proxyClassCache的定义如下所示:

// proxyClassCache的初始化
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
    proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

// WeakCache构造方法
public WeakCache(BiFunction<K, P, ?> subKeyFactory,
                 BiFunction<K, P, V> valueFactory) {
    // subKeyFactory是KeyFactory实例
    this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
    // valueFactory是ProxyClassFactory实例
    this.valueFactory = Objects.requireNonNull(valueFactory);
}

        WeakCache对外提供的构造方法WeakCache(BiFunction subKeyFactory, BiFunction valueFactory)需要两个BiFunction类型的参数,其中subKeyFactory的作用是生成缓存key,valueFactory的作用是当缓存中不存在目标Class时则负责创建Class(从源码可知proxyClassCache在初始化的时候传递了KeyFactory和ProxyClassFactory实例对象)。接下来我们看一下WeakCache的get()方法,如下所示:

public V get(K key, P parameter) {
    // 对参数parameter做非空校验
    Objects.requireNonNull(parameter);

    expungeStaleEntries();
    // 生成一个特殊的key:cacheKey
    Object cacheKey = CacheKey.valueOf(key, refQueue);

    // 根据cacheKey从缓冲中查找
    ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
    if (valuesMap == null) {
        ConcurrentMap<Object, Supplier<V>> oldValuesMap
            = map.putIfAbsent(cacheKey,
                              valuesMap = new ConcurrentHashMap<>());
        if (oldValuesMap != null) {
            valuesMap = oldValuesMap;
        }
    }


    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
    Supplier<V> supplier = valuesMap.get(subKey);
    Factory factory = null;

    while (true) {
        if (supplier != null) {
            // 调用supplier的get()方法
            V value = supplier.get();
            if (value != null) {
                return value;
            }
        }

        // 初始化factory
        if (factory == null) {
            factory = new Factory(key, parameter, subKey, valuesMap);
        }

        // 初始化supplier
        if (supplier == null) {
            supplier = valuesMap.putIfAbsent(subKey, factory);
            if (supplier == null) {
                supplier = factory;
            }
        } else {
            if (valuesMap.replace(subKey, supplier, factory)) {
                supplier = factory;
            } else {
                supplier = valuesMap.get(subKey);
            }
        }
    }
}

        WeakCache的get()方法核心就是从缓冲中查找目标Class对象,如果找到就返回,否则就调用ProxyClassFactory实例创建,ProxyClassFactory的源码如下:

private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
    // 生成代理Class的前缀
    private static final String proxyClassNamePrefix = "$Proxy";

    // 通过序列号确保生成的代理Class名字唯一
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        // 遍历数组做相关校验
        for (Class<?> intf : interfaces) {
            Class<?> interfaceClass = null;
            try {
                // 通过loader加载传递进来的接口Class
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            // 验证是否是同一个类加载器,如果不是就抛异常
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }

            // 验证是否是接口
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + " is not an interface");
            }
            // 禁止重出现复接口
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface: " + interfaceClass.getName());
            }
        }

        // 待生成代理Class的包名
        String proxyPkg = null;

        // 确定代理Class的包路径,如果接口数组中只有一个包访问权限的接口,则生成代理Class的包路径就是该接口的包路径,如果包含多个包访问权限的接口,则抛异常
        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        // 验证结束如果没有包权限的接口,则生成代理Class的包名为com.sun.proxy.
        if (proxyPkg == null) {
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        // 生成代理类的名字,格式为:com.sun.proxy.$Proxy0 ... com.sun.proxy.$ProxyN等
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        // 调用ProxyGenerator的generateProxyClass()方法生成二进制文件
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);
        try {
            // 调用native方法defineClass0()生成代理Class对象并返回
            return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            throw new IllegalArgumentException(e.toString());
        }
    }
}

        ProxyClassFactory的apply()方法注解写的很详细,该方法的流程是先遍历数组参数判断接口类的合法性,然后确定待生成Class的包路径和名字,准备就绪之后调用ProxyGenerator的generateProxyClass()方法生成一个byte[]数组(byte[]数组内容就是待生成代理类的二进制文件),最后调用native方法defineClass0()生成一个Class对象并返回。

        ProxyGenerator类在sun.misc包下,其源码可参照这里http://www.docjar.com/html/api/sun/misc/ProxyGenerator.java.html, 它的功能就是根据JVM规范生成Class类的二进制文件,ProxyGenerator类中的具体流程就不再分析了,有感兴趣的可以自行分析,由于没法直接调用该方法,我通过反射方式调用了ProxyGenerator的generateProxyClass()方法后得到了byte[]数组并将该byte[]数组内容直接保存在.class文件中,最后反编译该文件并重写排版后,结果如下所示:

public final class $Proxy0 extends Proxy implements IActivityManager {

    // 基础方法
    private static Method m0;   // hashCode()方法
    private static Method m1;   // equals()  方法
    private static Method m2;   // toString()方法

    // 以下是IActivityManager接口中定义的方法
    private static Method m3;   // registerReceiver()方法
    private static Method m4;   // unregisterReceiver()方法
    // ......                    

    static {
        try {
            // 基础方法
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);

            // 接口中定义的方法
            m3 = Class.forName("android.app.IActivityManager").getMethod("registerReceiver", new Class[] { Class.forName("android.content.BroadcastReceiver"), Class.forName("android.content.IntentFilter") });
            m4 = Class.forName("android.app.IActivityManager").getMethod("unregisterReceiver", new Class[] { Class.forName("android.content.BroadcastReceiver") });

            // ......

        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }

    public Proxy0(InvocationHandler var1) {
        super(var1);
    }

    public final int hashCode() {
        try {
            return ((Integer) super.h.invoke(this, m0, (Object[]) null)).intValue();
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final boolean equals(Object var1) {
        try {
            return ((Boolean) super.h.invoke(this, m1, new Object[] { var1 })).booleanValue();
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() {
        try {
            return (String) super.h.invoke(this, m2, (Object[]) null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final Intent registerReceiver(BroadcastReceiver var1, IntentFilter var2) {
        try {
            return (Intent) super.h.invoke(this, m3, new Object[] { var1, var2 });
        } catch (RuntimeException | Error var4) {
            throw var4;
        } catch (Throwable var5) {
            throw new UndeclaredThrowableException(var5);
        }
    }

    public final void unregisterReceiver(BroadcastReceiver var1) {
        try {
            super.h.invoke(this, m4, new Object[] { var1 });
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    // 省略部分方法
}

        根据反编译的结果,我们发现ProxyGenerator生成的$Proxy0类和它的内部方法都有final修饰符,它继承了Proxy且实现了从外界传递进来的接口类IActivityManger。$Proxy0类中定义了一系列Method对象,这些Method对象是在static语句块中进行的初始化(放在static语句块中确保这些Method对象仅且仅初始化一次),通过这些Method的初始化我们发现它们分别代表了接口中定义的方法和toString()、equals()、hashCode()方法,我们拿registerReceiver()方法举例:

public final Intent registerReceiver(BroadcastReceiver var1, IntentFilter var2) {
    try {
        return (Intent) super.h.invoke(this, m3, new Object[] { var1, var2 });
    } catch (RuntimeException | Error var4) {
        throw var4;
    } catch (Throwable var5) {
        throw new UndeclaredThrowableException(var5);
    }
}

        registerReceiver()方法中调用的是父类的super.h的invoke()方法,h是父类Proxy类中定义的属性,它是InvocationHandler类型(根据前边$Proxy0的初始化可知h就是我们调用Proxy的newProxyInstance()方法时传递的第三个参数)。在调用invoke(Object proxy, Method method, Object[] args)方法时第一个参数proxy传递的this(也就是说把当前$Proxy0对象本身传递给了super.h的invoke()方法中),第二个参数method传递的是m3,简单理解就是把当前方法名传递进去了(可以看m3的初始化代码),第三个args参数把当前方法的参数作为新的参数传递给了invoke()方法中,之后把invoke()方法执行的结果返回了。这就是生成代理类对方法的处理,其它方法也是同样逻辑就不再叙述了。

        接下来我们看一下InvocationHandler的源码,如下:

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

        InvocationHandler接口只定义了一个invoke()方法,该方法接收三个参数,其参数分别表示如下:

  • proxy 表示通过ProxyGenerator类动态创建出来的代理类的实例:$Proxy0、$Proxy1…$ProxyN
  • method 表示将要执行的方法,一般是接口中定义的方法或是toString(),equals(),hashCode()方法
  • args 表示执行method方法所需要的参数,如果method没有参数那么args为null

        分析到这里已经可以回答在上篇文章末尾遗留的问题了,调用Proxy的newProxyInstance(ClassLoader loader, Class[] interfaces, InvacationHandler h)方法返回一个Object对象,该对象就是$Proxy0、$Proxy1……$ProxyN,它们继承了Proxy类并且实现了参数interfaces中的接口,当调用这些代理对象的方法时又委托给我们从外界传递的InvocationHandler类型参数h从而允许我们有一个入口来对interfaces接口中的方法做拦截操作,仔细回味一下这种代码设计,真的很棒!!!

        好了,到这里已经分析完了Java的动态代理机制,由于篇幅原因部分细节没有做分析(特别是调用ProxyGenerator的generateProxyClass()方法),但这并不影响我们主流程的分析,有感兴趣的小伙伴可以自行分析,最后感谢收看(*^__^*) ……

Android控件阴影实现渐变

上一篇文章:Android控件阴影的一种实现方式,介绍了一个给控件或者容器实现一个不带渐变的简单阴影的方法。但是,有些时候希望阴影部分的颜色是有渐变的,这里介绍一个带渐变的阴影实现ShadowGradientDrawable。

注意,这里介绍的方法中赞不考虑偏移问题,如果需要偏移需要添加额外的计算,有兴趣可以自己实现。

  • 创建GradientShadowDrawable,继承自Drawable
public class GradientShadowDrawable extends Drawable {

    @Override
    public void draw(@NonNull Canvas canvas) {
    }

    @Override
    public void setAlpha(int alpha) {
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
    }

    @Override
    public int getOpacity() {
        // 不透明度,设置为半透明
        return PixelFormat.TRANSLUCENT;
    }
  • 几个重要的参数:最大阴影宽度(mMaxShadowSize),阴影宽度(mShadowSize),圆角大小(mCornerRadius),背景颜色(mBackgroundColor),阴影渐变起始颜色(mShadowStartColor),阴影渐变结束颜色(mShadowEndColor)。
    这个实现中,边缘空白区域是通过mMaxShadowSize的值进行预留的,不管是否有阴影,都会有边缘空白区域,阴影会投射到这个区域。

  • 画笔初始化

// 四角扇形阴影区域画笔
mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mCornerShadowPaint.setStyle(Paint.Style.FILL);
// 四面矩形阴影区域画笔
mEdgeShadowPaint = new Paint(mCornerShadowPaint);
mEdgeShadowPaint.setAntiAlias(false);
  • 确定中间非阴影区域边界和左上角扇形区域(mCornerShadowPath),说明,这里的位置都是相对View左上角的相对位置。
mCardBounds.set(bounds.left + mMaxShadowSize, bounds.top + mMaxShadowSize,
                bounds.right - mMaxShadowSize, bounds.bottom - mMaxShadowSize);
private void buildShadowCorners() {
        RectF innerBounds = new RectF(mMaxShadowSize, mMaxShadowSize,
                mMaxShadowSize + 2 * mCornerRadius, mMaxShadowSize + 2 * mCornerRadius);
        RectF outerBounds = new RectF(innerBounds);
        outerBounds.inset(-mShadowSize, -mShadowSize);

        if (mCornerShadowPath == null) {
            mCornerShadowPath = new Path();
        } else {
            mCornerShadowPath.reset();
        }
        mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
        mCornerShadowPath.moveTo(mMaxShadowSize, mMaxShadowSize + mCornerRadius);
        mCornerShadowPath.rLineTo(-mShadowSize, 0);
        // outer arc
        mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
        // inner arc
        mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
        mCornerShadowPath.close();

        float startRatio = mCornerRadius / (mCornerRadius + mShadowSize);
        int starColor = mShadowStartColor.getColorForState(getState(), mShadowStartColor.getDefaultColor());
        int endColor = mShadowEndColor.getColorForState(getState(), mShadowEndColor.getDefaultColor());
        // 角落扇形区域设置圆形渐变:中心 + 半径 + 渐变区间
        mCornerShadowPaint.setShader(new RadialGradient(mMaxShadowSize + mCornerRadius, mMaxShadowSize + mCornerRadius,
                mCornerRadius + mShadowSize,
                new int[]{starColor, starColor, endColor},
                new float[]{0f, startRatio, 1f},
                Shader.TileMode.CLAMP));
    }
  • 最后在draw方法内实现绘制过程
@Override
    public void draw(@NonNull Canvas canvas) {
        drawShadow(canvas);
        drawBackground(canvas);
    }

private void drawShadow(Canvas canvas) {
        // LT : 绘制左上角扇形区域,其他三个扇形区域绘制通过平移(translate)和旋转(rotate)实现
        int saved = canvas.save();
        canvas.translate(0, 0);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        canvas.restoreToCount(saved);

        // RT
        saved = canvas.save();
        canvas.translate(mMaxShadowSize + mCardBounds.right, 0);
        canvas.rotate(90f);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        canvas.restoreToCount(saved);

        // LB
        saved = canvas.save();
        canvas.translate(0, mMaxShadowSize + mCardBounds.bottom);
        canvas.rotate(270f);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        canvas.restoreToCount(saved);

        // RB
        saved = canvas.save();
        canvas.translate(mMaxShadowSize + mCardBounds.right, mMaxShadowSize + mCardBounds.bottom);
        canvas.rotate(180f);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        canvas.restoreToCount(saved);

        // 边缘矩形区域都是相对于左上角(0,0)位置的矩形区域
        int starColor = mShadowStartColor.getColorForState(getState(), mShadowStartColor.getDefaultColor());
        int endColor = mShadowEndColor.getColorForState(getState(), mShadowEndColor.getDefaultColor());
        float left, top, right, bottom;
        // 上边边缘
        left = mMaxShadowSize + mCornerRadius;
        top = mMaxShadowSize - mShadowSize;
        right = mCardBounds.width() + mMaxShadowSize - mCornerRadius;
        bottom = mMaxShadowSize;
        // 边缘区域的阴影渐变,使用的是线性渐变
        mEdgeShadowPaint.setShader(new LinearGradient(left, bottom, left, top, new int[]{starColor, endColor}, null, Shader.TileMode.CLAMP));
        canvas.drawRect(left, top, right, bottom, mEdgeShadowPaint);

        // 右边边缘
        left = mMaxShadowSize + mCardBounds.width();
        top = mMaxShadowSize + mCornerRadius;
        right = mMaxShadowSize + mCardBounds.width() + mShadowSize;
        bottom = mCardBounds.height() + mMaxShadowSize - mCornerRadius;
        mEdgeShadowPaint.setShader(new LinearGradient(left, top, right, top, new int[]{starColor, endColor}, null, Shader.TileMode.CLAMP));
        canvas.drawRect(left, top, right, bottom, mEdgeShadowPaint);

        // 下边边缘
        left = mMaxShadowSize + mCornerRadius;
        top = mMaxShadowSize + mCardBounds.height();
        right = mCardBounds.width() + mMaxShadowSize - mCornerRadius;
        bottom = mMaxShadowSize + mCardBounds.height() + mShadowSize;
        mEdgeShadowPaint.setShader(new LinearGradient(left, top, left, bottom, new int[]{starColor, endColor}, null, Shader.TileMode.CLAMP));
        canvas.drawRect(left, top, right, bottom, mEdgeShadowPaint);

        // 左边边缘
        left = mMaxShadowSize - mShadowSize;
        top = mMaxShadowSize + mCornerRadius;
        right = mMaxShadowSize;
        bottom = mCardBounds.height() + mMaxShadowSize - mCornerRadius;
        mEdgeShadowPaint.setShader(new LinearGradient(right, top, left, top, new int[]{starColor, endColor}, null, Shader.TileMode.CLAMP));
        canvas.drawRect(left, top, right, bottom, mEdgeShadowPaint);
    }

private void drawBackground(Canvas canvas){
    canvas.drawRoundRect(bounds, cornerRadius, cornerRadius, paint)
}

OkHttp源码解析(一) —请求和响应的处理过程

一直以来只是知道OkHttp是一个强大又精巧的http client库, 简单看过api以及它的一个整体的设计, 并一直在项目里边用,但并没有进入到代码层面去仔细看他的实现,因此花了一些时间来读他的源码并记录一些细节,希望对更好的使用OkHttp起到一些积极作用,避免出现一些使用上的问题,同时学习它的设计思路。

我们从一个Http请求的发出及得到相应过程入手,来梳理下它的源码。 使用OkHttp来发送http请求有同步及异步两种方式,同步方式如下:

OkHttpClient eagerClient = client.newBuilder()
    .build();
Response response = eagerClient.newCall(request).execute();

异步方式如下:

OkHttpClient eagerClient = client.newBuilder()
    .build();
Response response = eagerClient.newCall(request).enqueue(new Callback () {
    public void onFailure(Call call, IOException e) {
    }
    public void onResponse(Call call, Response response) throws IOException {}
    )
}

略过OkHttpClient实例的构建过程不讲,无论同步还是异步的方式都是先通过client实例的newCall调用获得一个Call实例,newCall的实现如下:

/**
   * Prepares the {@code request} to be executed at some point in the future.
   */
  @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }

可以看到返回的是一个RealCall(实现了Call接口)的实例,因此RealCall就是http调用接口的真正实现了,以下是RealCall的同步调用execute()的实现(只摘取关键部分):

  @Override public Response execute() throws IOException {
    synchronized (this) {
        /*一些异常处理忽略*/
    eventListener.callStart(this);//通知调用开始
    try {
      client.dispatcher().executed(this);//通知dispatcher调用执行开始
      Response result = getResponseWithInterceptorChain();//这一行最关键,是真正的http调用
      return result;
    } catch (IOException e) {
      eventListener.callFailed(this, e);//通知调用失败
      throw e;
    } finally {
      client.dispatcher().finished(this);//通知dispatcher调用结束
    }
  }

异步调用enqueue()方法的实现如下:

@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
    eventListener.callStart(this);
    client.dispatcher().enqueue(new AsyncCall(responseCallback));//生成一个AsyncCall对象放入到dispatcher中
  }

其实这个方法没有什么太多的信息量,AsyncCall是一个实现只是把一个AsyncCall对象放入到了dispatcher中,我们再看dispatcher.enqueue()的处理:

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call); //放入线程池执行
    } else {
      readyAsyncCalls.add(call); //添加到待执行列表
    }
  }

根据变量名runningAsyncCalls, runningCallsForHost,应该是说当所有这个dispatcher上的请求小于最大允许请求数,且对于这个请求的主机并发数小于单个主机最大允许请求数,则放入线程池执行,否则添加到待执行列表。 AsyncCall 继承自 NamedRunnable(实现了Runnable接口),NamedRunnable的run方法调用了: execute方法,因此我们需要看一下AsyncCall的execute方法才知道真正做的事情是什么:

    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain(); //看到了跟同步调用统一的实现,这里应该是真正的请求实现
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          eventListener.callFailed(RealCall.this, e);
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

我们看到无论同步还是异步的http请求调用最终都是走到了realcall.getResponseWithInterceptorChain(), 这个方法应该是最核心的逻辑了,方法实现如下:

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors()); //应用设置的拦截器
    interceptors.add(retryAndFollowUpInterceptor);//重试和302跳转处理拦截器
    interceptors.add(new BridgeInterceptor(client.cookieJar())); //处理用户请求,添加header
    interceptors.add(new CacheInterceptor(client.internalCache())); //处理缓存逻辑
    interceptors.add(new ConnectInterceptor(client)); //处理连接逻辑
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());//处理app设置的网络连接器
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));//处理往服务端写数据以及读数据的逻辑

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis()); //生成一个真正的Chain

    return chain.proceed(originalRequest);//真正执行
  }

看到了熟悉的interceptor和Chain的逻辑,这是OkHttp的核心设计,不同部分的处理逻辑分散在不同的Interceptor中,同时允许应用扩展interceptor, 靠Chain连接起不同的interceptor, 实现了不同逻辑之间的解耦,同时靠Interceptor的前后顺序控制了请求发出时的处理顺序,以及相应到达后的处理顺序(和请求发出时相反),相应的实现在RealInterceptorChain中, 核心逻辑如下:

@Override public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
  }

  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
      //忽略了一些错误监测
    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout); //生成下一个链对象
    Interceptor interceptor = interceptors.get(index); //得到interceptor
    Response response = interceptor.intercept(next); //调用interceptor的intercept方法
    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }
      ///忽略了一些错误监测
    return response;
  }

因为interceptor的intercept方法中调用chain.proceed方法,而chain的proceed方法中又生成了下一个chain对象,调用了下一个interceptor的intercept方法,这样所有的interceptor的方法就会一直调用下,那直到某一个interceptor的方法不再调用proceed方法,这样后边的interceptor就不再调用,整个链就停止了。

猜测缓存处理的逻辑(CacheInterceptor)可能会找到缓存会直接返回response而不调用proceed,最后的请求服务器的逻辑(CallServerInterceptor)也不会调用chain的proceed方法,不然就会数组越界,查看了相应的代码证实确实如此。

这样整个从请求到拿到响应的大的逻辑就走通了,其实整体逻辑还是非常清晰和简单的。

在读源码的过程中的一个比较有疑问的问题, 在RealCall的getResponseWithInterceptorChain方法中生成的RealInterceptorChain的代码:

Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

几个非常重要的参数:

public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
      HttpCodec httpCodec, RealConnection connection, int index, Request request, Call call,
      EventListener eventListener, int connectTimeout, int readTimeout, int writeTimeout) {

StreamAllocation, HttpCodec, RealConnection 都为空,通过阅读文件头注释和方法注释,可知: StreamAllocation是用来协调3个实例: Connections(物理的和服务端的socket连接), Streams(逻辑的http请求/相应), Calls(逻辑的请求实例).
HttpCodec是用来编码http请求和解析http响应的接口定义,有Http1Codec和Http2Codec两个实现分别处理http1.x以及http2.x的协议。
RealConnection是连接的实现,里边包含了socket连接等逻辑。 那么这3个类的实例是在哪里初始化的呢?

利用IDE的findUsage功能,在3个类的构造函数上分别找了下,很容易找到了答案, StreamAllocation在所有的interceptor第一个RetryAndFollowUpInterceptor 的intercept方法中(除去应用自己的)初始化的,然后在后边的RealInterceptorChain的proceed方法中就传入了这个实例。
HttpCodec在ConnetInterceptor的intercept方法中调用 streamAllocation.newStream方法生成了,也就是在连接服务器时生成的。
RealConnection是在StreamAllocation中的findConnection方法时,调用acquire方法将这个streamAllocation对象和connection对象绑定到一起的,同样findConnection也是在newStream方法时被调用到的,因此也是在ConnetInterceptor.intercept方法时生成的。

自此整个http请求的请求和响应的逻辑就基本畅通了。当然OkHttp源码还有很多其他比较重要的逻辑,比如: 线程管理、IO操作等,我们后边的文章再详细讲。

下篇预告:
OkHttp源码解析(二) — 线程管理

Android控件阴影的一种实现方式

有些APP的设计使用iOS的设计风格,比如伴鱼少儿英语的新版设计,这样就要求在Android设备上也要实现同样的但没有系统支持的界面效果。

给界面元素(包括内容控件和容器)加阴影,就是其中的一个。iOS系统提供了界面元素的阴影支持,但是在Android系统上没有这种支持。Google在安卓支持包com.android.support:cardview-v7中给出了一个带阴影的CardView,但是这个CardView的阴影效果不好,阴影颜色不可更改,特别难看。

自定义实现阴影的效果有多种,这里介绍的一种是构造一个不带渐变的背景元素ShadowDrawable。

首先需要说明的是,Android中自定义实现的阴影都是在界面元素内部的,所以展示阴影效果的同时会占用控件的边沿空间,导致内容展示的空间被压缩,所以在设计界面元素的大小时,要把内容元素的大小和阴影效果占用的空间同时考虑进来。

  • 创建ShadowDrawable类,继承自Drawable
public class ShadowDrawable extends Drawable {

    @Override
    public void draw(@NonNull Canvas canvas) {
    }

    @Override
    public void setAlpha(int alpha) {
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
    }

    @Override
    public int getOpacity() {
        // 不透明度,设置为半透明
        return PixelFormat.TRANSLUCENT;
    }
  • 几个重要的参数:边沿阴影宽度(mShadowWdith)、阴影起始颜色(mShadowColor)、背景颜色(mBackgroundColor)、X方向偏移量(mOffsetX)、Y方向偏移量(mOffsetY)、圆角大小(mShapeRadius)
  • 初始化背景阴影画笔对象
mShadowPaint = new Paint();
mShadowPaint.setColor(Color.TRANSPARENT);
mShadowPaint.setAntiAlias(true);
mShadowPaint.setShadowLayer(mShadowWidth, mOffsetX, mOffsetY, mShadowColor);
mShadowPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));
  • 内容展示区域的计算,覆盖实现基类方法setBounds
@Override
    public void setBounds(int left, int top, int right, int bottom) {
        // left/top/right/bottom是Drawable在界面元素中的位置,数字值是相对界面元素左上角的距离
        super.setBounds(left, top, right, bottom);
        mRect = new RectF(left + mShadowWidth - mOffsetX, top + mShadowWidth - mOffsetY, right - mShadowWidth - mOffsetX,bottom - mShadowWidth - mOffsetY);
    }
  • 最后在draw方法中画出阴影和内容区域
    @Override
    public void draw(@NonNull Canvas canvas) {
        Paint backgroundPaint = new Paint();
        backgroundPaint.setAntiAlias(true);
        backgroundPaint.setColor(mBackgroundColor);
        // 绘制阴影部分
        canvas.drawRoundRect(mRect, mShapeRadius, mShapeRadius, mShadowPaint);
        // 绘制内容区域背景
        canvas.drawRoundRect(mRect, mShapeRadius, mShapeRadius, backgroundPaint);
    }

Android通用翻页组件的实现

在伴鱼绘本1.0版中,有两个很重要的核心功能是听绘本和录绘本,这两个功能在页面展现上形式类似,都是模拟看实体绘本书,一页一页的展示,其中有一个非常核心的交互体验设计是翻页效果的设计。当时找了几个效果给大家评估,其中非常复杂的逼近真实翻书体验的、带纸张卷曲效果的翻页效果,然而大家非常明智的认为,这个效果太复杂了,翻页还是应该简单直接一些,因此最终决定,还是简单一些,对于竖向的绘本(图片宽<)手机竖屏展示,模拟书脊在左边,对于横向(图片宽>)绘本手机横屏展示,书脊在中间,用户左右滑动屏幕时,跟随用户手势,做一个页面翻起并落下的效果即可。

然而,这个简单的效果也不是Android自带的,简单的翻了下Github也没有找到类似的实现,看来只能自己造轮子了。因为要左右滑动翻页来展示整个绘本内容,多的可能要20~30页,同时每页也要展示文本、页码等元素,使用ViewPager来实现这个左右滑动的效果是最恰当的。

ViewPager中,默认的页面切换效果是滑动,页面从左到右排列,跟随手势做滑动操作,翻了下ViewPagerAPI文档,以及源码,可以看到setPageTransformer可以设置自定义的页面切换效果,允许在scroll的时候自定义实现页面切换效果。ViewPagersetPageTransformer方法定义如下,

PageTransformer这个接口定义如下:

根据文档在页面Scroll的时候,会调用这个自定义的接口,来根据-1 1 的区间映射到页面从最左边到最右边的位置,这样应用可以添加自定义的变换到页面上以实现自定义的页面切换效果。查看ViewPager的源码,在onPageScrolled方法实现中找到mPageTransformer的调用如下:

也就是说每次onPageScroll 所有ViewPager的子View,除了标记为isDecor的, 也就是说所有的页都会调用一次mPageTransformer.transformPage,用来设置变换。看明白这个原理,很容易就能想到对于竖向的绘本页,在滑动时将每一页的X坐标位置都置到左上角,然后对于正在翻页的那一页绕Y轴做恰当角度的旋转变换,再加上一个透视视角,就能达到竖向绘本沿着书左书脊翻书的效果,代码如下:

主要是3部分逻辑,对于不显示的页,将其透明度置为0,将可能之前设置的Rotation, translation都清除,防止其影响画面展示, 对于画面中显示的两页,首先保证两页显示X坐标都为0且不变,由于ViewPager默认已经将两个页面移动了位置(源码见ViewPageronPageScrolled方法),所以需要做一个反向的setTranslationX操作,将页面复位,然后对于前边一页(正在翻的页),计算旋转角度,设置恰当的camera位置,对于后边一页(不动的页),清空旋转角度。

这个竖向的绘本页翻页动画关键逻辑就这么点了,相对比较简单。但是仔细想下横向的翻页,由于书脊在中间,在页面切换的过程中,某一页绘本半页做Y轴旋转,半页不动,ViewPager提供的这中自定义的扩展不灵了起码不是很直接就能解决问题了。

在绘本页上除了绘本图片还有相关文本的展示,因此不能只绘制图片,还需要处理文本,最好的方案是按整页View处理,而不是手动去绘制图片和文本。有一个思路是当监测到用户滑动页面的时候,不绘制ViewPager的页面,而是将需要显示每一页View截图成一张Bitmap,分成左右两个Bitmap,然后根据滑动距离对Bitmap做一定的变换,绘制出翻页效果。但是这样做有一个问题是每一页里边的绘本图片是异步加载的,可能用户滑动的过程中图片加载出来,这样也需要处理图片加载出来时再更新一下Bitmap,需要将图片下载的逻辑和Bitmap更新的逻辑耦合起来,没办法做成通用的组件,将页面的内容和页面切换效果隔离开来。

更通用的方案是在View的绘制周期做特殊处理,绘制出翻页效果,这样也利用到了View的内容刷新机制,只要View刷新就会自动触发重绘流程。 那么利用View/ViewGroup的那个绘制函数?因为整个翻页效果涉及到多个页元素的自定义绘制,因此自定义ViewPager(继承自ViewGroup)的某个绘制方法可能是合适的方案。一般可以重写drawChild,重写绘制单个子View的过程或者dispatchDraw完整的重写整个ViewGroup的绘制子View的过程,dispatchDraw的灵活性会更大一些,可以完整的控制绘制整个子View的过程,可以以任意顺序绘制。于是就可以根据滑动的比例计算出应该旋转的角度,然后canvasclip一半子View大小,这样就能半个半个的绘制子View实现从View中间翻转的效果。

还有一个点是ViewPager是动态新增和消耗View,甚至重用View的,如何根据页面position找到对应是哪个View在负责显示这一页,从而绘制正确的页?翻看ViewPager的源码,可以看到ViewPager.LayoutParams 有一个position 的变量 用来保存当前View显示的数据的position,但是这个变量是保护的,没办法直接取到,可以通过反射方式来取得具体的值。代码如下:

取得这个值以后,就能通过遍历所有子View的方式找到正确的View

绘制的过程并不复杂,分成两种情况,当前页翻转过程未到50%时,后一个页面右半部分可以显示出来部分,需要最先绘制(简单处理可以全部绘制或者只绘制右半部分,因为会被其他部分覆盖),当前页面左半边是正常绘制,右半边绕中心点旋转一定角度翻转透视,  然后绘制。代码如下(current表示当前第一个显示的页面):

第二种情况是翻转过了50%时, 当前页面只绘制左半边页面(优先绘制),后一页的左半页做一定旋转和透视变换,右半页正常绘制,代码如下:

这样整个动画的绘制就完成了,如果理解了整个View的绘制流程,以及坐标变换的一些知识,即便这种可能半个View需要做变换,另外半个View不太一样的情况,是不是也比较简单就能实现了?