社内Android勉強会でCustom Android Lintを実装する中で得た知見
はじめに
この記事は DeNA Advent Calendar 2020 の22日目の記事です。
こんにちは、品質管理部 SWETグループ田熊です。
DeNAでは、社内のAndroidエンジニアが事業部やチームの垣根を超えて知見を共有できる場として「Android.Tuesday」という社内勉強会を実施しています。
Android.Tuesdayは名前の通り毎週火曜日に開催され、DeNA内だけでなくMobility Technologiesのエンジニアにも参加いただいており、2社間のAndoridエンジニアの技術交流の場としても活用されています。
このAndroid.Tuesdayの場で、6週に渡ってAndroid Lint勉強会を開催し、ViewDataBindingのリークを検知するLintの実装を行いました。
この記事では社内Android Lint勉強会の様子と、Lintを実装する中で得た知見について紹介します。
Android Lintに入門する
参加しているAndroidエンジニアは全員Lint開発の知識がほぼない状態でした。
まずは、以下の記事の内容を実装してみることからはじめました。
kotlinでも検出できるCustom Lintを作成してみた (現在Android Studioの最新は4.1ですので、手元でLintのプロジェクトを作成される際は AndoridStudio4+用のサンプル を参考にするのがよいと思います。Lintを登録する箇所がアップデートされています。)
記事の通りにLintの実装を行い、動くことが確認できたところで、実際に自分たちが作成するCustom Lintの方針を考えました。
Lintの実装案として、次のような候補がでました。
- CoroutineのGlobalScopeを使用している箇所を検知して、ライフサイクルにあわせたScopeを設定するようにしたい
- MutableLiveDataで初期値が設定されておらず、実質Nullableだけど、型がNonNullになっている箇所を検知したい
- ConstraintLayoutで、非推奨になっているmatch_parentを使用している箇所を検知したい
- ViewDataBindingをフィールドに持っている場合に、onDestroyViewで解放していない箇所を検知してメモリリークを防ぎたい
この中で難易度としてちょうどいいのではないかと思われた「ViewDataBindingをフィールドに持っている場合に、onDestroyViewで解放していない箇所を検知してメモリリークを防ぎたい」を実装してみることにしました。
ViewDataBindingのリークを検知するLint
Android DevelopersのView Binding#fragments に書いてあるように、FragmentでViewDataBindingを保持している場合は、リークしないようにonDestroyViewで破棄することが望ましいです。
今回は次のようなコードを想定して、onDestroyViewのメソッドの中で_binding = null
がなかったらエラーとするLintを実装します。
class MyFragment : Fragment() {
private var _binding: MyBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = MyBinding.inflate(inflater, container, false)
return binding.root
}
//
override fun onDestroyView() {
super.onDestroyView()
_binding = null // <- これがなかったら怒る
}
}
Lintの実装方針
ViewDataBindingのリークを検知するLintは、次の方針で実装をすることにしました。
androidx.fragment.app.Fragment
を実装したクラスを探すandroidx.databinding.ViewDataBinding
型のサブタイプのフィールドを探す- なかったらフィールドにView Bindingをもっていないのでskip
onDestroyView
メソッドを探す- メソッドがなかったら破棄し忘れている可能性が高いのでエラー
- メソッドのボディに
_binding = null
があるか探す- なかったら破棄をし忘れている可能性が高いのでエラー
(ここからAndroid Lintの実装に触れていきますが、前述の kotlinでも検出できるCustom Lintを作成してみた で触れられている内容は省略していますので、そちらも合わせて参照いただければと思います。)
Android Lintは主に次のメソッドをオーバーライドすることで実装することができます。
class ViewBindingLeakDetector : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>>? {
// フィルターしたい要素を指定する
return listOf(..)
}
override fun createUastHandler(context: JavaContext): UElementHandler? {
return object : UElementHandler() {
// getApplicableUastTypesで返す型に対応した実装をoverrideする
}
}
}
今回は、まずFragmentを実装しているクラスをフィルターするところから始めます。 LintではASTをみてOKやNGを判定しますが、ASTはコードをツリー構造にしたものです。そのため、クラスがフィルターできれば、そこからクラスの持つメソッドやフィールドをたどれるという算段です。
class ViewBindingLeakDetector : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>>? {
// クラスの情報を持つNodeでフィルターする
return listOf(UClass::class.java)
}
override fun createUastHandler(context: JavaContext): UElementHandler? {
return object : UElementHandler() {
// UClassに対応したvisitClassをoverride
override fun visitClass(node: UClass) {
// クラスの情報を持つnodeが1つずつ流れてくる
// 方針をたてた検出のロジックを実装する
}
}
}
}
検出ロジックの実装
androidx.fragment.app.Fragment
を実装したクラスを探す
親クラスが何か?という情報はPsiClass#superClass
から見つけることができました。また、親クラスの完全修飾名は、PsiClass#superClass#qualifiedName
で取得することができます。
ただし、superClassは直接継承しているクラスしか取得できません。 そのため、AFragment -> BFragment -> androidx.fragment.app.Fragmentといった継承関係がある場合でも検出できるように、再帰的にsuperClassをチェックするようにしました。
// createUastHandlerからvisitClassを抜粋
override fun visitClass(node: UClass) {
val psiClass: PsiClass = node.javaPsi
// Fragmentの実装クラスか
val isFragment = matchSuperType(node.javaPsi, "androidx.fragment.app.Fragment")
}
// 任意のクラスを継承しているかを再帰的に確認する
fun matchSuperType(psiClass: PsiClass, qualifiedName: String): Boolean {
val superClass = psiClass.superClass
return when {
superClass == null -> {
// 継承元がないときはnullになる
// 全継承元を走査したことになるはずなのでfalseを返す
false
}
superClass.qualifiedName == qualifiedName -> {
true
}
else -> {
// superClassを引数にして再帰的に確認
matchSuperType(superClass, qualifiedName)
}
}
}
androidx.databinding.ViewDataBinding
型のサブタイプのフィールドを探す
Classが保持しているフィールドはPsiClass#allFields
から取得できました。戻り値はPsiField
です。
そこからPsiField#type.superTypes
とたどることで、フィールドの型とその親をたどることができました。
val viewBindingFields: PsiField =
psiClass.allFields.filter { field ->
// interfaceなど複数実装できるためsuperTypesは配列で返ってくる
field.type.superTypes.any { type ->
// ViewBindingはViewDataBindingを直接継承しているので再帰的にはみない
type.canonicalText == "androidx.databinding.ViewDataBinding"
}
}
onDestroyView
メソッドを探す
メソッドも、フィールドと同じ要領でPsiClass#allMethods
から取得できました。
val onDestroyView: PsiMethod = psiClass.allMethods.firstOrNull { method ->
method.name == "onDestroyView"
}
メソッドのボディに_binding = null
があるか探す
まずはメソッドのボディを取得します。
ひとつ上の項目で取得したPsiMethod
にはbody
プロパティがありますが、このプロパティを取得してもnullが返ってきてしまいます。
どうも実際の型はKtLightMethod
型で、KtLightMehod
型のbody
プロパティは常にnullを返すようです。ボディの中身はKtLightMethod#kotlinOrigin
から取得する必要がありました。
onDestroyView as KtLightMethod // PsiMethodからキャスト
val block = onDestroyView.kotlinOrigin?.children?.first {
// メソッドのボディはKtBlockExpressionで取得できる
// このあたりはPsiViewerを見ながらフィルターしました
it is KtBlockExpression
}
つぎにボディの中から、_binding = null
を探します。
_binding = null
という構文はPSI上ではBinaryExpressionに該当します。
左辺が_binding
、オペレーターが=
、右辺がnull
のBinaryExpressionがボディに含まれているかをチェックします。
// BinaryExpressionのみフィルター
val binaryExpression = block.children?.filterIsInstance<KtBinaryExpression>()
// _binding = nullを探す
// resultがnullの場合はエラーにする
val result = binaryExpression?.firstOrNull {
it.left?.text == "_binding" // 厳密には取得したViewDataBindingと比較する
&& it.operationToken.toString() == "EQ"
&& it.right?.text == "null"
これらの処理をつなげ、必要な箇所でエラーを返すように実装すればViewDataBindingのリークを検知するLintの完成です!
デバッグのTips
今回、LintはLint対象プロジェクト内のモジュールとして実装しました。 Lint実行対象のモジュールに、次の設定をすることで実現できます。
dependencies {
lintChecks project(':lint')
}
あわせて、Android StudioでGradleを実行するRun Configurationを作成し、Lintを実行するタスクを指定します。(例: lintDebug)
すると、通常の開発と同じようにAndroid Studio上でデバッグ実行をすることができ、Lintの処理の中をブレークポイントで止めることができます。
実装中はフィールドの中身を見たい場面が多々有り、何度もお世話になりました。
おわりに
社内勉強会でAndroid Lintに入門し、実際にAndroid Lintを実装する中で得た知見を紹介しました。
Android Lint何もわからない状態からはじまった勉強会ですが、参加者の知恵を出し合いながらなんとか動くLintを実装できたことを嬉しく思っています。
この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や facebook、はてなブックマークにてコメントをお願いします!
また DeNA 公式 Twitter アカウント @DeNAxTech では、 Blog記事だけでなく色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい。
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。