陈明亮的 Blog


  • 首页

  • 关于

  • 标签

  • 归档

ViewBinding的使用及原理

发表于 2022-12-02

ViewBinding的使用及原理

背景

1.findViewById

作为 Android 开发人员,没有不知道 findViewById 的。

想要操作视图中的控件,都需要先用到 findViewById 来找到这个控件,然后就可以对齐进行一系列的操作了(如:改变属性、设置监听、添加动画)。

缺点:当 xml 视图中控件较多时,代码中的 findViewById 也相应的越来越多。

2.ButterKnife

通过注解的方式来完成布局文件的绑定,从而避免写 findViewById 来提高效率。

示例:

@BindView(R.id.tv_content)
TextView tvContent;

缺点:(其实也不能算缺点,只是时代在发展)在如今组件化随处可见的时期,使用 ButterKnife 就有点麻烦了,在 library 中需要将 R 修改为 R2 才可,这时候当 module 在 application 和 library 切换时就比较麻烦了。

3.kotlin-android-extensions

作为很多人初始接触 Kotlin 的一个入门操作,最初都是因为使用 kotlin-android-extensions 简便而用上了 Kotlin。

使用确实很简单,在工程 build.gradle 文件中添加

dependencies{
    classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
}

然后在 module 的 build.gradle 中添加

apply plugin: 'kotlin-android-extensions'

这时候就直接可以使用 xml 视图中的控件 id 来操作控件了

tv_content.text = "Smaple"

缺点:
1.在模块之间不能通用,比如:业务模块中不能使用基础模块中的基础布局
2.由于不同的资源文件可以使用相同的控件 id 名称,会有一定的错误可能性。

由于上面的原因,所以有了 ViewBinding ,后面还有 DataBinding。
关于 DataBinding 可以参考笔者的这篇介绍:

ViewBinding 的使用

添加配置在 module 的 build.gradle

android{
    buildFeatures{
        viewBinding true
    }
}

之后在编译的时候就会为我们的所有 xml 文件生成对应的 Binding 类了,如:activity_main.xml 对应的 ActivityMainBinding。

使用也非常简单,Activity 如下:

class MainActivity : AppCompatActivity() {

    private lateinit var activityMainBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        activityMainBinding.tv_content = "Smaple"
    }
}

Fragment 如下:

class MainFragment: Fragment() {

    private var fragmentMainBinding: FragmentMainBinding? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        fragmentMainBinding = FragmentMainBinding.inflate(inflater, container, false)
        return super.onCreateView(inflater, container, savedInstanceState)
    }

    // 注意:在这里将 Binding 对象置为 null,防止内存泄漏(因为 Fragment 存在的时间大于视图)
    override fun onDestroyView() {
        super.onDestroyView()
        fragmentMainBinding = null
    }

}

如果某个布局不想使用 ViewBinding ,也就是不生成对应的 xxxBinding 类,那么只需要在 xml 的根 View 中添加 tools:viewBindingIgnore="true"。

ViewBinding 原理

原理追溯到本质上还是使用了 findViewById 来实现了绑定。

只不过这个过程是通过 gradle 插件来实现的,在编译的时候 gradle 插件会为每一个 xml 布局生成对应的 xxxBinding 类,当然如果某个 xml 中配置了 tools:viewBindingIgnore="true" 的话就会被插件跳过了。

ActivityMainBinding.java 源码如下:

public final class ActivityMainBinding implements ViewBinding {
  @NonNull
  private final RelativeLayout rootView;

  @NonNull
  public final TextView tvContent;

  private ActivityMainBinding(@NonNull RelativeLayout rootView, @NonNull TextView tvContent) {
    this.rootView = rootView;
    this.tvContent = tvContent;
  }

  @Override
  @NonNull
  public RelativeLayout getRoot() {
    return rootView;
  }

  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.activity_main, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static ActivityMainBinding bind(@NonNull View rootView) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    int id;
    missingId: {
      id = R.id.tv_content;
      TextView tvContent = ViewBindings.findChildViewById(rootView, id);
      if (tvContent == null) {
        break missingId;
      }

      return new ActivityMainBinding((RelativeLayout) rootView, tvContent);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

可以看到,显示通过 inflate 方法得到了 View 对象,然后通过 bind 方法中通过 findViewById 完成了控件的绑定。

而 getRoot 方法其实就是对根 View 的返回。

ViewBinding 封装

既然使用 ViewBinding 的套路是固定的,那么我们就可以将其抽取出来。

代码如下:

abstract class BaseActivity<T: ViewBinding>: AppCompatActivity() {

    lateinit var mViewBinding: T

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mViewBinding = getViewBinding()
        setContentView(mViewBinding.root)
    }

    abstract fun getViewBinding(): T

}

使用如下:

class SecondActivity: BaseActivity<ActivitySecondBinding>() {

    override fun getViewBinding(): ActivitySecondBinding {
        return ActivitySecondBinding.inflate(layoutInflater)
    }

    fun test(){
        mViewBinding.tv_content = "Sample"
    }

}

Kotlin 标准库函数

发表于 2022-08-10

Kotlin 标准库函数

apply

说明:可以当做配置函数,可以在其 lambda 中直接执行调用者的内部函数,方便对其进行配置,且会返回配置好的调用者。

示例代码:

import java.io.File
//对 file 设置可读写
fun main(){
    //不使用 apply
    val file = File("path")
    file.setReadable(true)
    file.setWritable(true)
    file.setExecutable(false)

    //使用 apply
    val file = File("path").apply{
        setReadable(true)
        setWritable(true)
        setExecutable(false)
    }
}

let

说明:let 可以将调用者传入其 lambda 中,使用 it 来代表调用者,且 let 会返回 lambda 的最后一行。

示例代码:

// 计算集合中第一个数的平方值
fun main(){
    // 不使用 let
    val firstElement = listOf(3,2,2).first()
    val result = firstElement * firstElement

    // 使用 let
    val resutl = listOf(3,2,1).let{
        it * it
    }
}

run

说明:run 函数基本上跟 apply 函数相似,只是在返回值上有所不同,run 不返回调用者,返回的是 lambda 的结果,也就是 true 或者 false。

示例代码:

import java.io.File
// 判断 file 中是否包含指定字符串
fun main(){
    //不使用 run
    val file = File("path")
    val fileContent = file.readText()
    val containsResult = fileContent.contains("great")

    //使用 run
    val file = File("path")
    val containsResult = file.run{
        readText().contains("great")
    }
}

扩展使用:

说明:run 函数也可以执行函数引用

示例代码:

fun main(){
    "Benjamin"
        .run(::isLong)
        .run(::showMessage)
        .run(::println)
}

fun isLong(name: String) = name.length >= 5

fun showMessage(isLong: Boolean){
    return if(isLong){
        "Name is too long."
    }else{
        "Please rename."
    }
}

with

说明:with 在功能上与 run 函数一样,区别只是在调用方式上,with 是将调用者作为自己的参数传递进来的。

示例代码:

run main(){
    val resutl = with("Benjamin"){
        lenth >= 10
    }
}

also

说明:also 的功能上同 let 一样,都是将调用者传递到 lambda 中,不同点是在返回值上,let 返回的是 lambda 的结果,而 also 的返回值是调用者。针对这个特点,also 就非常适合用在针对同一对象的连续调用上,比如:链式调用

示例代码:

import java.io.File

fun main(){
    var fileContent = ""
    File("path")
        .also{
            print(it.name)
        }.also{
            fileContent = it.readContent()
        }
    println(fileContent)
}

takeIf

说明:takeIf 从功能上说有点类似于 if,需要根据其 lambda 中的表达式结果来决定返回值是什么,当lambda 中的表达式结果是 true 时,返回的是调用者,反之返回的是 null。

示例代码:

import java.io.File

fun main(){
    val fileContent = File("path").takeIf{
        it.canRead() && it.canWrite()    
    }?.readContent()
}

takeUnless

说明:跟 takeIf 相反,只有 lambda 中的返回值是 false 时,才会返回调用者对象,否则返回 null 。

示例代码:

import java.io.File

fun main(){
    val fileContent = File("path").takeUnless{
        it.isHidden
    }?.readContent()
}

Charles 代理抓包

发表于 2021-07-01

Charles 代理抓包

HTTP

1.在电脑上安装 Charles

2.打开 Charles --> Proxy --> Proxy Settings 可以看到 Prot 在里面填入你想自定的端口号,如:8888、9999 ……

3.1 在手机上 设置 --> 无线网络 --> 链接的 Wifi --> Wifi 的设置 --> 代理设置 --> 手动 --> 填入电脑的 IP 和上一步设置的端口号 Port --> 保存

查看电脑 IP ,Mac:终端输入 ifconfig | grep inet 

3.2 在 TV 上 通过 adb 链接 TV 之后,执行命令:adb shell settings put global http_proxy ip_address:port,将 ip_address 替换为电脑的 IP 和 第二部自定义的端口号即可

//TV 上删除代理的命令,依次执行这三条
adb shell settings delete global http_proxy
adb shell settings delete global global_http_proxy_host
adb shell settings delete global global_http_proxy_port

4.接下来在设备上打开你想抓包的 App,它发生网络请求的时候,就可以在电脑上的 Charles 中看到了。

HTTPS

1.在电脑上安装 Charles

2.电脑安装根证书 打开 Charles --> Help --> SSL Proxying --> Install Charles Root Certificate --> 双击进入证书详情,选择信任

3.1手机安装根证书 Charles --> Help --> Install Charles Root Certificate on a Mobile Device or Remote Browser --> 里面会提供一个下载的网址 chls.pro/ssl --> 在浏览器中打开这个网址,下载证书 --> 安装到手机上 --> 手机的 wifi 上设置代理到电脑上 --> 再在电脑的 Charles --> Proxy --> SSL Proxying Settings --> 打勾Enable SSL Proxying --> add Host:*,Port:443 --> ok

3.2 在 TV 上安装证书

具体的在设备上安装证书的细节,可参考此文章Android TV 、Phone 安装 CA 证书

Android TV 、Phone 安装 CA 证书

发表于 2021-06-30

Android TV、Phone 安装 CA 证书

TV

直接按照如下的命令执行

1.openssl x509 -inform PEM -subject_hash_old -in <your certificate>.pem | head -n 1

将 <your certificate>.pem 替换成你的证书全路径,执行之后会得到一个 hash 值,如:711d79cc

2.将你本地的 xxx.pem 证书 重命名为 上一步得到的 hash.0,如:711d79cc.0

3.通过 adb 链接设备

4.adb push hash.0 /system/etc/security/cacerts/hash.0

5.adb shell chmod 644 /system/etc/security/cacerts/hash.0

ps : /system/etc/security/cacerts TV 设备的证书存储在此目录下,最终目的就是将需要安装的证书,push 到这里即代表已安装。

Phone

手机上相对比较简单了,因为都有设置可以操作

1.将需要的证书下载到手机的 sd 卡

2.操作路径:设置 --> 安全 --> 设备管理与凭证 --> 从存储盘安装 --> 输入密码 --> 确定。不同品牌手机的操作路径名称可能不能,这时候可以利用手机设置的搜索功能,直接搜索凭证、存储盘安装等关键字就能找到了。

Android-动画

发表于 2021-04-12

Android-动画

Android-动画 源文件 思维导图

链接: https://pan.baidu.com/s/1T4LB5H6qwfgQtpAE86skEA 密码: rkae

自定义 View --> 绘制

发表于 2021-04-12

自定义 View –> 绘制

自定义View-绘制 源文件 链接: https://pan.baidu.com/s/1t3KMEaZ8cOHUhbPQwF8wsw 密码: t4kp

Resource.getSystem()与 context.getResource()

发表于 2020-08-06

Android Library 打 jar aar 混淆

发表于 2020-07-23

Android Library 打 jar 包

正确的 library 项目的 build.gradle 中有这样的代码标志

apply plugin: 'com.android.library'

接下来就是打 Jar 包了

在需要打 Jar 的 library 项目中的 build.gradle 文件最下面也就是 dependencies {} 下面

看一个完整的示例

apply plugin: 'com.android.library'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"


    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}


task makeJar(type: Copy) {
    delete 'build/libs/' //删除已经存在的jar包
    from('build/intermediates/packaged-classes/release/') //已经帮我们打好的 jar 的路径
    into('build/libs/') //要将jar包保存的目录
    include('classes.jar')//设置过滤,只打包classes文件
    rename('classes.jar', 'mylibrary.jar')//重命名,mylibrary.jar 根据自己的需求设置
}
makeJar.dependsOn(build)

接下来就是执行命令

mac : 在当前项目的根目录下执行

./gradlew makeJar

然后等待编译成功之后,就可以在前面指定的目录中看到我们想要的 jar 包了。

Jar 包混淆

有时候我们打出来的 Jar 想将代码混淆,该怎么做呢?

这个时候我们还是再 library 项目的 build.gradle 的文件中做文章。

看改动如下

apply plugin: 'com.android.library'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"


    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

task makeJar(type: proguard.gradle.ProGuardTask, dependsOn: "build") {
    injars 'build/intermediates/packaged-classes/release/classes.jar'
    outjars 'build/outputs/modify.jar'
    configuration 'proguard-rules.pro'
}

通过对比,不难发现,其实改动的只是 task makeJar()方法
其实只是在原有的 jar 上加了混淆,也就是引入了这样的代码

configuration 'proguard-rules.pro'

也就是我们的混淆文件

至于proguard-rules.pro里面的内容就需要我们自己根据项目中的内容去写了

1.首先可以把AndroidStudio 自带的标准内容写入,如下:

#表示混淆时不使用大小写混合类名
-dontusemixedcaseclassnames
#表示不跳过library中的非public的类
-dontskipnonpubliclibraryclasses
#打印混淆的详细信息
-verbose

# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
-dontoptimize
##表示不进行校验,这个校验作用 在java平台上的
-dontpreverify
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.

-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
    native <methods>;
}

# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}

-keepclassmembers class **.R$* {
    public static <fields>;
}

# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version.  We know about them, and they are safe.
-dontwarn android.support.**

# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep

-keep @android.support.annotation.Keep class * {*;}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}

2.一般都需要的如下(其中 xxx 替换成你自己本地的相关路径)

#引入依赖包rt.jar(jdk路径)
-libraryjars /xxx/jdk1.8.0_144.jdk/Contents/Home/jre/lib/rt.jar
#引入依赖包android.jar(android SDK路径)
-libraryjars /xxx/Android/sdk/platforms/android-29/android.jar

#忽略警告
-ignorewarnings
#保证是独立的jar,没有任何项目引用,如果不写就会认为我们所有的代码是无用的,从而把所有的代码压缩掉,导出一个空的jar
-dontshrink
#保护泛型
-keepattributes Signature

3.最后加上我们不希望被混淆的类或者方法(根据自己的实际需求添加,比如入口什么的)

# 不希望被混淆的类
-keep class com.cml.lib.M
# 不希望被混淆的方法
-keep class com.cml.lib.M {
    void a(...);
}    

好了,接下来再次再项目的根目录执行命令

./gradlew makeJar

然后等待编译完成,看看输出出来的 Jar 就已经被混淆过了。

打 aar 包

aar 相对于 Jar 来说可以携带更多的东西,比如资源文件、Manifest文件等。

aar 的生成就简单多了,用 AndroidStudio 就可以帮我们完成。

点 AndroidStudio 右上角有一个 Gradle(竖这放的)–> 项目名 –> library 名–> Tasks –> build –> assemble , 双击最后的这个 assemble 就完成了

aar 的输出路径在 library 的 build/outputs/aar/ 下面 会发现有debug和 release 两个版本,取自己需要的就行了,如果不知道需要什么,那么取 release 版本即可

但是我们查看发现这个 aar 并没有被混淆,那么怎么混淆呢?

其实跟我们 apk 的混淆一样了,有一个开关需要打开在 library 的 build.gradle 中

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}

对,就是这个minifyEnabled设置为 true 即可打开混淆了。

但同样不要忘记proguard-rules.pro文件里面的内容的编写哦,具体的编写方法,上面已经说过了,跟前面的 Jar 混淆方式一样。

jsBridge

发表于 2019-09-02

apk 和 H5 混合

1.汇总

jsbridge 是 java 和 js 之间的一个桥梁

最主要的就是 两端的通信

安全

效率

覆盖率

解决的问题

1.漏洞问题

2.代码繁琐问题

3.android 接收 js 的回调只支持 android4.4 以上的版本

2.本质问题

阅读全文 »

Kotlin 中那些便捷的写法

发表于 2019-09-02

这篇讨论的仅仅是记录一些常用的便捷写法,方便查询!

方法的重载

Kotlin 中并没有重载这一说,但 Java 中是存在的,因此为了兼容 Java 所以有了@JvmOverloads

举例:

Java 代码

void add(int a){}
void add(int a,int b){}
void add(int a,int b,int c){}
void add(int a,int b,int c,int d){}
void add(int a,int b,int c,int d,int e){}

Kotlin 代码

@JvmOverloads
fun add(a: Int,b:Int=0,c:Int=0,d:Int=0,e:Int=0) {}

Kotlin 中的一个方法就可以替代 Java 中的五个方法,关键就在于@JvmOverloads

同样的最常用的就是在 Android 的自定义 View 中

Java 代码

public class CustomView extends View {

    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

Kotlin 代码

class CustomView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0) : View(context,attrs,defStyleAttr)

是不是方便的!

@JvmOverloads作用:给有默认值的参数生成重载方法。

设置监听

监听的设置本质上是内部的方法被调用.在 Kotlin 中省去了接口的使用,因为 Kotlin 中方法(或者说含函数)是可以被传递的,同时也存在函数类型.

Java 方式的监听代码

var listener: OnClickListener? = null

interface OnClickListener{
    fun onClick(view:View?)
}

fun setOnClickListener(listener: OnClickListener){
    this.listener = listener
}

//调用时
listener?.onClick(view)

Kotlin 方式的监听代码

var onClick: ((View) -> Unit)? = null

fun setOnClickListener(onClick: (View) -> Unit){
    this.onClick = onClick
}

//调用时
onClick?.invoke(view)
12…4
cml

cml

32 日志
10 标签
© 2022 cml
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4