乱码三千 – 分享实用IT技术

乱码三千 – 码出一个新世界


  • 首页

  • 归档

  • 搜索

将代码发布至jcenter步骤

发表于 2019-12-24

Step 1

在项目的build.gradle中buildscript中添加如下脚本,使用最新版本号,在https://github.com/novoda/bintray-release查看:

     

buildscript {
repositories {
    jcenter()
}
dependencies {
    //classpath 'com.novoda:bintray-release:<latest-version>'
    classpath 'com.novoda:bintray-release:0.8.1'
}
}

Step 2

在库模块(需要上传的模块)的build.gradle中添加:

apply plugin: 'com.novoda.bintray-release'

publish {
    userOrg = '组织ID' //bintray账户下某个组织id 个人用户填写账户名
    groupId = 'com.insworks.plugin' //maven仓库下库的包名,一般为模块包名
    artifactId = 'framework-plugin' //项目名称
    publishVersion = '1.0.2' //版本号
    desc = '组件化架构插件' //项目介绍,可以不写
    website = '' //项目主页,可以不写
}
tasks.withType(Javadoc) {//防止编码问题
    options.addStringOption('Xdoclint:none', '-quiet')
    options.addStringOption('encoding', 'UTF-8')
    options.addStringOption('charSet', 'UTF-8')
}

Step 3

在bintray中创建仓库并获取仓库key。Bintray网站点击右上角用户名–>Edit Your Profile -> API Key –>输入密码–>Submit–>Show。

Step 4

使用指令上传代码:

在Android Studio的Terminal面板中执行下面命令,其中BINTRAY_USERNAME替换为你的binatray用户名,BINTRAY_KEY替换为上面获取的API Key,-PdryRun=false会上传到仓库中,如果为true,只会执行gradle任务,但不会上传。替换完成后回车执行

示例
gradlew clean build bintrayUpload -PbintrayUser=BINTRAY_USERNAME -PbintrayKey=BINTRAY_KEY -PdryRun=false

我的
gradlew clean build bintrayUpload -PbintrayUser=songjianziana -PbintrayKey=5e7f0ea95e85af4cceca20cb109fb50ad7cba6bc -PdryRun=false

或者使用gradle快捷上传, 需要先编译项目(切记)

Step 5

Android Studio中配置https://bintray.com仓库地址

maven { url 'https://dl.bintray.com/songjianzaina/insoan' }

和然后在app模块build.gradle中添加依赖:

1
implementation 'com.xxx.xxx.xx.:1.0.0'

Step 6

添加到JCenter。点击“Add to JCenter”,填写项目介绍,点击Send发送,然后等待审核,审核成功之后会发送站内通知

上传成功之后将maven { url 'https://dl.bintray.com/songjianzaina/insoan' }替换成jcenter即可

本帖附件

点击下载

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Android源码下载

发表于 2019-12-24

Android源码下载支持的系统目前只有Ubuntu和Mac OS两种操作系统, 本次以Ubuntu系统为例.

官方网站: http://source.android.com/source/downloading.html

下载单独项目的源码:https://github.com/android

  1. 下载Git(版本控制工具). 调出命令行: ctrl + alt + T

    1
    sudo apt-get install git   (下载地址在源中,"系统设置/软件和更新",路径:/etc/apt/sources.list)
  2. 安装curl(上传和下载数据的工具).

    1
    sudo apt-get install curl
  3. 安装repo(一个基于git的版本库管理工具, 这里用于自动批量下载android整个项目.).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 创建目录
    mkdir ~/bin

    // 下载repo脚本到本地bin文件夹下
    curl http://android.git.kernel.org/repo >~/bin/repo (官网:curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo)
    // 如果上面下载失败, 采用下面这种方式
    curl "http://php.webtutor.pl/en/wp-content/uploads/2011/09/repo" >~/bin/repo

    // 给所有用户追加可执行的权限
    chmod a+x ~/bin/repo

    // 临时把repo添加到环境变量中, 方便后面执行.
    // 注意: 每次重启ubuntu之后此环境变量失效, 重新配置就可以了.
    export PATH=~/bin:$PATH
  4. 创建文件夹, 用于存放下载的Android源码.

    1
    2
    3
    4
    5
    6
    7
    // 创建目录
    mkdir ~/android_source

    // 修改权限
    chmod 777 ~/android_source

    cd ~/android_source
  5. 初始化库.

    1
    2
    3
    4
    5
    6
    7
    8
    // 需要先配置git的用户信息
    git config --global user.email "dai_zhenliang@163.com"
    git config --global user.name "haha"

    repo init -u https://android.googlesource.com/platform/manifest -b android-2.3_r1

    // 如果上面初始化失败, 用下面的代码
    repo init -u git://codeaurora.org/platform/manifest.git -b gingerbread
    当屏幕出现以下信息表示成功初始化
    1
    repo initialized in /home/haha/android_source
  6. 开始同步下载.

    1
    repo sync

    注意: 下载过程中, 因为网络问题, 可能会中断下载. 当中断下载时, 继续使用repo sync命令继续下载.

本帖附件

点击下载

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Android源码编译

发表于 2019-12-24

在编译源码之前需要做一些准备操作, 详细步骤如下:

1. 安装JDK, google官方要求编译2.3源码需要JDK1.6.

  • 1). 下载JDK1.6, 下载地址:http://www.oracle.com/technetwork/java/javasebusiness/downloads/java-archive-downloads-javase6-419409.html、http://download.oracle.com/otn/java/jdk/6u45-b06/jdk-6u45-linux-x64.bin

  • 2). 创建目录.

    1
    sudo mkdir /usr/java
  • 3). 在文件系统中右击上面的创建的文件夹,选择“以管理员权限打开”,然后把下载好的jdk-6u45-linux-x64.bin拖动到/usr/java目录中

  • 4). 添加可执行权限.

    1
    sudo chmod 755 /usr/java/jdk-6u45-linux-x64.bin
  • 5). 解压.

    1
    2
    cd /usr/java
    sudo ./jdk-6u45-linux-x64.bin
  • 6). 配置环境变量.

    1
    2
    3
    export JAVA_HOME=/usr/java/jdk1.6.0_45
    export PATH=$PATH:$JAVA_HOME/bin
    export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
  • 7). 验证是否成功.

    1
    java -version

2. 安装其他编译时依赖的软件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sudo apt-get install gnupg
sudo apt-get install flex
sudo apt-get install bison
sudo apt-get install gperf
sudo apt-get install zip
sudo apt-get install curl
sudo apt-get install build-essential
sudo apt-get install libesd0-dev
sudo apt-get install libwxgtk2.8-dev
sudo apt-get install libsdl-dev
sudo apt-get install lsb-core
sudo apt-get install lib32readline-gplv2-dev
sudo apt-get install g++-multilib
sudo apt-get install lib32z1-dev
sudo apt-get install libswitch-perl

安装注意事项:

3. 开始编译, 在源码的目录下, 执行一下命令:

1
2
3
cd ~/android_source
make clean(如果之前有编译过,则需要调用这个命令)
make

注意: ubuntu自带的源中速度比较慢, 有些软件找不到, 所以需要修改为国内的源, 修改源步骤如下:

  • 1). 备份ubuntu自带的源.

    1
    sudo cp /etc/apt/sources.list /etc/apt/sources.list.old
  • 2). 修改源文件.

    1
    sudo gedit /etc/apt/sources.list
  • 3). 这时会弹出一个文本编辑框, 先删除所有内容, 然后把以下内容拷贝进去, 并保存.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    deb http://mirrors.163.com/ubuntu/ trusty main restricted universe multiverse
    deb http://mirrors.163.com/ubuntu/ trusty-security main restricted universe multiverse
    deb http://mirrors.163.com/ubuntu/ trusty-updates main restricted universe multiverse
    deb http://mirrors.163.com/ubuntu/ trusty-proposed main restricted universe multiverse
    deb http://mirrors.163.com/ubuntu/ trusty-backports main restricted universe multiverse
    deb-src http://mirrors.163.com/ubuntu/ trusty main restricted universe multiverse
    deb-src http://mirrors.163.com/ubuntu/ trusty-security main restricted universe multiverse
    deb-src http://mirrors.163.com/ubuntu/ trusty-updates main restricted universe multiverse
    deb-src http://mirrors.163.com/ubuntu/ trusty-proposed main restricted universe multiverse
    deb-src http://mirrors.163.com/ubuntu/ trusty-backports main restricted universe multiverse

    deb http://mirrors.sohu.com/ubuntu/ trusty main restricted universe multiverse
    deb http://mirrors.sohu.com/ubuntu/ trusty-security main restricted universe multiverse
    deb http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted universe multiverse
    deb http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted universe multiverse
    deb http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted universe multiverse
    deb-src http://mirrors.sohu.com/ubuntu/ trusty main restricted universe multiverse
    deb-src http://mirrors.sohu.com/ubuntu/ trusty-security main restricted universe multiverse
    deb-src http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted universe multiverse
    deb-src http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted universe multiverse
    deb-src http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted universe multiverse
  • 4). 保存之后, 更新数据源.

    1
    sudo apt-get update

本帖附件

点击下载

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

git stash 详解

发表于 2019-12-24

应用场景

  1. 当正在dev分支上开发某个项目,这时项目中出现一个bug,需要紧急修复,但是正在开发的内容只是完成一半,还不想提交,这时可以用git stash命令将修改的内容保存至堆栈区,然后顺利切换到hotfix分支进行bug修复,修复完成后,再次切回到dev分支,从堆栈中恢复刚刚保存的内容。
  2. 由于疏忽,本应该在dev分支开发的内容,却在master上进行了开发,需要重新切回到dev分支上进行开发,可以用git stash将内容保存至堆栈中,切回到dev分支后,再次恢复内容即可。

总的来说,git stash命令的作用就是将目前还不想提交的但是已经修改的内容进行保存至堆栈中,后续可以在某个分支上恢复出堆栈中的内容。

这也就是说,stash中的内容不仅仅可以恢复到原先开发的分支,也可以恢复到其他任意指定的分支上。git stash作用的范围包括工作区和暂存区中的内容,也就是说没有提交的内容都会保存至堆栈中。

注: stash只对被追踪的文件才有效, 也就是说 是针对被add的文件 。

命令详解

1. git stash

能够将所有未提交的修改(工作区和暂存区)保存至堆栈中,用于后续恢复当前工作目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: src/main/java/com/wy/CacheTest.java
modified: src/main/java/com/wy/StringTest.java

no changes added to commit (use "git add" and/or "git commit -a")

$ git stash
Saved working directory and index state WIP on master: b2f489c second

$ git status
On branch master
nothing to commit, working tree clean

2. git stash save

作用等同于git stash,区别是可以加一些注释,git stash save “test1”的效果 如下:

1
stash@{0}: On master: test1

3. git stash list

查看当前stash中的内容

4. git stash pop

将当前stash中的内容弹出,并应用到当前分支对应的工作目录上。
注:该命令将堆栈中最近保存的内容删除(栈是先进后出)
顺序执行git stash save “test1”和git stash save “test2”命令,效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git stash list
stash@{0}: On master: test2
stash@{1}: On master: test1

$ git stash pop
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: src/main/java/com/wy/StringTest.java

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (afc530377eacd4e80552d7ab1dad7234edf0145d)

$ git stash list
stash@{0}: On master: test1

可见,test2的stash是首先pop出来的。
如果从stash中恢复的内容和当前目录中的内容发生了冲突,也就是说,恢复的内容和当前目录修改了同一行的数据,那么会提示报错,需要解决冲突,可以通过创建新的分支来解决冲突。

5. git stash apply

将堆栈中的内容应用到当前目录,不同于git stash pop,该命令不会将内容从堆栈中删除,也就说该命令能够将堆栈的内容多次应用到工作目录中,适应于多个分支的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git stash apply
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: src/main/java/com/wy/StringTest.java

no changes added to commit (use "git add" and/or "git commit -a")

$ git stash list
stash@{0}: On master: test2
stash@{1}: On master: test1

堆栈中的内容并没有删除。
可以使用git stash apply + stash名字(如stash@{1})指定恢复哪个stash到当前的工作目录。

6. git stash drop + 名称

从堆栈中移除某个指定的stash

7. git stash clear

清除堆栈中的所有 内容

8. git stash show

查看堆栈中最新保存的stash和当前目录的差异。

1
2
3
$ git stash show
src/main/java/com/wy/StringTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

git stash show stash@{1}查看指定的stash和当前目录差异。
通过 git stash show -p 查看详细的不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ git stash show -p
diff --git a/src/main/java/com/wy/CacheTest.java b/src/main/java/com/wy/CacheTest.java
index 6e90837..de0e47b 100644
--- a/src/main/java/com/wy/CacheTest.java
+++ b/src/main/java/com/wy/CacheTest.java
@@ -7,6 +7,6 @@ package com.wy;
*/
public class CacheTest {
public static void main(String[] args) {
- System.out.println("git stash test");
+ System.out.println("git stash test1");
}
}
diff --git a/src/main/java/com/wy/StringTest.java b/src/main/java/com/wy/StringTest.java
index a7e146c..711d63f 100644
--- a/src/main/java/com/wy/StringTest.java
+++ b/src/main/java/com/wy/StringTest.java
@@ -12,7 +12,7 @@ public class StringTest {

@Test
public void test1() {
- System.out.println("=================");
+ System.out.println("git stash test1");
System.out.println(Strings.isNullOrEmpty(""));//true
System.out.println(Strings.isNullOrEmpty(" "));//false
System.out.println(Strings.nullToEmpty(null));//""

同样,通过git stash show stash@{1} -p查看指定的stash的差异内容

9. git stash branch

从最新的stash创建分支。

应用场景:当储藏了部分工作,暂时不去理会,继续在当前分支进行开发,后续想将stash中的内容恢复到当前工作目录时,如果是针对同一个文件的修改(即便不是同行数据),那么可能会发生冲突,恢复失败,这里通过创建新的分支来解决。可以用于解决stash中的内容和当前目录的内容发生冲突的情景。
发生冲突时,需手动解决冲突。

本帖附件

点击下载

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Handler机制详解

发表于 2019-12-22

Handler机制主要涉及到五个类

  • Handler (用于处理消息)
  • Message (消息对象)
  • MessageQueue (按一定顺序储存消息对象)
  • looper (内部是死循环, 不断从MessageQueue 中取消息)
  • HandlerThead

首先咱们来看一张Handler消息机制图

些许懵逼?

那咱们从源码开始入手,先从Handler.java开始

得出结论:

  • handler利用MessageQueue对象,调用其enquequeMessage方法将消息塞给MessageQueue
  • msg.target为handler自身对象

那么enquequeMessage具体是怎么个塞法呢?

得出结论:

  • enquequeMessage将传入的消息根据延时时间进行排序,0毫秒在前,非0毫秒在后

咱们接着看Lopper.java

得出结论

  • loop方法内部实际是一个死循环

  • 通过queque.next()从消息队列中取消息,如果没有消息就阻塞住,有消息就往下执行

  • 如果有消息, 通过dispatchMessage进行消息分发, 注意 这个dispatchMessage方法是Handler对象的

进入到Message.java中


得出结论:

  • 到最后调用了handleMessage交由用户去处理
  • 之所以系统创建多个Handler处理成百上千的消息而不会乱套,主要靠的就是target变量,相当于给每个消息绑定了一个Handler, 指定该Handler进行处理
  • 从sendMessage到handleMessage大致就是这么一个流程

那么到此为止, 问题来了:

  • 前面说到, loop方法内部是一个死循环, 那这个循环是如果实在UI线程,岂不是会造成线程阻塞?

android肯定不会允许这种情况发生的, 为了解决这个问题, HandlerThread上场了,单独开启了一个子线程用于handler

HandlerThread继承自Thread进入到HandlerThread.java中我们先找到关键方法run

得出结论:

  • perpare()主要用于初始化创建looper对象, 并将该对象存放到线程变量中,供线程对象使用
  • 初始化完成后调用loop方法开始死循环
  • 如果自己使用looper需要开启一个线程, 否则会阻塞UI线程, 并且调用perpare和loop方法
  • 平常我们在使用的handler的时候不需要开启线程, 是因为系统已经替我们开启了ActivityThread线程

Handler消息分发流程

附加

ActivityThread中也有一个loop方法

实际上, Android应用的启动运行靠的也是handler, 不断地处理系统消息, 这样四大组件才能正常运转起来

本帖附件

点击下载

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Activity跳转之scheme跳转法

发表于 2019-12-19

Activity的跳转可以说是非常简单的了、从一个页面跳转至另一个页面,我们最常用的也就是下面这种:

1
2
Intent intent = new Intent(this, xxx.class);
startActivity(intent);

当然还有我们的隐式跳转,为Intent指定一个action即可

1
2
Intent intent = new Intent("this is an action");
startActivity(intent);

这篇文章上面提到的两个都不讲,这里来说使用scheme协议来进行页面跳转

  • manifest / data 配置
  • 如果需要让我们的Activity能被其他应用或者网页所打开,需要在AndroidManifest中进行配置(浏览网页的时候点击一些广告可以进对应的app大概就是这个原理)
1
2
3
4
5
6
7
8
9
10
11

<activity android:name=".TestOneActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!--配置scheme-->
<data
android:scheme="azhon.scheme" />
</intent-filter>
</activity>
  • 如上代码我们只是简单的配置了一个android:scheme属性,那要打开这个Activity也是so easy的
1
startActivity(new Intent(Intent.ACTION_VIEW,                        Uri.parse("azhon.scheme://")));
  • 当然< data/>中还可以配置更多的属性(截取自官方中文文档,可以点击上面的manifest / data 配置)
1
2
3
4
5
6
7
<data android:scheme="string"
android:host="string"
android:port="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:mimeType="string" />

scheme 路径的规则

1
<scheme> :// <host> : <port> [<path>|<pathPrefix>|<pathPattern>]

scheme开头 :// 主机名 : 端口号 / [路径/参数] 路径后面拼接参数也是可以的下面会用到。
这个地址和我们经常使用的网页地址差不多是一样滴
eg:https://loaclhost:8080/index.jsp

在网页中打开我们对应的页面

  • 我们修改Manifest中Activity的配置在多加点料,我这里创建了两个页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
activity android:name=".TestOneActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="azhon"
android:port="1011"
android:scheme="azhon.scheme" />
</intent-filter>
</activity>

<activity android:name=".TestTwoActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="azhon"
android:port="1012"
android:scheme="azhon.scheme" />
</intent-filter>
</activity>
  • 在主页面布局添加一个WebView控件,并让它加载assets文件夹中的html文件
  • 布局文件代码:
1
2
3
4
5
<WebView
android:id="@+id/web"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp" /
  • assets文件夹中的Html代码:
1
2
3
<a href="azhon.scheme://azhon:1011/我是路径?user=958460248&psd=123456">跳转至TestOneActivity</a>
<p>
<a href="azhon.scheme://azhon:1012/i am path?toId=25&tmId=888">跳转至TestTwoActivity</a>

效果图:

在网页中只需要使用一个超链接便可以轻松的跳转至我们的app,是不是很开心。
当你将这个html文件使用手机的浏览器查看时,点击也是一样可以跳转进app的
跳转页面的时候还可以通过 ?key=value&key1=value2 键值对的方式往地址后面拼接参数

  • 在跳转过去的页面接收传递过来的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Intent intent = getIntent();
Uri uri = intent.getData();

assert uri != null;
sb.append("scheme: ").append(intent.getScheme()).append("<p>");
sb.append("host: ").append(uri.getHost()).append("<p>");
sb.append("port: ").append(uri.getPort()).append("<p>");
sb.append("path: ").append(uri.getPath()).append("<p>");
sb.append("params: ").append(uri.getQuery()).append("<p>");

//获取跳转过来携带所有参数的 键名
Set<String> names = uri.getQueryParameterNames();
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
sb.append(key).append(": ").append(uri.getQueryParameter(key)).append("<p>");
}
tv.setText(Html.fromHtml(sb.toString()));

有了这种跳转方式 你只要知道其他app的scheme你就可以随意的跳过去了想想都刺激,比如下面代码打开微信和QQ程序。

1
2
<a href="weixin://">打开微信</a><p>  
<a href="mqqzone://">打开QQ</a>

效果图:

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Android Studio 常见错误异常以及解决

发表于 2019-12-17

案例1:

1
Invoke-customs are only supported starting with Android O (--min-api 26)
解决方案:

将以下代码添加到app的build.gradle的android节点下

1
2
3
4
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

案例2

1
2
Could not create service of type FileAccessTimeJournal using GradleUserHomeScopeServices.createFileAccessTimeJournal()
Owner PID: 4567
解决方案
1
2
#杀进程
sudo kill -9 1902

案例3

在Fragment中显示popwindow报错:token null is not valid; is your activity running?

解决方案

布局绘制完后显示, 如下:

1
2
3
4
5
6
7
8
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//待布局绘制完成后显示
view.viewTreeObserver.addOnGlobalLayoutListener {
mTipPopView.showAsDropDown(binding.topBg)

}
}

案例4

Vector矢量图兼容性问题 低版本机型直接崩溃

解决方案

代码中使用:AppCompatResources.getDrawable

不要使用:ContextCompat.getDrawable

示例:

1
toolbar.setBackgroundDrawable(AppCompatResources.getDrawable(this, R.drawable.bg_toolbar_v))

本文为作者原创 转载时请注明出处 谢谢

img

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Android APT快速教程

发表于 2019-12-17

简介

APT(Annotation Processing Tool)即注解处理器,是一种用来处理注解的工具。JVM会在编译期就运行APT去扫描处理代码中的注解然后输出java文件。

image

简单来说~~就是你只需要添加注解,APT就可以帮你生成需要的代码

许多的Android开源库都使用了APT技术,如ButterKnife、ARouter、EventBus等

动手实现一个简单的APT

image

小目标

在使用Java开发Android时,页面初始化的时候我们通常要写大量的view = findViewById(R.id.xx)代码

作为一个优(lan)秀(duo)的程序员,我们现在就要实现一个APT来完成这个繁琐的工作,通过一个注解就可以自动给View获得实例

本demo地址

第零步 创建一个项目

创建一个项目,名称叫 apt_demo
创建一个Activity,名称叫 MainActivity
在布局中添加一个TextView, id为test_textview

image

第一步 自定义注解

创建一个Java Library Module名称叫 apt-annotation

在这个module中创建自定义注解 @BindView

1
2
3
4
5
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
  • @Retention(RetentionPolicy.CLASS):表示这个注解保留到编译期
  • @Target(ElementType.FIELD):表示注解范围为类成员(构造方法、方法、成员变量)

第二步 实现APT Compiler

创建一个Java Library Module名称叫 apt-compiler

在这个Module中添加依赖

1
2
3
dependencies {
implementation project(':apt-annotation')
}

image

在这个Module中创建BindViewProcessor类

直接给出代码~~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
public class BindViewProcessor extends AbstractProcessor {
private Filer mFilerUtils; // 文件管理工具类
private Types mTypesUtils; // 类型处理工具类
private Elements mElementsUtils; // Element处理工具类

private Map<TypeElement, Set<ViewInfo>> mToBindMap = new HashMap<>(); //用于记录需要绑定的View的名称和对应的id

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);

mFilerUtils = processingEnv.getFiler();
mTypesUtils = processingEnv.getTypeUtils();
mElementsUtils = processingEnv.getElementUtils();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
return supportTypes; //将要支持的注解放入其中
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();// 表示支持最新的Java版本
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
System.out.println("start process");
if (set != null && set.size() != 0) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//获得被BindView注解标记的element

categories(elements);//对不同的Activity进行分类

//对不同的Activity生成不同的帮助类
for (TypeElement typeElement : mToBindMap.keySet()) {
String code = generateCode(typeElement); //获取要生成的帮助类中的所有代码
String helperClassName = typeElement.getQualifiedName() + "$$Autobind"; //构建要生成的帮助类的类名

//输出帮助类的java文件,在这个例子中就是MainActivity$$Autobind.java文件
//输出的文件在build->source->apt->目录下
try {
JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
Writer writer = jfo.openWriter();
writer.write(code);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}

}
return true;
}

return false;
}

//将需要绑定的View按不同Activity进行分类
private void categories(Set<? extends Element> elements) {
for (Element element : elements) { //遍历每一个element
VariableElement variableElement = (VariableElement) element; //被@BindView标注的应当是变量,这里简单的强制类型转换
TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement(); //获取代表Activity的TypeElement
Set<ViewInfo> views = mToBindMap.get(enclosingElement); //views储存着一个Activity中将要绑定的view的信息
if (views == null) { //如果views不存在就new一个
views = new HashSet<>();
mToBindMap.put(enclosingElement, views);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class); //获取到一个变量的注解
int id = bindAnnotation.value(); //取出注解中的value值,这个值就是这个view要绑定的xml中的id
views.add(new ViewInfo(variableElement.getSimpleName().toString(), id)); //把要绑定的View的信息存进views中
}

}

//按不同的Activity生成不同的帮助类
private String generateCode(TypeElement typeElement) {
String rawClassName = typeElement.getSimpleName().toString(); //获取要绑定的View所在类的名称
String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement)).getQualifiedName().toString(); //获取要绑定的View所在类的包名
String helperClassName = rawClassName + "$$Autobind"; //要生成的帮助类的名称

StringBuilder builder = new StringBuilder();
builder.append("package ").append(packageName).append(";\n"); //构建定义包的代码
builder.append("import com.example.apt_api.template.IBindHelper;\n\n"); //构建import类的代码

builder.append("public class ").append(helperClassName).append(" implements ").append("IBindHelper"); //构建定义帮助类的代码
builder.append(" {\n"); //代码格式,可以忽略
builder.append("\t@Override\n"); //声明这个方法为重写IBindHelper中的方法
builder.append("\tpublic void inject(" + "Object" + " target ) {\n"); //构建方法的代码
for (ViewInfo viewInfo : mToBindMap.get(typeElement)) { //遍历每一个需要绑定的view
builder.append("\t\t"); //代码格式,可以忽略
builder.append(rawClassName + " substitute = " + "(" + rawClassName + ")" + "target;\n"); //强制类型转换

builder.append("\t\t"); //代码格式,可以忽略
builder.append("substitute." + viewInfo.viewName).append(" = "); //构建赋值表达式
builder.append("substitute.findViewById(" + viewInfo.id + ");\n"); //构建赋值表达式
}
builder.append("\t}\n"); //代码格式,可以忽略
builder.append('\n'); //代码格式,可以忽略
builder.append("}\n"); //代码格式,可以忽略

return builder.toString();
}

//要绑定的View的信息载体
class ViewInfo {
String viewName; //view的变量名
int id; //xml中的id

public ViewInfo(String viewName, int id) {
this.viewName = viewName;
this.id = id;
}
}
}

image

image

2-1 继承AbstractProcessor抽象类

我们自己实现的APT类需要继承AbstractProcessor这个类,其中需要重写以下方法:

  • init(ProcessingEnvironment processingEnv)
  • getSupportedAnnotationTypes()
  • getSupportedSourceVersion()
  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)

2-2 init(ProcessingEnvironment processingEnv)方法

这不是一个抽象方法,但progressingEnv参数给我们提供了许多有用的工具,所以我们需要重写它

1
2
3
4
5
6
7
8
9
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);

mFilerUtils = processingEnv.getFiler();
mTypesUtils = processingEnv.getTypeUtils();
mElementsUtils = processingEnv.getElementUtils();
}
12345678
  • mFilterUtils 文件管理工具类,在后面生成java文件时会用到
  • mTypesUtils 类型处理工具类,本例不会用到
  • mElementsUtils Element处理工具类,后面获取包名时会用到

限于篇幅就不展开对这几个工具的解析,读者可以自行查看文档~

2-3 getSupportedAnnotationTypes()

由方法名我们就可以看出这个方法是提供我们这个APT能够处理的注解

这也不是一个抽象方法,但仍需要重写它,否则会抛出异常(滑稽),至于为什么有兴趣可以自行查看源码~

这个方法有固定写法~

1
2
3
4
5
6
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
return supportTypes; //将要支持的注解放入其中
}

2-4 getSupportedSourceVersion()

顾名思义,就是提供我们这个APT支持的版本号

这个方法和上面的getSupportedAnnotationTypes()类似,也不是一个抽象方法,但也需要重写,也有固定的写法

1
2
3
4
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();// 表示支持最新的Java版本
}

2-5 process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)

最主要的方法,用来处理注解,这也是唯一的抽象方法,有两个参数

  • set 参数是要处理的注解的类型集合
  • roundEnv表示运行环境,可以通过这个参数获得被注解标注的代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
System.out.println("start process");
if (set != null && set.size() != 0) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//获得被BindView注解标记的element

categories(elements);//对不同的Activity进行分类

//对不同的Activity生成不同的帮助类
for (TypeElement typeElement : mToBindMap.keySet()) {
String code = generateCode(typeElement); //获取要生成的帮助类中的所有代码
String helperClassName = typeElement.getQualifiedName() + "$$Autobind"; //构建要生成的帮助类的类名

//输出帮助类的java文件,在这个例子中就是MainActivity$$Autobind.java文件
//输出的文件在build->source->apt->目录下
try {
JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
Writer writer = jfo.openWriter();
writer.write(code);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}

}
return true;
}
return false;
}

process方法的大概流程是:

  1. 扫描所有被@BindView标记的Element
  2. 遍历Element,调用categories方法,把所有需要绑定的View变量按所在的Activity进行分类,把对应关系存在mToBindMap中
  3. 遍历所有Activity的TypeElment,调用generateCode方法获得要生成的代码,每个Activity生成一个帮助类

2-6 categories方法

把所有需要绑定的View变量按所在的Activity进行分类,把对应关系存在mToBindMap中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void categories(Set<? extends Element> elements) {
for (Element element : elements) { //遍历每一个element
VariableElement variableElement = (VariableElement) element; //被@BindView标注的应当是变量,这里简单的强制类型转换
TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement(); //获取代表Activity的TypeElement
Set<ViewInfo> views = mToBindMap.get(enclosingElement); //views储存着一个Activity中将要绑定的view的信息
if (views == null) { //如果views不存在就new一个
views = new HashSet<>();
mToBindMap.put(enclosingElement, views);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class); //获取到一个变量的注解
int id = bindAnnotation.value(); //取出注解中的value值,这个值就是这个view要绑定的xml中的id
views.add(new ViewInfo(variableElement.getSimpleName().toString(), id)); //把要绑定的View的信息存进views中
}
}

注解应该已经很详细了~
实现方式仅供参考,读者可以有自己的实现

2-7 generateCode方法

按照不同的TypeElement生成不同的帮助类(注:参数中的TypeElement对应一个Activity)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private String generateCode(TypeElement typeElement) {
String rawClassName = typeElement.getSimpleName().toString(); //获取要绑定的View所在类的名称
String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement)).getQualifiedName().toString(); //获取要绑定的View所在类的包名
String helperClassName = rawClassName + "$$Autobind"; //要生成的帮助类的名称

StringBuilder builder = new StringBuilder();
builder.append("package ").append(packageName).append(";\n"); //构建定义包的代码
builder.append("import com.example.apt_api.template.IBindHelper;\n\n"); //构建import类的代码

builder.append("public class ").append(helperClassName).append(" implements ").append("IBindHelper"); //构建定义帮助类的代码
builder.append(" {\n"); //代码格式,可以忽略
builder.append("\t@Override\n"); //声明这个方法为重写IBindHelper中的方法
builder.append("\tpublic void inject(" + "Object" + " target ) {\n"); //构建方法的代码
for (ViewInfo viewInfo : mToBindMap.get(typeElement)) { //遍历每一个需要绑定的view
builder.append("\t\t"); //代码格式,可以忽略
builder.append(rawClassName + " substitute = " + "(" + rawClassName + ")" + "target;\n"); //强制类型转换

builder.append("\t\t"); //代码格式,可以忽略
builder.append("substitute." + viewInfo.viewName).append(" = "); //构建赋值表达式
builder.append("substitute.findViewById(" + viewInfo.id + ");\n"); //构建赋值表达式
}
builder.append("\t}\n"); //代码格式,可以忽略
builder.append('\n'); //代码格式,可以忽略
builder.append("}\n"); //代码格式,可以忽略

return builder.toString();
}
123456789101112131415161718192021222324252627

大家可以对比生成的代码

1
2
3
4
5
6
7
8
9
10
11
package com.example.apt_demo;
import com.example.apt_api.template.IBindHelper;

public class MainActivity$$Autobind implements IBindHelper {
@Override
public void inject(Object target ) {
MainActivity substitute = (MainActivity)target;
substitute.testTextView = substitute.findViewById(2131165315);
}

}

有没有觉得字符串拼接很麻烦呢,不仅麻烦还容易出错,那么有没有更好的办法呢(留个坑)

同样的~ 这个方法的设计仅供参考啦啦啦~

image

第三步 注册你的APT

这应该是最简单的一步
这应该是最麻烦的一步

image

客官别急,往下看就知道了~

注册一个APT需要以下步骤:

  1. 需要在 processors 库的 main 目录下新建 resources 资源文件夹;
  2. 在 resources文件夹下建立 META-INF/services 目录文件夹;
  3. 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
  4. 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;

image

正如我前面所说的~
简单是因为都是一些固定的步骤
麻烦也是因为都是一些固定的步骤

最后一步 对外提供API

4.1 创建一个Android Library Module,名称叫apt-api,并添加依赖

1
2
3
4
5
dependencies {
...

api project(':apt-annotation')
}

4.2 分别创建launcher、template文件夹
image

4.3 在template文件夹中创建IBindHelper接口

1
2
3
4
public interface IBindHelper {
void inject(Object target);
}
123

这个接口主要供APT生成的帮助类实现

4.4在launcher文件夹中创建AutoBind类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class AutoBind {
private static AutoBind instance = null;

public AutoBind() {
}

public static AutoBind getInstance() {
if(instance == null) {
synchronized (AutoBind.class) {
if (instance == null) {
instance = new AutoBind();
}
}
}
return instance;
}

public void inject(Object target) {
String className = target.getClass().getCanonicalName();
String helperName = className + "$$Autobind";
try {
IBindHelper helper = (IBindHelper) (Class.forName(helperName).getConstructor().newInstance());
helper.inject(target);
} catch (Exception e) {
e.printStackTrace();
}
}
}

这个类是我们的API的入口类,使用了单例模式
inject方法是最主要的方法,但实现很简单,就是通过反射去调用APT生成的帮助类的方法去实现View的自动绑定

完成!拉出来遛遛

在app module里添加依赖

1
2
3
4
5
dependencies {
...
annotationProcessor project(':apt-compiler')
implementation project(':apt-api')
}

我们来修改MainActivity中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {

@BindView(value = R.id.test_textview)
public TextView testTextView;

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

AutoBind.getInstance().inject(this);
testTextView.setText("APT 测试");
}
}
1234567891011121314

大功告成!我们来运行项目试试看
image

TextView已经正确显示了文字,我们的小demo到这里就完成啦~

还可以更好

我们的APT还可以变得更简单!

痛点

  • 生成代码时字符串拼接麻烦且容易出错
  • 继承AbstrctProcessor时要重写多个方法
  • 注册APT的步骤繁琐

下面我们来逐个击破~

使用JavaPoet来替代拼接字符串

JavaPoet是一个用来生成Java代码的框架,对JavaPoet不了解的请移步官方文档
JavaPoet生成代码的步骤大概是这样(摘自官方文档):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();

javaFile.writeTo(System.out);

使用JavaPoet来生成代码有很多的优点,不容易出错,可以自动import等等,这里不过多介绍,有兴趣的同学可以自行了解

使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion()

1
2
3
4
5
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.example.apt_annotation.BindView")
public class BindViewProcessor extends AbstractProcessor {
...
}

这是javax.annotation.processing中提供的注解,直接使用即可

使用Auto-Service来自动注册APT

这是谷歌官方出品的一个开源库,可以省去注册APT的步骤,只需要一行注释
先在apt-compiler模块中添加依赖

1
2
3
4
5
dependencies {
...

implementation 'com.google.auto.service:auto-service:1.0-rc2'
}

然后添加注释即可

1
2
3
4
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
...
}

总结

APT是一个非常强大而且频繁出现在各种开源库的工具,学习APT不仅可以让我们在阅读开源库源码时游刃有余也可以自己开发注解框架来帮自己写代码~

本文转载自:https://juejin.im/post/5bcdb901f265da0ac8496fed

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Java中getName()、getCanonicalName()和getSimpleName()的异同

发表于 2019-12-17

最简单的方法, 就是写个测试类测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.getname.pkg;

public class Main {

public class Demo1 {
class Demo2 {
}
}

public static void main(String[] args) {
/* 三种方式获取普通类Main的名字 */
System.out.println("-----三种方式获取普通类的名字-----");
String name = Main.class.getName();
System.out.println("\t" + "getName()方法:" + name);

String canonicalname = Main.class.getCanonicalName();
System.out.println("getCanonicalName()方法:" + "\t" + canonicalname);

String simplename = Main.class.getSimpleName();
System.out.println("getSimpleName()方法:" + "\t" + simplename);

/* getName()和getCanonicalName()获取数组名比较 */
int[] aaa = { 1, 2, 3 };
System.out.println("\r\n" + "-----对于数组 int[]-----");
name = aaa.getClass().getName();
System.out.println("\t" + "getName()方法:" + name);

canonicalname = aaa.getClass().getCanonicalName();
System.out.println("getCanonicalName()方法:" + "\t" + canonicalname);

/* getName()和getCanonicalName()获取内部类名比较 */
System.out.println("\r\n" + "-----对于内部类-----");
name = Demo1.Demo2.class.getName();
System.out.println("\t" + "getName()方法:" + name);

canonicalname = Demo1.Demo2.class.getCanonicalName();
System.out.println("getCanonicalName()方法:" + "\t" + canonicalname);
}
}

打印结果为:

1
2
3
4
5
6
7
8
9
10
11
12
-----三种方式获取普通类的名字-----
getName()方法:com.getname.pkg.Main
getCanonicalName()方法: com.getname.pkg.Main
getSimpleName()方法: Main

-----对于数组 int[]-----
getName()方法:[I
getCanonicalName()方法: int[]

-----对于内部类-----
getName()方法:com.getname.pkg.Main$Demo1$Demo2
getCanonicalName()方法: com.getname.pkg.Main.Demo1.Demo2

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

彻底搞懂Git Rebase

发表于 2019-12-17

使用 Git 已经好几年了,却始终只是熟悉一些常用的操作。对于 Git Rebase 却很少用到,直到这一次,不得不用。

一、起因

上线构建的过程中扫了一眼代码变更,突然发现,commit 提交竟然多达 62 次。我们来看看都提交了什么东西:
commit1

这里我们先不说 git 提交规范,就单纯这么多次无用的 commit 就很让人不舒服。可能很多人觉得无所谓,无非是多了一些提交纪录。

然而,并非如此,你可能听过破窗效应,编程也是如此!

二、导致问题

1.不利于代码 review
设想一下,你要做 code review ,结果一个很小的功能,提交了 60 多次,会不会有一些崩溃?

2.会造成分支污染
你的项目充满了无用的 commit 纪录,如果有一天线上出现了紧急问题,你需要回滚代码,却发现海量的 commit 需要一条条来看。

遵循项目规范才能提高团队协作效率,而不是随心所欲。

三、Rebase 场景一:如何合并多次提交纪录?

基于上面所说问题,我们不难想到:每一次功能开发, 对多个 commit 进行合并处理。

这时候就需要用到 git rebase 了。这个命令没有太难,不常用可能源于不熟悉,所以我们来通过示例学习一下。

1.我们来合并最近的 4 次提交纪录,执行:

1
git rebase -i HEAD~4

2.这时候,会自动进入 vi 编辑模式:

1
s cacc52da add: qrcodes f072ef48 update: indexeddb hacks 4e84901a feat: add indexedDB floders 8f33126c feat: add test2.js# Rebase 5f2452b2..8f33126c onto 5f2452b2 (4 commands)## Commands:# p, pick = use commit# r, reword = use commit, but edit the commit message# e, edit = use commit, but stop for amending# s, squash = use commit, but meld into previous commit# f, fixup = like "squash", but discard this commit's log message# x, exec = run command (the rest of the line) using shell# d, drop = remove commit## These lines can be re-ordered; they are executed from top to bottom.## If you remove a line here THAT COMMIT WILL BE LOST.## However, if you remove everything, the rebase will be aborted.#

有几个命令需要注意一下:

  • p, pick = use commit
  • r, reword = use commit, but edit the commit message
  • e, edit = use commit, but stop for amending
  • s, squash = use commit, but meld into previous commit
  • f, fixup = like “squash”, but discard this commit’s log message
  • x, exec = run command (the rest of the line) using shell
  • d, drop = remove commit

按照如上命令来修改你的提交纪录:

1
s cacc52da add: qrcodes f072ef48 update: indexeddb hacks 4e84901a feat: add indexedDB floderp 8f33126c feat: add test2.js

3.如果保存的时候,你碰到了这个错误:

1
error: cannot 'squash' without a previous commit

注意不要合并先前提交的东西,也就是已经提交远程分支的纪录。

4.如果你异常退出了 vi 窗口,不要紧张:

1
git rebase --edit-todo

这时候会一直处在这个编辑的模式里,我们可以回去继续编辑,修改完保存一下:

1
git rebase --continue

5.查看结果

1
git log

三次提交合并成了一次,减少了无用的提交信息。

commit2

四、Rebase 场景二:分支合并

1.我们先从 master 分支切出一个 dev 分支,进行开发:

1
git:(master) git checkout -b feature1

git1
2.这时候,你的同事完成了一次 hotfix,并合并入了 master 分支,此时 master 已经领先于你的 feature1 分支了:
git2
3.恰巧,我们想要同步 master 分支的改动,首先想到了 merge,执行:

1
git:(feature1) git merge master

git3
图中绿色的点就是我们合并之后的结果,执行:

1
git:(feature1) git log

就会在记录里发现一些 merge 的信息,但是我们觉得这样污染了 commit 记录,想要保持一份干净的 commit,怎么办呢?这时候,git rebase 就派上用场了。

4.让我们来试试 git rebase ,先回退到同事 hotfix 后合并 master 的步骤:
git4
5.使用 rebase 后来看看结果:

1
git:(feature1) git rebase master

这里补充一点:rebase 做了什么操作呢?

首先,git 会把 feature1 分支里面的每个 commit 取消掉;
其次,把上面的操作临时保存成 patch 文件,存在 .git/rebase 目录下;
然后,把 feature1 分支更新到最新的 master 分支;
最后,把上面保存的 patch 文件应用到 feature1 分支上;

git5

从 commit 记录我们可以看出来,feature1 分支是基于 hotfix 合并后的 master ,自然而然的成为了最领先的分支,而且没有 merge 的 commit 记录,是不是感觉很舒服了。

6.在 rebase 的过程中,也许会出现冲突 conflict。在这种情况,git 会停止 rebase 并会让你去解决冲突。在解决完冲突后,用 git add 命令去更新这些内容。

注意,你无需执行 git-commit,只要执行 continue

1
git rebase --continue

这样 git 会继续应用余下的 patch 补丁文件。

7.在任何时候,我们都可以用 --abort 参数来终止 rebase 的行动,并且分支会回到 rebase 开始前的状态。

1
git rebase —abort

五、更多 Rebase 的使用场景

git-rebase 存在的价值是:对一个分支做「变基」操作。

1.当我们在一个过时的分支上面开发的时候,执行 rebase 以此同步 master 分支最新变动;
2.假如我们要启动一个放置了很久的并行工作,现在有时间来继续这件事情,很显然这个分支已经落后了。这时候需要在最新的基准上面开始工作,所以 rebase 是最合适的选择。

六、为什么会是危险操作?

根据上文来看,git-rebase 很完美,解决了我们的两个问题:
1.合并 commit 记录,保持分支整洁;
2.相比 merge 来说会减少分支合并的记录;

如果你提交了代码到远程,提交前是这样的:
git2

提交后远程分支变成了这样:
git5

而此时你的同事也在 feature1 上开发,他的分支依然还是:
git6

那么当他 pull 远程 master 的时候,就会有丢失提交纪录。这就是为什么我们经常听到有人说 git rebase 是一个危险命令,因为它改变了历史,我们应该谨慎使用。

除非你可以肯定该 feature1 分支只有你自己使用,否则请谨慎操作。

结论:只要你的分支上需要 rebase 的所有 commits 历史还没有被 push 过,就可以安全地使用 git-rebase来操作。

本文转载自:http://jartto.wang/2018/12/11/git-rebase/

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

1…434445…48

乱码三千

android程序员一枚,擅长java,kotlin,python,金融投资,欢迎交流~

479 日志
139 标签
RSS
© 2024 乱码三千
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%