0%

什么是APT

APT是annotation processing Tools的缩写意思是注解处理工具,常用于生成模版代码来简化代码

如何自定义注解处理器

下面参考butterknife的BindView来自定义注解处理器,butterknife是jakewharton大神开发的用于android简化view初始化的库,原理是使用apt来解析注解生成辅助类自动绑定view。

工程创建

通常加上主工程需要创建4个模块

app:主工程

bindview-annotations:需要被注解处理器处理的注解聚合模块

bindview-compiler:注解处理器模块

bindview:为上层应用提供api的模块,例如ViewBinder.bind(this)

工程目录如下

工程模块创建完以后接下来做什么,重新回归我们的目的是什么,我现在要做的是实现一个view自动绑定的功能,消除findviewById模版代码,实现原理是声明绑定注解通过注解处理器获取注解以及绑定的viewId生成view绑定辅助类,辅助类中包含view绑定逻辑。在需要绑定的Actiivty种调用辅助类的绑定方法进行view自动绑定。下面就需要将原理变为代码实现

声明注解

在当前工程中创建一个java or kotlin library,声明需要注解处理器处理的注解,包括注解使用的目标类型这里是字段Field,以及注解保留策略这里的场景需要字节码文件中保留,可以在编译时获取

声明注解处理器

新建一个java or kotlin library命名为bindview-compiler并创建自定义注解处理器BindViewProcesor

根据前面工程创建部分最后面原理部分介绍,接下来需要在注解处理器中编写注解处理器逻辑

  1. 配置注解处理器支持的java源代码版本

    @Override
    public SourceVersion getSupportedSourceVersion() {
    return processingEnvironment.getSourceVersion();
    }
  2. 配置可处理的注解集合

    @Override
    public Set<String> getSupportedAnnotationTypes() {
    HashSet<String> supportAnnotationTypes = new HashSet<>();
    supportAnnotationTypes.add(BindView.class.getCanonicalName());
    return supportAnnotationTypes;
    }
  3. 初始化工具类

    /**
    *
    * @param processingEnv 提供了一些工具集合
    */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    this.processingEnvironment = processingEnv;
    filer = processingEnv.getFiler();
    }
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    environment = roundEnvironment;
    Messager messager = processingEnvironment.getMessager();
    }

    需要获取的工具类有:

    ProcessingEnvironment:提供了一些工具集合,这里用于获取日志工具,注解处理器支持的java源代码版本以及文件生成器

    RoundEnvironment:用于查询支持注解处理器处理的注解

  4. 编写注解处理器逻辑

    由于注解处理器需要引用我们声明的自定义注解,因此注解处理器模块要依赖注解模块,在注解处理器的build.gradle.kts文件中添加依赖关系

    dependencies{
    implementation(project(":bindview-annotations"))
    }

    按以下步骤实现注解处理器逻辑(在process方法中)

    • 定义注解处理器

      public class BindViewProcessor extends AbstractProcessor {
      RoundEnvironment environment;
      ProcessingEnvironment processingEnvironment;
      private Filer filer;

      /**
      * 注解处理器初始化
      * @param processingEnv 提供了一些工具集合
      */
      @Override
      public synchronized void init(ProcessingEnvironment processingEnv) {
      super.init(processingEnv);
      this.processingEnvironment = processingEnv;
      filer = processingEnv.getFiler();
      }

      @Override
      public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
      // 在这里编写注解处理器逻辑
      }
      }
    • 注册注解处理器

      注解处理器发挥作用还需要能够被编译器能够检测到,需要注册注解处理器

      在bindview-compiler模块的src/main/resources目录下创建META-INF/services目录并在这个目录下创建javax.annotation.processing.Processor文件,在文件中声明注解处理器完全限定名称(全路径名)

    • 扫描所有的添加了BindView注解的Element

      Set<? extends Element> elementsAnnotated = roundEnvironment.getElementsAnnotatedWith(BindView.class);
    • 按照Activity进行分组

      HashMap<String, List<VariableElement>> elementMap =  new HashMap<>();
      for (Element element : elementsAnnotated) {
      if (element.getKind() == ElementKind.FIELD) {
      messager.printMessage(Diagnostic.Kind.NOTE, "element:" + element);
      }
      // 因为BindView是添加在字段上面的所以这里可以强转
      VariableElement variableElement = (VariableElement)element;

      // 获取注解字段外层结构最接近的Element其实也就是Activity类型的Element强转为TypeElement
      // 目的是获取Activity的全路径名称
      TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement();
      String activityName = enclosingElement.getQualifiedName().toString();

      // 控制台打印日志
      messager.printMessage(Diagnostic.Kind.NOTE,"activityName:"+activityName);

      // 按照Activity进行分组
      List<VariableElement> elements = elementMap.get(activityName);
      if(elements == null){
      elements = new ArrayList<>();
      elementMap.put(activityName,elements);
      }
      elements.add(variableElement);
      }
    • 生成辅助类

      1. 借助Filer按照指定规则生成java源代码类文件,我们现在要生产view绑定辅助类比如MainActivityBinding,代码逻辑如下

        // 生成xxxBinding辅助类代码
        // 生成规则是Activity名称为前缀拼接Binding后缀生成辅助类
        // 辅助类中生成绑定view的代码
        for(Map.Entry<String,List<VariableElement>> entry:elementMap.entrySet()){
        String activityName = entry.getKey();
        List<VariableElement> variableList = entry.getValue();
        TypeElement activityElement = (TypeElement) variableList.get(0).getEnclosingElement();
        String packageName = processingEnv.getElementUtils().getPackageOf(activityElement).toString();
        messager.printMessage(Diagnostic.Kind.NOTE,"packageName:"+packageName);
        messager.printMessage(Diagnostic.Kind.NOTE,"activityName:"+activityName);
        String simpleName = activityElement.getSimpleName().toString();
        String className = simpleName+ConstantValues.SUFFIX_BINDING_CLASS;
        messager.printMessage(Diagnostic.Kind.NOTE,"binding class:"+className);
        Writer writer = null;
        try {
        JavaFileObject javaFileObject = filer.createSourceFile(className);
        writer = javaFileObject.openWriter();
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("package "+packageName+";\n");
        stringBuffer.append("import com.hezd.bindview.IBinder;\n");
        stringBuffer.append("public class "+className+" implements IBinder<"+activityName+">{\n");
        stringBuffer.append("\tpublic void bind("+activityName+" target) {\n");
        for(VariableElement variableElement:variableList){
        String variableName = variableElement.getSimpleName().toString();
        int resourceId = variableElement.getAnnotation(BindView.class).value();
        stringBuffer.append("\t\ttarget."+variableName+"=target.findViewById("+resourceId+");\n");
        }
        stringBuffer.append("\t}\n");
        stringBuffer.append("}\n");
        writer.write(stringBuffer.toString());
        } catch (IOException e) {
        throw new RuntimeException(e);
        }finally {
        if(writer!=null){
        try {
        writer.close();
        } catch (IOException e) {
        throw new RuntimeException(e);
        }
        }
        }
        }

        这里为了简化反射调用让辅助类实现IBinder接口这样只需要一次反射拿到IBinder实例调用bind方法即可

        public interface IBinder<T> {
        void bind(T target);
        }

提供外部调用接口的SDK

生成辅助类后需要让上层应用调用辅助类对外接口,例如这里需要让app调用到MainActivityBinding类中的bind方法进行view的绑定,还需要创建一个提供对外接口调用的模块

这里创建一个andorid library命名为bindview

因为用到bindview-compiler模块中的常量类,所以需要bindview模块需要建立依赖关系,另外还需要对外提供注解BindView访问需要添加bindview-annotations的传递依赖

dependencies {
//...
implementation(project(":bindview-compiler"))
api(project(":bindview-annotations"))
//...
}

当前模块提供辅助类访问对象ViewBinder

public class ViewBinder {
public static void bind(Object target){
String targetName = target.getClass().getCanonicalName();
String className = targetName+ ConstantValues.SUFFIX_BINDING_CLASS;
try {
Class bindgClass = Class.forName(className);
if(IBinder.class.isAssignableFrom(bindgClass)){
IBinder binder = (IBinder)bindgClass.newInstance();
binder.bind(target);
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}

使用注解处理器

  1. 引入注解处理器

    在app主工程模块引入注解处理器

    dependencies {
    //...
    annotationProcessor(project(":bindview-compiler"))
    //...
    }
  2. 引入sdk模块

    dependencies {
    //...
    implementation(project(":bindview"))
    annotationProcessor(project(":bindview-compiler"))
    //...
    }

3.在Activity中view字段添加BindView注解

public class MainActivity extends AppCompatActivity {

@BindView(R.id.textview)
public TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBinder.bind(this);

getWindow().getDecorView().postDelayed(() -> {
textView.setText("bind success");
}, 2000);

}
}

使用BindView注解view字段,然后调用ViewBinder.bind(this)实现了view自动绑定,消除了findviewbyid模版代码

4.clean然后rebuild后可以在build目录中找到注解处理器生成的MainActivityBinding

5.运行后可看到页面先显示默认Helloworld文本,两秒后被修改为bind success

写在最后

这篇文章介绍了什么是注解处理器,以及用注解处理器简单实现view自动绑定,另外还有一些可以优化的点比如可以使用google的auto-service免除手动注册注解处理器,使用javapoet库简化java代码生成,从而提高开发效率。

代码仓库地址:codelab-android-apt

什么是动态代理?

动态是指运行时动态创建某些接口的实例对象,当调用接口方法时会被分发到InvocationHandler的invoke方法中,代理是指动态创建的接口实例对象可以对某个对象做代理,进行hook等操作

应用场景

  • 为接口添加统一逻辑处理
  • 结合反射动态替换接口实现
  • 面向切面编程

为接口做统一逻辑处理

使用动态代理生成实现某个接口代理对象然后在invocationHandler中做通用逻辑处理,例如Retrofit中巧妙利用动态代理实现网络请求

简化版代码示例

fun main(args: Array<String>) {
// 代理接口
proxyInterface()
}

private fun proxyInterface() {
val httpClient = HttpClient.Builder().build()
httpClient.create(Api::class.java).request()
}

class HttpClient {
class Builder {
fun build(): HttpClient {
return HttpClient()
}
}

@Suppress("UNCHECKED_CAST")
fun <T> create(server: Class<T>): T {
return Proxy.newProxyInstance(server.classLoader, arrayOf(server)) { proxy, method, args ->
println("proxy ${method.name}")
} as T
}
}

/**
* 请求接口
*/
interface Api {
fun request()
}

打印结果

proxy request

Process finished with exit code 0

结合反射动态替换接口实现

通过反射替换静态成员对象为动态代理对象(实现了同样的接口),从而实现hook,对静态成员操作一次操作终身受用😭

在Android系统低版本(好像是8.0以前),AMS获取是通过ActivityManagerNative的getDefault方法获取的,getDefault返回的是ActivityManagerNative静态成员gDefault,某些插件化框架通过动态代理生IActivityManager代理对象并反射替换gDefault,实现对ActivityManagerNative的hook

简化版示例代码

fun main(args: Array<String>) {
// 代理hook
proxyForHook()
}

private fun proxyForHook() {

val proxy = Proxy.newProxyInstance(
IActivityManager::class.java.classLoader, arrayOf(IActivityManager::class.java)
) { proxy, method, args ->
println("proxy activitymanager")
}

val mInstanceField = Class.forName("Singleton").getDeclaredField("mInstance")
mInstanceField.isAccessible = true
val gDefaultField = Class.forName("ActivityManagerNative").getDeclaredField("gDefault")
gDefaultField.isAccessible = true
val gDefault = gDefaultField.get(null)
mInstanceField.set(gDefault, proxy)
ActivityManagerNative.getDefault()?.startActivity()
}


class ActivityManagerNative {
companion object {
@JvmStatic
private val gDefault = object : Singleton<IActivityManager>() {
override fun create(): IActivityManager {
return ActivityManager()
}

}

fun getDefault(): IActivityManager? = gDefault.get()
}


}

interface IActivityManager {
fun startActivity()
}

class ActivityManager : IActivityManager {

override fun startActivity() {
println("start Activity")
}
}


abstract class Singleton<T> {
private var mInstance: T? = null
protected abstract fun create(): T
fun get(): T? {
synchronized(this) {
if (mInstance == null) {
mInstance = create()
}
return mInstance
}
}
}

打印结果

proxy activitymanager

Process finished with exit code 0

面向切面编程

另外动态代理还可以用来做Aop面向切面编程,例如需要在被代理对象执行某个操作前后添加其他操作例如打印日志

示例代码

fun main(args: Array<String>) {
// 代理AOP
proxyForAop()
}

fun proxyForAop() {
val person = Person()
val personProxyHandler = PersonProxyHandler(person)
val personProxy = Proxy.newProxyInstance(
person.javaClass.classLoader,
arrayOf(IAnimal::class.java),
personProxyHandler
) as IAnimal
personProxy.eat()
}

class PersonProxyHandler(private val person: Person) : InvocationHandler {

override fun invoke(proxy: Any?, method: Method, args: Array<out Any>?): Any {
println("doSomething before eat")
method.invoke(person)
println("doSomething after eat")
return Unit
}

}

class Person : IAnimal {
override fun eat() {
println("start eat")
}

}

interface IAnimal {
fun eat()
}

打印结果

doSomething before eat
start eat
doSomething after eat

Process finished with exit code 0

Flow

Flow是基于流的编程模型。更简单的说Flow代表数据流。

数据流有三个重要角色

producer:数据生产者

imediatly:中介,可以修改数据流

comsumer:数据消费者

案例分析

pancho(潘乔)需要到湖边打水,以前他都需要每次提桶到湖边打完水返回驻地。可能有时候到湖边发现湖水已经干涸。后来他发现他可以架设管道从湖边到驻地,这样他只需要打开水龙头就可以收集到水流了,并且可以观察到湖水是否已经干涸。

这里的水流可以对应到应用中的数据流

响应式编程

像上面案例分析中这种观察者可以对被观察对象变化做出响应的系统称为响应式编程

下面以一个定时器案例进行数据流创建、转换、收集

创建数据流

使用flow构建器方法创建

class DataFlowViewModel : ViewModel() {
val countFlow = flow{
var count = 0
while (true) {
println("emit data:$count")
emit(count)
delay(1000)
count++
}
}
}

修改数据流

使用中间操作符例如map进行转化,

class DataFlowViewModel : ViewModel() {
val countFlow = flow{
var count = 0
while (true) {
println("emit data:$count")
emit(count)
delay(1000)
count++
}
}.map {
it.toString()
}
}

从数据流中收集

通过终端操作符collect进行数据收集

class DataFlowActivity : AppCompatActivity() {
private val binding by lazy {
ActivityDataFlowBinding.inflate(layoutInflater)
}

private val viewModel : DataFlowViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val root = binding.root
setContentView(root)

startCollect()
}

private fun startCollect() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
viewModel.countFlow.collect {
println("collect data:$it")
binding.textview.text = it.toString()
}
}
}
}
}

collect会触发数据发送以及数据流修改,并最终收集到数据

注:协程中将这种按需创建并在被观察时才会发送数据的的数据流叫做冷流

在Android视图中收集数据流

通常我们是在Android视图中收集数据流,这时我们要关注两个方面,后台运行时不应该浪费资源和配置变更

安全收集

安全收集的目的是页面置于后台的时候不应该浪费资源,这里所说的浪费资源是指countFlow属于冷流当置于后台时如果收集任务不取消后台生产者还可以持续的发送数据浪费内存资源。Google官方的建议是使用repeatOnlifecycle例如

lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
viewModel.countFlow.collect {
println("collect data:$it")
binding.textview.text = it.toString()
}
}
}

当页面进入后台时收集任务会被取消

另外还提到有launchWhenStarted,页面进入后台后会挂起收集任务而不是取消,可能会造成一定内存开销

lifecycleScope.launchWhenStarted {
repeatOnLifecycle(Lifecycle.State.STARTED){
viewModel.countFlow.collect {
println("collect data:$it")
binding.textview.text = it.toString()
}
}
}

配置变更

当配置变更时例如屏幕旋转会触发Activity重启,而ViewModel却会保留,使用冷流会导致重新收集数据创建新的数据流,造成资源浪费。有没有一种机制可以保证不管Activity重启多少次都不需要重新创建数据流呢。可以使用热流StateFlow,热流发送数据的活动独立于观察者,当重新触发收集数据时并不会创建新的数据流,并保留了重建前的数据。

但是新的问题来了,对于上面短时间内重启情况下热流对我们很有用,但是如果按下Home键应用回到后台后,虽然收集任务被取消,但是由于热流的特性,后台可能还在继续发送数据,造成资源浪费。

对于这个问题可以使用超时机制,利用stateIn运算符将冷流转为热流同时设置收集取消后数据发送等待多久自动取消

val countFlow = flow{
var count = 0
while (true) {
println("emit data:$count")
emit(count)
delay(1000)
count++
}
}.map {
it.toString()
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000),0)

如果短时间内重启不会重建数据流,而长时间置于后台触发超时机制会自动取消数据发送,大功告成perfect😁

stateFlow

stateFlow是热数据流,负责更新stateFlow的类是生产者,收集数据的类是消费者,数据收集不会触发生产者数据发送。

可使用stateFlow替换项目中的LiveData,但请注意StateFlow热流特性,页面销毁并不会停止数据发送

基本用法:

class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {

// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LatestNewsUiState> = _uiState

init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// Update View with the latest favorite news
// Writes to the value property of MutableStateFlow,
// adding a new element to the flow and updating all
// of its collectors
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}

// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(val exception: Throwable): LatestNewsUiState()
}

GC回收机制

简介

GC是Garbage Collection垃圾回收的英文释义

什么是垃圾呢,垃圾是指内存中无用的对象。如何判断那些对象是无用对象呢,可以通过可达性分析来判断。

那什么是可达性分析呢,可达性分析是指将内存中的对象引用关系用一张图来表示,GC Root对象作为顶点,对象引用关系连接线走过的路径叫做引用链,如果一个对象的引用链对Gc Root是不可达的那就认为这个对象是无用对象。

Gc Root

  • 虚拟机栈局部变量表中引用的对象
  • 方法区中静态引用的对象
  • 存活的线程对象
  • Native方法中JNI引用的对象

何时回收

不同虚拟机会有不同的gc回收机制,一般来说以下两种情况都会触发垃圾回收:

1.Allocation Failure:当内存过低导致内存分配时,会触发一次gc

2.System.gc():应用层主动调用gc api

回收算法

gc回收算法常见的有三种分别是标记清除、复制清除、标记压缩

标记清除

标记清除分为两个阶段

标记阶段:

从顶点的Gc Root对象开始遍历引用链,将垃圾对象标记为黑色,否则标记为灰色

清除阶段:

标记完成后,将垃圾对象全部清除

优点:操作简单

缺点:可能会产生内存碎片

复制清除

将内存分为AB两块,每次只使用一块内存,将A内存设置为可用内存,gc回收时遍历Gc Root引用链进行标记,

标记完成后,将存活对象复制到B内存块中,将A内存中的对象全部清除,将B内存设置为可用内存

优点:运行高效,不会产生内存碎片

缺点:可用内存会变为原来的一半

标记压缩

标记压缩也分为两个阶段

标记阶段:

遍历Gc Root引用链,标记无用对象为黑色否则为灰色

压缩阶段:

将存活对象压缩到内存的另一端,清除垃圾对象

优点:不会产生内存碎片,不会造成内存浪费

缺点:对象需要移动,一定程度上造成性能损失

分代回收策略

虚拟机根据对象存活周期不同将堆内存分为几块,一般来说会分为新生代和老年代。新生代又分为eden、survivior0、survivor2三个区,新生代多次回收后还存活的对象会进入老年代。

Hotspot除了新生代和老年代还有一个永久代

新生代

新建的对象优先分配到新生代

新生代对象会优先分配到Eden区,当Eden区满了以后,会将存活的对象复制到s0区并清空Eden区,当Eden区第二次满的时候,会将Eden区和s0去的存活对象复制到s1区并清空Eden区和s0区,s0和s1多次切换后(默认15次)还有存活的对象会进入老年代。

从新生代的回收过程可以看到使用的是复制清除算法

老年代

新生代多次回收后还存活的对象会进入老年代,新生代分配大对象时如果内存不足也会进入老年代

JVM内存区域

Java程序在运行过程中会在内存中生成对象、常量等各种数据,JVM为了方便管理这些数据对内存中的数据进行了区域划分。

JVM规范中定义的内存区域有:

程序计数器、虚拟机栈、本地方方法栈、方法区、堆。

程序计数器

在Java多线程执行中cpu会为多线程分配执行时间片段,当某个线程被挂起时需要记录当前线程执行的位置,以便在恢复时继续执行。这就是程序计数器的作用

JVM规范中没有对程序计数器定义OutOfMemory之类的异常

程序计数器是线程私有的,生命周期同线程一样

如果线程执行的是Java方法代码实际上记录的是当前执行的class字节码指令的地址

如果是是native方法记录会为空(undefined)

虚拟机栈

虚拟机栈是用来描述Java方法执行的内存模型,每个Java方法在执行时JVM都会在虚拟机栈中创建一个栈帧

栈帧包含:

局部常量表、操作数栈、动态链接、返回地址

JVM规范定义虚拟机栈的异常有:OutofMemoryError、StackOverFlowError

虚拟机栈是线程私有的,生命周期同线程一样

局部常量表:保存方法局部变量

操作数栈:对常量表中的数据进行算术运算

动态链接:方法调用时的方法符号转化为方法所在内存中地址的直接引用

返回地址:方法执行完后要返回的代码位置

本地方法栈

本地方方法栈是针对于native方法的栈与虚拟机栈类似

本地方法栈是线程私有,生命周期同线程一样

方法区

方法区存储的是被JVM加载过的类信息、常量、静态变量、及时编译器编译过的代码和数据

方法区数据是线程共享

方法区是JVM规范,永久代和元空间是方法区的具体实现

堆存放的数据是Java程序运行时产生的对象实例

JVM规范定义的堆的异常有:OutOfMemoryError

堆数据是线程共享的

背景

​ 项目拉取一些依赖库需要连指定仓库并且还要连vpn,而有时候vpn又不稳定,导致开发变的异常繁琐。因此想要把本地缓存过的依赖库迁移到私有仓库。

​ 需要用到migrate-local-repo-tool.jar工具,它可以直接将本地maven仓库中的依赖库直接上传到远程仓库(或者符合本地仓库库目录结构样式的其他目录)。

​ 但是比如三方库我们本地仓库中并没有,只存在gradle的缓存目录中,这时就需要将gradle目录中的三方库格式化输出到一个目录中。

需要解决的问题:

  • 依赖库缓存在哪里
  • 如何格式化输出
  • 如何上传

依赖库缓存

​ 依赖库的缓存目录在用户目录下的.gradle/caches目录,但是要找到库的目录就比较困难因为目录太多了,可以用反向查找的方式,在Android Studio中的依赖库列表External Libraries中找到依赖库jar包右键在文件夹中打开就可以找到了

格式化输出

​ 为什么需要格式化输出,因为我们可以发现gradle缓存目录中的库目录结构是groupId作为目录名,而本地.m2仓库中目录结构是groupId分割以后每一段都是一级目录,举例:

com.tyt.goods:dialog:1.0.0

gradle缓存路径:
/Users/hezd/.gradle/caches/modules-2/files-2.1/com.tyt.goods/dialog/1.0.0/f86e207d698686ec58e48613c1f4f2c6e6e67283/dialog-1.0.0.aar
-->
com.tyt.goods/dialog/1.0.0/f86e207d698686ec58e48613c1f4f2c6e6e67283/dialog-1.0.0.aar

本地仓库路径:
/Users/hezd/.m2/repository/com/tyt/goods/dialog/1.0.0/dialog-1.0.0.aar
-->
com/tyt/goods/dialog/1.0.0/dialog-1.0.0.aar

因此gradle缓存目录中库目录结构需要格式化为本地仓库中的格式

为了避免误操作污染gradle缓存目录中的依赖库,可以将需要格式化输出的库拷贝到其他目录在做操作

我是用了python脚本来处理格式化输出的

脚本获取地址:migrate-local-repo 

仓库上传

格式化输出到目录后就要开始上传,例如格式化输出到temp/release目录下,migrate-local-repo上传命令

java -jar migrate-local-repo-tool.jar -cd "temp/release" -t "你的仓库地址" -u 你的用户名 -p 你的密码

注:上面命令中的参数需要根据自己环境做调整,不能无脑复制:smile:

如果terminal出现部署成功的日志说明上传成功,如

Sucessful Deployments:
com.mb.gradle.plugin:app-opt:jar:1.1.1-REMOVE-MODULE-INFO-3-SNAPSHOT
com.mb.gradle.plugin:app-opt:jar:sources:1.1.1-REMOVE-MODULE-INFO-3-SNAPSHOT
com.mb.gradle.plugin:app-opt:pom:1.1.1-REMOVE-MODULE-INFO-3-SNAPSHOT

至此,将gradle缓存中依赖库迁移至远程私有仓库完成了!

二分查找

  • 什么是二分查找
  • 实现原理
什么是二分查找

二分查找是从一个有序数组中找到目标元素(通常是找下标)的过程

实现原理

先来看两张图
图例1
image
图例2
image

nums:有序数组
fromIndex:起始指针,跟toIndex一起确定查找范围
toIndex:结束指针,结合fromIndex确定查找范围
mid:中间指针,指向查找范围中间位置的指针
midValue:中间指针指向元素的值

  • 查找过程:

    首先指定查找范围是整个数组,然后找到中间指针指向的元素(midVal)的值跟目标值(如target)进行比对,如果midVal小于target,fromIndex指针从mid位置向右移动一位(图例2),反之toIndex从mid位置向左移动一位,重新确定查找范围,继续重复上述操作直到找到target等于midVal或者fromIndex>toIndex查找结束

代码实现:
public static int binarySearch(int[] nums, int target) {
int fromIndex = 0;
int toIndex = nums.length - 1;
while (fromIndex <= toIndex) {
int mid = (fromIndex + toIndex) >> 1;
int midVal = nums[mid];
if (midVal < target) {
fromIndex = mid + 1;
} else if (midVal > target) {
toIndex = mid - 1;
} else {
return mid;
}
}
return -1;
}

题目

给定一个单链表的头结点pHead,长度为n,反转该链表后,返回新链表的表头

解题思路

利用栈的先进后出的特点,先将单链表元素依次入栈后在依次弹出生成新的单链表

步骤:

①创建一个栈数据结构

②遍历单链表并将元素push入栈

③将栈顶第一个元素pop出栈并作为新链表表头

④将栈中的元素依次pop出栈并生成新的单链表

注意:

栈底的最后一个元素作为单链表的尾节点,并且需要将next指向置空,否则会造成无限循环,因为这个元素来自于反转前的单链表表头next指向另一个节点的

示例代码
/**
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/

public class Solution{
public static void reverseListNode(ListNode head){
// 创建栈
Stack<ListNode> stack = new Stack<>();
// 遍历单链表并入栈
while(head!=null){
stack.push(head);
head = head.next;
}
if(stack.isEmpty()){
return null;
}
// 栈顶元素出栈并作为反转后单链表头节点
ListNode node = stack.pop();
ListNode result = node;
while(!stack.isEmpty()){
ListNode tempNode = stack.pop();
node.next = tempNode;
node = node.next;
}
// 链表尾部节点next指向置空,防止死循环
node.next = null;
return result;
}
}

题目链接:

https://www.nowcoder.com/exam/oj?page=1&tab=%E7%AE%97%E6%B3%95%E7%AF%87&topicId=295

单链表查找倒数第K个元素

给定一个单链表查找倒数第k个元素,例如单链表有节点1->2->3->4->5查找倒数第2个元素

解题思路

使用快慢指针fast和slow,fast先移动K步,slow指针指向头节点head,然后同时开始移动快慢指针指针直到fast指针移动到链表尾部时slow指针指向的节点就是倒数第k个节点元素

public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}

class Solution{
public static ListNode getKthFromEnd(ListNode head,int k){
ListNode slow = head;
ListNode fast = head;
// 先将快指针移动K步
while(k>0){
fast = fast.next;
k--;
}
// 同时移动快慢指针直到快指针移动到链表尾部
while(fast!=null){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}

参考:

https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/

什么是泛型?

泛型是编译器的类型安全检查机制,让更多类型错误暴露在编译期,提高程序稳定性。

最常见的应用场景是集合

List list = ArrayList();
list.add("a");
list.add(1);

如果希望list只接受字符串类型,使用泛型很容易做到这一点

List<String> strList = ArrayList<>();
strList.add("a");
strList.add(1);//编译失败,只能是String类型

泛型类型

泛型类型是从类型上参数化的类或接口.

泛型类型定义

class name<T1,T2,…Tn>{}

举例:

class Box<T>{}

名词解释:

类型形参:T就是泛型类Box的类型形参

类型实参:泛型调用时替换T的值,例如Box strBox ;String就是类型实参

调用和实例化

泛型类型调用

要在代码中引用泛型类型要使用泛型类型调用,将泛型形参T替换为具体的值

例如上面声明的泛型类Box的类型调用如下:

Box<String> strBox;

泛型类型调用也叫作参数化类型parameterized type

实例化

泛型类型实例化与普通类类似,使用new关键字,在类名和括号之间使用尖括号标明泛型类型

Box<String> strBox = new Box<>();// 由于类型推断可以省略<>里面的泛型类型

泛型方法

泛型方法是指在声明方法时引入泛型类型

例如:

public <T>void setValue(T t){}

在返回值前声明泛型类型,返回或参数使用泛型类型

有界类型形参

有时我们希望对泛型类型形参边界进行限定,使用extends关键字限定上边界

public Box<T extends Number>{}

如果我们希望Box泛型类型实参既可以是Integer又可以是Double可以使用上面方法进行限定

注意:泛型类型边界只能限定上边界,无限定下边界方法。不可与通配符的限定下边界混淆

泛型类型继承

泛型类型实参也存在继承关系,只要类型兼容可以将一个类型分配给另一种类型

List<Number> list = new ArrayList<>();
list.add(new Integer(1));
list.add(new Double(1));

但是请注意下面代码

public void setList(List<Number> list){}
setList(new ArrayList<Integer>());//compile error

对于fun方法参数如果传递ArrayList会编译报错,虽然Integer是Number的子类型,但是ArrayList并不是List的子类型,要解决这个问题可以使用下面讲到的通配符来解决

public void setList(List<? extends Number> list){}
setList(new ArrayList<Integer>())

通配符

在泛型代码中使用问号(?)表示未知类型,泛型类型调用时作为类型实参。

// ArrayList的addAll方法使用通配符
public boolean addAll(Collection<? extends E> c) {}

注意:因为通配符作为泛型类型调用的实参,所以不能用于泛型类或接口定义(泛型类定义中的类型变量是形参)

上界通配符

上界通配符? extends限定上边界,限定类型是指定类型或它的子类型

比如下面方法参数是一个集合既想接收Integer类型又想接收Double类型可以使用上界通配符

public void addNumbers(List<? extends Number> list){}
addNumbers(new ArrayList<Integer>());
addNumbers(new ArrayList<Double>());

注意以下代码

List<? extends Number> numbers = new ArrayList<>();
numbers.add(new Integer(1));//compile error
numbers.add(new Double(1));// compile error

使用上界通配符在对集合进行写操作时编译报错,这是因为虽然虽然限定了类型为Number及子类,但是实际类型可能是Integer也可能是Double编译器无法判断唯一性,所以不允许进行写操作。

无界通配符

无界通配符使用?表示未知类型来指定泛型类型实参

无界通配符应用场景:

① 使用Object类提供的功能实现的方法

② 泛型代码不依赖于泛型形参

​ 例如Class<?>,因为Class中的大多数方法不依赖于T

例如以下方法

public void println(List<Object> list){
for(Object item : list){
System.out.println(item);
}
}

如果想要打印List或List是不可以的,这时候可以使用无界通配符打印所有类型的List

public void println(List<?> list){
for(Object item:list){
System.out.println(item);
}
}
下界通配符

使用? super来限定下边界,限定类型为指定类型或它的父类型

例如以下方法

public void addNumbers(List<? super Integer> list){}
addNumbers(new ArrayList<Integer>());
addNumbers(new ArrayList<Number>());

使用? super Integer限定接收的类型为Integer或它的父类型

注意以下代码

List<? super Integer> list = new ArrayList<>();
Number num = 1;
list.add(new Integer(1));
list.add(num);//compile error
list.add(new Object());//compile error

向限定下界为Integer的list中添加Number类型时编译报错,因为虽然限定了下界为Integer或它的父类,但是这个类型可能是Number或者Object编译器无法判断,而类型又必须唯一,为了类型安全只能写入Integer或它的子类型。

如果想要既可以写入Integer又可以写入Number,限定下边界需要改为Number

List<? super Number> list = new ArrayList<>();
Number num = 1;
list.add(new Integer(1));
list.add(num);

在来看以下代码

List<? super Number> list = new ArrayList<>();
list.add(new Integer(1));
Number item = list.get(0);//compile error
Object item = list.get(0);//compile success

使用下界通配符后list集合获取元素只能用Object接收,这是因为元素了类型可能是符合限定条件的所有类型,可能是Integer也可能是Double,编译器无法确定所以不能用Number来接收。相当于失去了读的属性

可以看出限定上边界以后泛型类型相当于只有读权限,限定下边界相当于只写权限,关于通配符使用原则可以看下面通配符使用指南。

通配符使用指南

我们在通配符使用过程中可能会有困惑,什么时候使用上界通配符,什么时候使用下界通配符,官方建议是根据”in”和”out”原则,也有人叫作生产消费者原则

例如

copy(src,dest);//src变量作为"in"变量,dest作为"out"变量

“in”变量使用上界通配符,使用extends关键字(生产者)

“out”变量使用下界通配符,使用super关键字(消费者)

可以使用Object的方法来访问”in”变量使用无界通配符

变量既要读数据又要写数据时不要使用通配符

类型擦除

泛型类型是jdk1.5引入的,为了与以前版本兼容,编译器在编译阶段会将泛型类型擦除,同时对泛型类型做类型转换。

对于无界类型形参替换为Object

对于有界类型形参替换为边界类型

类型擦除示例

// 无界类型
// 擦除前
Class Box<T>{
private T t;
public void setValue(T t){
this.t = t;
}
public T getValue(){
return t;
}
}
// 擦除后
Class Box {
private Object t;
public void setValue(Object t){
this.t = t;
}
public void getValue(){
return t;
}
}
// 有界类型
// 擦除前
Class Box<T extends Integer>{
private T t;
public void setValue(T t){
this.t = t;
}
public T getValue(){
return t;
}
}
// 擦除后
Class Box{
private Integer t;
public void setValue(Integer t){
this.t = t;
}
public Integer getValue(){
return t;
}
}

来看一到面试题

List<String> list1 = new ArrayList<>():
List<Integer> list2 = new ArrayList<>():
System.out.print(list1.getClass()==list2.getClass());

最终输出结果是true,因为两个list在类型擦除后Class类型都是List.class

泛型的限制

无法用基本类型实例化泛型类型
List<int> list = new ArrayList<int>();//compile error
List<Integer> list = new ArrayList<Integer>();//compile success
无法创建类型形参的实例
public static <E> append(List<E> list){
E e = new E();//compile error
list.add(e);
}

// 解决办法是提供Class通过反射创建
public static <E>append(List<E> list,Class<E> cls){
E e = cls.newInstance();
list.add(e);
}
无法声明为类型形参的静态字段
public class Box<T>{
private static T t;//compile error
}

因为静态字段是对象共享的如果允许这样操作,每个对象可以在创建实例时可以指定不同类型,但是它不能同时是多种类型

不能使用参数化类型进行类型转化或instanceof
public void check(List<E> list){
if(list instanceof ArrayList<Integer>)//compile error
}

因为泛型类型会被擦除

无法重载类型参数擦除后原始类型相同的方法
public void set(List<String> strList);
public void set(List<Integer> intList);

类型擦除后原始类型都为Object因此无法重载

无法创建参数化类型的数组
无法创建,捕获或抛出参数化类型的对象

参考

https://docs.oracle.com/javase/tutorial/java/generics/index.html

https://pingfangx.github.io/java-tutorials/java/generics/types.html

https://blog.csdn.net/briblue/article/details/76736356