0%

事件分发,谁会响应点击事件?

一、背景

场景是在一个Activity中有一个ViewGroup(比如Framelayout),ViewGroup中有一个子View,手指在子view上按下然后滑动直到ViewGroup区域外松开手指,如果他们两个都设置了click监听,那么谁会响应到点击事件呢?为什么?

二、分析

先来回顾一下事件分发机制,首先需要了解三个非常重要的方法dispatchTouchEvent onInterceptTouchEvent onTouchEvent

dispatchTouchEvent:事件分发逻辑,返回结果依赖当前View的onTouchEvent和下级view的dispatchTouchEvent方法结果,表示是否消耗此事件

onInterceptTouchEvent:在dispathTouchEvent方法内调用表示是否拦截事件,如果当前view拦截了某个事件,那么在同一个事件序列中此方法不会被再次调用

onTouchEvent:用来处理点击事件表示是否消耗当前事件,如果不消耗,在同一个事件序列中,无法再次接收到事件

通过一段经典伪代码来展示他们之间的关系

public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if(onInterceptToucheEvent(event)){
consume = onTouchEvent(event);
}else{
consume = child.dispatchTouchEvent(event);
}
return consume;
}

实际代码细节还有很多,为了更好理解和解决实际开发中的问题,包括上面提到谁会响应点击事件诸如此类问题。下面先总结事件分发的规则在通过代码源码分析验证

2.1分发规则

分发规则其实是上面经典的伪代码的体现以及细节补充

  1. 分发规则是针对某一次的事件序列,事件序列是指手指按下触发到松开手指过程中产生的down、up和move事件
  2. 正常情况下一个事件序列只能被一个view消耗,一旦view开始处理事件消耗了down事件同一个事件序列中的后续事件都会交给它处理,特殊情况是代码调用onTouchEvent强制传递给其他View
  3. 如果一个view不消耗down事件,那么这个事件序列中的其他事件也不会在交给它处理并且事件会重新被传递给父View同时父View的onTouchEvent方法会被调用
  4. 如果一个VIew不消耗down事件以外的其他事件,那么这个事件会消失,不会传递给父View,父view的onTouchEvent方法不会被调用,最终这些事件会传递给Activity处理
  5. 一旦viewgroup拦截事件,同一个事件序列中后续事件都会交给它来处理,并且onInterceptTouchEvent不会被再次调用
  6. viewGourp默认不拦截事件,它的onInterceptTouchEvent默认返回false
  7. view没有onInterceptTouchEvent方法,事件传递给View后它的onTouchEvent方法会被调用
  8. view的onTouchEvent默认都会消耗事件返回true,除非view是不可点击的,clickable和longClickable同时为false。不受enable属性限制
  9. view的click能响应的条件是可以点击并且接收到了down和up事件
  10. 通过requestDisallowInterceptTouchEvent可以在子View中干预父View的事件分发过程但down事件除外

2.2源码解析

源码解析是对分发规则的验证,同时对分发规则中更多细节逻辑的补充说明

以Android 12系统源代码为例先来看ViewGroup的dispatchTouchEvent

/Users/hezd/Library/Android/sdk/platforms/android-31/android.jar!/android/view/ViewGroup.class

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 忽略其他无关代码...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// 忽略其他无关代码...
}

从上面这部分代码可以看出

2.3案例解析:谁会响应点击事件

三、代码验证

Hexo博客迁移教程(转载)

原文链接: https://wokron.github.io/posts/hexo-blog-migration/

一、前言

因为用了新的笔记本,为了继续更新自己的博客,我决定把原来那台笔记本上的博客资源迁移过来。不过呢,当然不能用u盘拷贝这种比较low的方法,最好还是把资源放到 github 上,这样不仅方便现在的迁移,更能防止数据丢失。

二、将博客资源推送到仓库

如果你使用 hexo 搭建了自己的博客,并且把博客放到了 github 上,那么很容易注意到使用 hexo 部署时并不是将本地的所有内容推送到了 github,实际推送的只是 ./public 路径下的文件。而现在我们要做的就是将博客的所有资源推送到仓库,不仅是用于网页的部分。

我们选择就在博客网站所在的仓库存储博客资源,为了做到这一点,首先要在本地克隆一个仓库

git clone https://github.com/<username>/<username>.github.io.git

随后我们新建一个分支用于存储博客资源。该分支与博客网站所使用的 master 分支无关,因此最好创建成一个“孤儿”分支。

git checkout --orphan <branch_name>

切换到该分支后,原本随着克隆拉取到本地的文件现在依旧存在,需要将这些文件删除

git rm -rf .

接着将位于本地的博客资源复制到该文件夹下。

cp -r <old_blog_dir>/* .

这里需要注意,如果你使用了 next 等主题,并且是通过克隆仓库的方式下载的,那么此时应该把主题对应的项目路径下的 .git 文件夹删除。

# take next theme as example
rm -r ./themes/next/.git

以上的工作都完成后,将这些复制到仓库中的博客资源文件添加并提交

git add .
git commit -m "commit info"

最后将本地分支推送到远程仓库的新分支中

git push --set-upstream origin <remote_branch_name>

本质上其实是在github上创建两个分支一个存储博客网站的资源文件,一个存储hexo项目资源

网站资源文件:是指使用hexo d命令发布时生成的网页资源文件,推送到指定分支分支配置在_config.yml的deploy节点中配置,比如我配置的是master分支

hexo项目资源:包含主题文件、项目配置等所有相关的hexo项目文件

三、迁移博客

接下来要将博客迁移到另一台设备上。首先当然要下载 git 并配置用户名和邮箱

sudo apt install git
git config --global user.name <username>
git config --global user.email <email>

之后克隆仓库并切换到博客资源所在的分支

git clone https://github.com/<username>/<username>.github.io.git
cd <username>.github.io
git checkout -b <branch_name> origin/<remote_branch_name>

接着下载 nodejs、npm 和 hexo

sudo apt install nodejs
sudo apt install npm
sudo npm install -g hexo-cli

最后下载项目中使用的其他包

npm install

四、在新设备上生成网页以及部署

hexo 的命令不用多说,可以用 hexo g 生成网页,并使用 hexo s 命令在本地运行。

最后使用 hexo d 将网页部署到 github 上。但是这时可能会出现如下的信息:

# omit...
Username for 'https://github.com': <username>
Password for 'https://wokron@github.com': <password>
remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
# omit...

这时就需要增加授权。可以使用 ssh 生成密钥。输入如下命令后按三次回车。

ssh-keygen -t rsa -C <email>

之后查看 ~/.ssh/id_rsa.pub 中的明文密钥

cat ~/.ssh/id_rsa.pub

进入 github,找到 Settings - SSH and GPG keys - SSH keys - New SSH Key,将该密钥粘贴并保存。之后输入如下命令查看密钥是否设置成功。

ssh git@github.com

此时可能会输出如下内容,这时只要输入 yes 并回车即可。

The authenticity of host '[ssh.github.com]:443 ([20.205.243.160]:443)' can't be established.
ED25519 key fingerprint is <fingerprint>.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])?

最终出现如下输出则表示成功

PTY allocation request failed on channel 0
Hi wokron! You've successfully authenticated, but GitHub does not provide shell access.
Connection to ssh.github.com closed.

最后打开博客的配置文件 _config.yml,将其中部署部分改为如下形式,使用 ssh 链接进行部署

deploy:
type: git
repo: git@github.com:<username>/<username>.github.io.git
branch: master

最后再次应用 hexo d 即可部署。

Http三次握手和四次挥手

了解Http的三次握手和四次挥手有助于对Http请求过程有更深刻的认识,同时也可以解决面试相关问题😁

三次握手

第一次握手:客户端想服务器发起请求连接的SYN报文,发送完毕后进入SYN_SENT状态

第二次握手:服务器收到客户端请求后,向客户端发送SYN ACK报文,进入SYN_RCVD状态

第三次握手:客户端收到服务器SYN ACK报文后,向服务器发送ACK报文,当服务器收到客户端ACK报文后客户端和服务器都会进入ESTABLISHED状态,TCP连接成功

三次握手示意图

四次挥手

当需要关闭http连接时需要经历四次挥手

第一次挥手:客户端向服务器发送FIN报文,进入FIN_WAIT状态

第二次挥手:服务器收到客户端FIN报文后向客户端发送ACK报文,进入CLOSE_WAIT状态

第三次挥手:当服务器没有数据可发送准备好关闭连接后向客户端发送FIN报文,进入LAST_ACK状态

第四次挥手:客户端收到服务端FIN报文后向服务端发送ACK报文进入TIME_WAIT状态,服务器收到客户端ACK报文后关闭连接,客户端在等待2MSL(最大报文生存时间)时间后也关闭连接

四次挥手示意图

到这里三次挥手和四次握手已经讲解完了

什么是动态代理?

动态是指运行时动态创建某些接口的实例对象,当调用接口方法时会被分发到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