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

Android Lint扫描规则说明(一)

主要内容

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

Accessibility

可访问性的检查,除了第一项之外,其他项更像是为某些自动化的工具做的准备工作,不影响APP的运行。

ClickableViewAccessibility

可点击View的可访问性:如果重写onTouchEvent或使用OnTouchListener的View在检测到单击时没有实现performClick并调用它,则视图可能无法正确处理可访问性操作。理想情况下,处理单击操作的逻辑应该放在View#performClick中,因为当单击操作发生时,一些可访问性服务会调用performClick。

ContentDescription

非文本Widget描述:
– 首先,像ImageViews和ImageButtons这样的非文本Widget应该使用contentDescription属性指定文本对Widget进行说明,以便屏幕阅读器和其他可访问性工具能够充分描述和理解用户界面。
– 其次,如果一个非文本Widget在用户界面上只是一个装饰,不展示任何内容也不接受任何用户操作,就不需要给它提供描述性的contentDescription属性文本,而是使用tool属性ContentDescription抑制lint提醒。
– 第三,对于文本型Widget不能同时设置hintcontentDescription,否则hint将不会展示在界面上,只设置hint就可以。

参考:Make apps more accessible

GetContentDescriptionOverride

重写非文本Widget描述方法getContentDescription:重写View的getContentDescription方法,可能会阻止某些可访问性服务正确导航视图公开的内容。相反,当内容描述需要更改时,调用setContentDescription。

KeyboardInaccessibleWidget

键盘无法访问Widget:如果一个Widget声明了可以点击,但是没有声明可以获得焦点,这个Widget是无法通过键盘访问的,需要设置focusable=true。

LabelFor

缺少可访问标签:可编辑的控件例如EditText应该为hint属性赋值,或者在minSDKVersion大于等于17时,使用labelFor属性为EditText指定一个标签控件。标签控件可以是指定了text属性的文字控件如TextView,也可以是指定了contentDescription的非文字控件如ImageView。

如果被指定的标签控件如TextView在另外一个layout文件中,且使用layout属性引用了EditText所在的layout文件,可以ignore这个lint检查。

Internationalization

ByteOrderMark

查了一下这个ByteOrderMark,简称BOM,指的是一些标记字符。这种问题一般是“在不同编码格式的文件之间拷贝字符或者在某些文件系统上编辑了文件”导致的。

文件内BOM提醒:Lint会把文件中包含的BOM字符标记出来。因为Android工程中我们期望使用utf-8对文件和字符进行编码。BOM字符对utf-8来说不是必需的,而且有一些工具是不能正确处理带BOM字符的文本的。

参考:Android提示BOM错误排查UTF8最好不要带BOM

EnforceUTF8

资源文件编码格式非utf-8:XML文件对编码类型的支持比较广泛。然而有些工具不能正确某些类型编码的文件,而utf-8在Android应用中是一种被广泛支持的编码类型。使用utf-8对文件进行编码,可以防止在处理non-ASCII类型的字符时出现奇怪的问题。尤其是Gradle在合并XML类型的资源文件时,预先假定文件是使用utf-8进行编码的。

HardcodedText

硬编码文本属性:不要在layout文件或者代码中直接为文本控件设置text属性值。
– 在不同的位置多次使用相同的文本,如果需要修改文本则会引起多处修改。
– APP不能通过为文本提供一份翻译列表就可以适用新的语言的用户,而有很多工具可以快速的完成提供翻译列表的操作。
– 好的做法是,把text属性的文本值定义在string.xml文件中,方便国际化拓展。

SetTextI18n

TextView国际化:在调用TextView.setText给TextView赋值时,不能使用Number.toString例如图中的Integer.toString把数字转为字符串赋值,因为Number.toString不能正确处理分隔符和特定语言环境中的数字。

建议使用 String.format 指定合适的占位符进行赋值。

不能直接使用文本给TextView.setText,具体参照HardcodedText的说明。代码中可以使用@SuppressLint(“SetTextI18n”) 禁用lint的这项检查。

RelativeOverlap

RelativeOverlap是指RelativeLayout位于同一水平方向的两个Widget分别位于layout的左右两边,又都没有限制控件的长度,随着内容的增长,两个控件中间的距离不断缩小,最后会有部分重叠。所以,要控制一下边界。

Bidrrectional Text 双向文本

RtlEnabled

在API 17或更高版本上使用RTL属性需要在manifest文件的application标签中设置android:supportsRtl=”true”。如果已经开始在layout文件中加入RTL属性,但是没有完全使用RTL替代旧版的属性设置,可以设置android:supportsRtl=”false”规避lint的检查。

RtlCompat

API 17以后给文本控件提供了一个textAlignment属性来控制水平方向文字的对齐方式。但是,如果APP支持的版本包含小于 API 17 的版本,那么必须要设置gravity或者layout_gravity属性,因为老版本的系统会忽略textAlignment属性值。

RtlHardcoded

强制方向设置:在文本对齐和控件对齐中使用Gravity.LEFT/Gravity.RIGHT 的方式指定在左侧或者右侧对齐的情形,或在文字从右往左书写的国家或者地区造成困扰。使用Gravity.START/Gravity.END 就可以解决这个问题。同理,在layout文件中设置gravity/layout_gravity 属性时使用start/end 替代left/right

属性paddingLeft/paddingRight和属性layout_marginLeft/layout_marginRight也需要替换为paddingStart/paddingEndlayout_marginStart/layout_marginEnd

如果APP支持的最小API版本小于 API 17,那么需要同时提供left/right属性和start/end属性,因为低版本的系统会忽略start/end属性。

RtlSymmetry

margin|padding左右对称:如果对一个layout对象指定一边的内边距或外边距,那么应该对另一边指定同样大小的内边距或外边距。

参考文献

使用Android Studio Lint静态分析(三)

主要内容

  1. 在终端通过Gradle命令执行Lint检查。
  2. 在编译时进行lint检查。
  • Android Studio中Project目录下有两个文件gradlewgradlew.bat分别是在Mac/Linux系统和Windows系统上的Gradle命令工具。在Mac系统Project目录下执行./gradlew -p ${module_dir} lint命令,就可以执行对指定module按照Android Studio默认支持的扫描规则进行Lint检查。

  • Gradle提供了名为lintOptions的插件对Lint扫描进行个性化配置。

Gradle文件中的Lint配置

在${module_dir}/build.gradle文件android块内使用DSL对象lintOptions对Lint进行配置,配置项可以参考Android Plugin DSL Reference: LintOptions

android {
    lintOptions {
        // true--关闭lint报告的分析进度
        quiet true
        // true--错误发生后停止gradle构建
        abortOnError false
        // true--只报告error
        ignoreWarnings true
        // true--忽略有错误的文件的全/绝对路径(默认是true)
        //absolutePaths true
        // true--检查所有问题点,包含其他默认关闭项
        checkAllWarnings true
        // true--所有warning当做error
        warningsAsErrors true
        // 关闭指定问题检查
        disable 'TypographyFractions','TypographyQuotes'
        // 打开指定问题检查
        enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
        // 仅检查指定问题
        check 'NewApi', 'InlinedApi'
        // true--error输出文件不包含源码行号
        noLines true
        // true--显示错误的所有发生位置,不截取
        showAll true
        // 回退lint设置(默认规则)
        lintConfig file("default-lint.xml")
        // true--生成txt格式报告(默认false)
        textReport true
        // 重定向输出;可以是文件或'stdout'
        textOutput 'stdout'
        // true--生成XML格式报告
        xmlReport false
        // 指定xml报告文档(默认lint-results.xml)
        xmlOutput file("lint-report.xml")
        // true--生成HTML报告(带问题解释,源码位置,等)
        htmlReport true
        // html报告可选路径(构建器默认是lint-results.html )
        htmlOutput file("lint-report.html")
        //  true--所有正式版构建执行规则生成崩溃的lint检查,如果有崩溃问题将停止构建
        checkReleaseBuilds true
        // 在发布版本编译时检查(即使不包含lint目标),指定问题的规则生成崩溃
        fatal 'NewApi', 'InlineApi'
        // 指定问题的规则生成错误
        error 'Wakelock', 'TextViewEdits'
        // 指定问题的规则生成警告
        warning 'ResourceAsColor'
        // 忽略指定问题的规则(同关闭检查)
        ignore 'TypographyQuotes'
    }
}

部分配置项的说明

下面前三条所涉及的配置项,后面跟的参数都是issue_id,这些ID值跟lint.xml文件中使用的ID是同一个集合。

  1. 检查结果警报级别四个:fatal、error、warning、ignore。
  2. 检查结果输出方式三种:textReport、htmlReport、xmlReport,每一种都有开关并可以指定输出位置。后两种默认放在${module_dir}/build/reports/目录下。
  3. enable、disable是对某些检查做临时的开关设置。
  4. lintConfig指定一个lint文件,文件的内容格式与使用Android Studio Lint静态分析(二)中的lint.xml一样,文件名不必指定为lint.xml,文件路径传给lintConfig的file做参数。

配置本地编译时执行lint检查

配置Gradle脚本可实现编译Android工程时执行Lint检查:好处是既可以尽早发现问题,又可以有强制性;缺点是对编译速度有一定的影响。

编译Android工程执行的是assemble任务,让assemble依赖lint任务,即可在编译时执行Lint检查;同时配置LintOptions,发现Error级别问题时中断编译。

  • 在Application模块的gradle中加入配置
android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        def lintTask = tasks["lint${variant.name.capitalize()}"]
        output.assemble.dependsOn lintTask
    }
}
  • 在library模块的gradle中加入配置
android.libraryVariants.all { variant ->
    variant.outputs.each { output ->
        def lintTask = tasks["lint${variant.name.capitalize()}"]
        output.assemble.dependsOn lintTask
    }
}

参考文章

使用Android Studio Lint静态分析(二)

主要内容

在使用Android Studio提供的工具配置了扫描范围和检查项之后,通过lint.xml文件列出issue列表,对静态扫描进行补充。属于手动运行检查中的步骤,参照使用Android Studio Lint静态分析(一)

文件名约定:
1. 把Inspections对话框中配置生成的文件命名为inspection.xml。
2. lint.xml文件名是固定的。

❣️特别提醒:Inspections对话框中的配置,保存在工程目录下的 .idea/inspectionProfiles/inspection.xml 中。

<component name="InspectionProjectProfileManager">
  <profile version="1.0">
    <option name="myName" value="Default" />
    <inspection_tool class="AndroidLintUnusedResources" enabled="false" level="ERROR" enabled_by_default="false" />
  </profile>
</component>
  1. enabled:是否对此项进行检查
  2. level: 问题警告级别
  3. class: class的值与lint.xml文件中的issue.id是对应的, class=AndroidLint${issue.id}

使用lint.xml文件

lint.xml文件的文件名是固定的,放在项目的根目录。lint.xml文件内定义的规则,是对已设置的检查项做排除–禁用某些已经启用的检查项。

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- Disable the given check in this project -->
    <issue id="UnusedResources" severity="ignore"/>
    <issue id="Deprecated" severity="warning">
        <ignore regexp="singleLine"/>
        <ignore path="aaa.txt" />
    </issue>
</lint>

每一个issue标签指定一个规则。指定禁用规则的方法有两种:
1. 直接使用severity=”ignore”,指定禁用规则下所有情况;
2. 使用ignore标签,通过路径或者正则式,指定禁用规则的部分情况;

lint.xml支持的规则id集合

Android Studio支持的检查项是Android Lint的超集。

lint.xml支持的规则包含在Android->Lint内。如图二、图三所示,Android Lint支持的规则有🤙种类型共304个小项。

类型 数量 检查项
Accessibility 5 ClickableViewAccessibility、ContentDescription、GetContentDescriptionOverride、KeyboardInaccessibleWidget、LabelFor
Internationalization 9 ByteOrderMark、EnforceUTF8、HardcodedText、RelativeOverlap、RtlCompat、RtlEnabled、RtlHardcoded、RtlSymmetry、SetTextI18n
Performance 32 AnimatorKeep、DevModeObsolete、DisableBaselineAlignment、DrawAllocation、DuplicateDivider、FloatMath、HandlerLeak、InefficientWeight、LogConditional、MergeRootFrame、NestedWeights、ObsoleteLayoutParam、ObsoleteSdkInt、Overdraw、Recycle、StaticFieldLeak、TooDeepLayout、TooManyViews、UnpackedNativeCode、UnusedIds、UnusedResources、UseCompoundDrawables、UseOfBundledGooglePlayServices、UseSparseArrays、UseValueOf、UselessLeaf、UselessParent、VectorPath、ViewHolder、ViewTag、WakelockTimeout、WearableBindListener
Security 34 AddJavascriptInterface、JavascriptInterface、AllowAllHostnameVerifier、AllowBackup、AuthLeak、BadHostnameVerifier、EasterEgg、ExportedContentProvider、ExportedPreferenceActivity、ExportedReceiver、ExportedService、GetInstance、GrantAllUris、HardcodedDebugMode、HardwareIds、InvalidPermission、PackageManagerGetSignatures、PackagedPrivateKey、SSLCertificateSocketFactoryCreateSocket、SSLCertificateSocketFactoryGetInsecure、SecureRandom、SetJavaScriptEnabled、SetWorldReadable、SetWorldWritable、SignatureOrSystemPermissions、TrustAllX509TrustManager、UnprotectedSMSBroadcastReceiver、UnsafeDynamicallyLoadedCode、UnsafeNativeCodeLocation、UnsafeProtectedBroadcastReceiver、UseCheckPermission、UsingHttp、WorldReadableFiles、WorldWriteableFiles
Usability 14 AlwaysShowAction、AppLinkUrlError、BackButton、ButtonCase、ButtonOrder、ButtonStyle、GoogleAppIndexingApiWarning、GoogleAppIndexingWarning、MenuTitle、NegativeMargin、SelectableText、SmallSp、TextFields、ViewConstructor
Usability.Typography 6 AllCaps、TypographyDashes、TypographyEllipsis、TypographyFractions、TypographyOther、TypographyQuotes
Usability.Icons 18 ConvertToWebp、GifUsage、IconColors、IconDensities、IconDipSize、IconDuplicates、IconDuplicatesConfig、IconExpectedSize、IconExtension、IconLauncherShape、IconLocation、IconMissingDensityFolder、IconMixedNinePatch、IconNoDpi、IconXmlAndPng、MipmapIcons、MissingApplicationIcon、WebpUnsupported
Correctness 173 AaptCrash、AccidentalOctal、AdapterViewChildren、AppCompatCustomView、AppCompatMethod、AppCompatResource、AppIndexingService、AppLinksAutoVerifyError、AppLinksAutoVerifyWarning、ApplySharedPref、Assert、BatteryLife、CheckResult、CommitPrefEdits、CommitTransaction、CustomViewStyleable、CutPasteId、DefaultLocale、Deprecated、DeviceAdmin、DuplicateActivity、DuplicateDefinition、DuplicateIds、DuplicateIncludedIds、DuplicatePlatformClasses、DuplicateUsesFeature、EllipsizeMaxLines、ExifInterface、ExtraText、FindViewByIdCast、FontValidationError、FontValidationWarning、FullBackupContent、GetLocales、GradleCompatible、GradleDependency、GradleDeprecated、GradleDynamicVersion、GradleGetter、GradleIdeError、GradleOverrides、GradlePath、GradlePluginVersion、GridLayout、HalfFloat、HighAppVersionCode、IllegalResourceRef、ImpliedTouchscreenHardware、InOrMmUsage、IncludeLayoutParam、IncompatibleMediaBrowserServiceCompatVersion、InconsistentArrays、InconsistentLayout、InflateParams、InlinedApi、InnerclassSeparator、InstantApps、InvalidAnalyticsName、InvalidId、InvalidImeActionId、InvalidResourceFolder、InvalidUsesTagAttribute、InvalidVectorPath、InvalidWearFeatureAttribute、JobSchedulerService、LibraryCustomView、LocalSuppress、LocaleFolder、LogTagMismatch、LongLogTag、MangledCRLF、ManifestOrder、ManifestResource、MergeMarker、MinSdkTooLow、MissingBackupPin、MissingConstraints、MissingFirebaseInstanceTokenRefresh、MissingId、MissingIntentFilterForMediaSearch、MissingLeanbackLauncher、MissingLeanbackSupport、MissingMediaBrowserServiceIntentFilter、MissingOnPlayFromSearch、MissingPermission、MissingPrefix、MissingSuperCall、MissingTvBanner、MissingVersion、MockLocation、MultipleUsesSdk、NamespaceTypo、NestedScrolling、NetworkSecurityConfig、NewApi、NewerVersionAvailable、NfcTechWhitespace、NotInterpolated、NotSibling、ObjectAnimatorBinding、OldTargetApi、OnClick、Orientation、Override、OverrideAbstract、ParcelClassLoader、ParcelCreator、PendingBindings、PermissionImpliesUnsupportedHardware、PinSetExpiry、PrivateApi、PrivateResource、Proguard、ProguardSplit、PropertyEscape、ProtectedPermissions、PxUsage、Range、RecyclerView、ReferenceType、Registered、RequiredSize、ResAuto、ResourceAsColor、ResourceCycle、ResourceName、ResourceType、RestrictedApi、SQLiteString、ScrollViewCount、ScrollViewSize、SdCardPath、ServiceCast、ShiftFlags、ShortAlarm、ShowToast、SimpleDateFormat、SpUsage、StateListReachable、StopShip、StringShouldBeInt、SupportAnnotationUsage、Suspicious0dp、SuspiciousImport、SwitchIntDef、TestAppLink、TextViewEdits、UniqueConstants、UniquePermission、UnknownId、UnknownIdInLayout、UnlocalizedSms、UnsupportedTvHardware、UnusedAttribute、UseAlpha2、UsesMinSdkAttributes、ValidFragment、ValidRestrictions、VectorDrawableCompat、VectorRaster、VisibleForTests、WearStandaloneAppFlag、WebViewLayout、WifiManagerLeak、WifiManagerPotentialLeak、WrongCall、WrongCase、WrongConstant、WrongFolder、WrongRegion、WrongThread、WrongThreadInterprocedural、WrongViewCast
Correctness.Messages 11 ExtraTranslation、ImpliedQuantity、MissingQuantity、MissingTranslation、PluralsCandidate、StringEscaping、StringFormatCount、StringFormatInvalid、StringFormatMatches、UnusedQuantity、Typos
Correctness.Chrome OS 2 PermissionImpliesUnsupportedChromeOsHardware、UnsupportedChromeOsHardware
合计 304

结束语

规则项目太多而且没有说明,这样阅读比较困难对实际应用没有太多作用,希望后面会有时间整理。

使用Android Studio Lint静态分析(一)

主要内容

使用Android Studio提供的工具,配置Lint扫描范围和检查项。在使用 Lint 改进您的代码文档中,属于手动运行检查

程序静态分析

程序静态分析是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。

Java-Android代码常用的分析工具

  • Checkstyle
  • FindBugs
  • Soot
  • Lint

使用 Lint 改进您的代码

Lint是Android Studio提供的 代码扫描工具,自动化地对代码进行扫描,帮助改善代码结构的规范性和可维护性,提高代码质量。

Lint静态扫描的工作原理如下图。

Android Studio中Lint的操作步骤

Android Stuido Lint提供了 Specify Inspection Scope 面板,在面板中对代码扫描任务做个性化定义。

打开Specify Inspection Scope面板:菜单栏->Analyze->Inspect Code

设定扫描范围Custom Scope

  • 在面板中可以配置Lint扫描的范围,其中File选项会根据面板弹出前光标所在位置显示三种状态:
    • Files:在打开的文件上调出面板,可以对当前打开的文件进行扫描;
    • Directory:在Project面板中选中名录调出面板,对选定的目录进行扫描;
    • 隐藏:不提供此选项
  • 个性化扫描范围设定
    • 标题右侧的下拉框可以选择已有的自定义扫描配置
    • 下拉框右侧的“…”按钮,可以调起自定义扫描配置面板,可以任意的为一项配置添加和删除待扫描的源文件。左侧一列是自定义的配置列表,他们将出现在上一层的下拉框内备选。
    • 设定完成之后,生成对应的配置文件放在.idea/scopes目录下

检查项设置

Specify Inspection Scope 面板上最后一项Inspection profile,指定在代码扫描过程中对哪些问题进行检查。系统默认只提供一个Default可选项,我们可以点击下拉框右边的“…”按钮添加个性化的可选项。

  • 如下图,我们需要先把Default配置项Copy to Project,改个可爱的名字后就可以在新产生的配置项上进行编辑了。

第一步可以点击橡皮擦(图中被弹出框盖住了),把从Default中复制下来的选项全部清除,然后根据需要选择合适的集合即可。

其中Android、General、XML和Spelling是必选项,如果工程中只有Java代码,就只需要勾选Java一项就可以了,否则可能还需要选上kotlin、flutter、dart等相关的选项。

另外,上面说的那些选项,下面还有许多的子选项,并不是都必须的,可以根据需要选择部分子选项进行检查。

设定完成之后,生成对应的配置文件存放在.idea/inspectionProfiles目录下

  • 问题警告级别

选中检查项设置某一个选项,右侧会给出对应的描述和安全提醒级别。Android Studio Lint提供了六级安全提示,每种级别对应着不同的外观,重要程度不同,醒目的程度也不一样。

设置了所需的选项之后,点击右下角的“OK”,就可以使用这个设置集合了。

执行Lint静态扫描&查看结果

扫描范围和检查项设置完成之后,就可以执行静态扫描了。点击Specify Inspection Scope面板右下角的“OK”按钮,就开始执行扫描操作了。扫描完成之后,Android Studio底部会弹出Inspection Results面板, 可以根据安全级别或者检查项进行归类,到这里我们就可以根据扫描的结果对代码进行相应的整理和修改了。

参考文章

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

开头

编码时使用注解,可以提高编码效率、简化代码增强可读性等优点;使用注解还是代码静态扫描的一部分,促进代码规范。安卓注解使用介绍一文中介绍了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 使用这个库可以更方便地生成代码。

参考文章

安卓注解使用介绍

在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

参考文章

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)
}

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);
    }

APP代码结构杂谈

为什么讲软件结构

编码要有一个观念,就是你不是在为现在的自己编程。通常我们在看别人的代码时都牢骚满腹、百般嫌弃,但自己编码却也是天马行空、随心所欲。代码根本不能看,名称定义杂乱无章,改了一个Bug却出了更多的Bug。本来一个小问题,却发现需要修改一大堆的代码,处理非常复杂的代码逻辑。这就有了代码要容易阅读和便于维护的问题,当然软件引入代码结构的原因不会这么简单,但是这两个问题是最容易有切身体会的。

关于代码结构的几个概念

本文把一些概念列出,给出简单的说明,并不提供深入介绍,只是做一个引子,希望引发工程师注意编程规范和软件结构的意识。

  • 系统和子系统
    系统,泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。它的意思是“总体”“整体”或者“联盟”。
    子系统,也是由一群有关联的个体组成,通常是更大系统的一部分。

关联:系统是指由一群有关联的个体组成,没有关联的个体堆在一起不能成为系统。
规则:规则规定了系统内个体分工和协作的方式,而不是单个个体各自独立运作。
能力:系统能力和个体能力有本质差别,系统能力不是个体能力之和,而是产生了新的能力。

  • 架构和框架
    架构,是软件系统的顶层设计,规定了系统的各个部分的分解划分方法和各个部分的静态结构、逻辑结构以及交互关系。
    框架,通常是指实现某个标准或完成特定基本任务的“软件规范”;也可以指实现某个“软件规范”所要求之功能的软件产品。为构建解决方案提供良好的基础。

  • 模块和组件

    类别 目的 特点 接口 成果 架构和定位
    组件 重用、解耦 高重用、松耦合 无统一接口 基础库、基础组件 纵向分层
    模块 隔离、封装 高内聚、松耦合 统一接口 业务框架、业务模块 横向分块

组件:最初的目的是代码重用,功能相对单一或者独立。在整个系统的代码层次上位于最底层,被其他代码所依赖,所以说组件化是纵向分层。
模块:最初的目的是将同一类型的代码整合在一起,所以模块的功能相对复杂,但都同属于一个业务。不同模块之间也会存在依赖关系,但大部分都是业务性的互相跳转,从地位上来说它们都是平级的。

  • 设计模式和代码规范
    设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。这个概念是大多数教材里比较统一的一个定义。编程模式是一套经验性的代码设计方法,使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。
    代码规范,是实际编码过程中的变量命名,代码缩进,逻辑书写、注释说明等各方面细节的规范总称。良好统一的代码规范可以让软件代码更易懂,便于阅读和维护。所以工程代码中最好是有统一的代码规范,最好能形成文档以便后来的同事阅读并保持一致。因为涉及到细节,所以不同工程师的代码风格很有可能不一样,我的观点是,工程师不能局限于一种既定的自己喜欢的代码风格,要积极的去适应既有的代码规范和风格。

Android架构演变简介

  1. 架构设计的基本目的是要解决系统复杂度带来的问题。复杂度在APP上主要有可用性,扩展性,性能和规模几个方面。
  2. 架构设计的三原则:合适原则、简单原则和演进原则。

架构设计最基础的原则就是合适,选择合适于当前系统的架构而不是追求一步到位的追求业界领先,保证工程的快速迭代进度和代码实现逻辑简单化。在随后有需求的时候适度进行架构演进。
安卓APP架构既经历了Web App、原生APP、Hybrid APP、组件化&容器化的演进过程,又有MVC、MVP、MVVM到组件化的演进过程。

参考文献

  1. 组件化开发和模块化开发概念辨析
  2. 编程必备23种设计模式
  3. Android应用架构的前世今生