2017年6月29日星期四

Git分支管理策略 -- Git-Flow

Git-Flow是2010年Vincent Driessen在他的博客中提出的一个git分支模型,是最流行的分支模型。

分支类型

Git-Flow包含了两大类共五小类分支类型,下面我们一一介绍:
  1. 主分支(main branches)
    1. master
    2. develop
  2. 辅助分支(supporting branches)
    1. Feature branches
    2. Release branches
    3. Hotfix branches

主分支

主分支包括masterdevelop分支,这两个分支作为主分支,始终存在于远程仓库中。

master分支

master分支是git仓库创建后自动创建的分支。最终测试通过的正式版本都是在这个分支上发布的。

develop分支

develop分支最初由master分支分出来,这个分支用来反映下一个发布状态的最新代码。我们在发布下一个版本时,首先要将功能分支开发完成的功能在develop分支中合并、整理,达到稳定状态时才会创建预发布分支进行发布准备。

创建develop分支

$ git checkout -b develop master
Switched to a new branch 'develop'

辅助分支

辅助分支在开发中属于临时性的分支,在使用完成后都可以删除。

功能分支(Feature branches)

Branch offMerge back
developdevelop
功能分支一般从develop分支分出,用来开发新功能新特性。这个分支最终会合并到develop分支(如果不需要这个功能了,也可以放弃这个分支的合并并删除)。
功能分支一般只存在于开发者本地仓库,不需要推送到origin仓库。在合并到develp或抛弃后,删除这个分支。

创建功能分支

下面我们从develop分支创建feature-1功能分支:
$ git checkout -b feature-1 develop
Switched to a new branch 1feature-1'

合并功能分支到develop

功能开发完成,准备发布下一个发布版本时,先合并到develop分支:
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff feature-1
Merge made by the 'recursive' strategy.
 build.gradle                      | 7 +++++++
 src/main/java/me/daemon/Main.java | 7 +++++++
 2 files changed, 14 insertions(+)
 create mode 100644 build.gradle
 create mode 100644 src/main/java/me/daemon/Main.java
$ git branch -d feature-1
Deleted branch feature-1 (was c6fc6e6).
$ git push origin develop

预发布分支(Release branches)

Branch offMerge back
developdevelop and master
预发布分支用来准备新版本的发布。
我们在开发中,一个版本可以包含多个功能或特性的开发,可能需要多个功能分支并行开发。当这些功能都开发完成后,准备发布时,先将这些功能分支合并到develop分支,然后从develop分支分出预发布分支。
当分出预发布分支后,我们可以用这个分支用来提测,修复bug等,这样develop分支就可以继续进行下一个版本的开发工作,新版本的功能分支可以合并到develop分支而不影响上一个版本的测试、修复BUG、发布等工作。

创建预发布分支

develop分支创建预发布分支release-v0.1
$ git checkout -b release-v0.1 develop
Switched to a new branch 'release-v0.1'
创建预发布分支后,在该分支上修改发布版本号,然后提交修改。

完成预发布分支

在预发布分支通过最终测试,准备发布时,需要合并到masterdevelop分支。
首先合并到master并打上tag
$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git merge --no-ff release-v0.1
Merge made by the 'recursive' strategy.
 build.gradle                         | 9 +++++++++
 src/main/java/me/daemon/Feature.java | 7 +++++++
 src/main/java/me/daemon/Main.java    | 7 +++++++
 3 files changed, 23 insertions(+)
 create mode 100644 build.gradle
 create mode 100644 src/main/java/me/daemon/Feature.java
 create mode 100644 src/main/java/me/daemon/Main.java
$ git tag -a v0.1 -m "tag v0.1"
然后合并到develop
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-v0.1
Merge made by the 'recursive' strategy.
 build.gradle | 2 ++
 1 file changed, 2 insertions(+)
此时,该预发布分支的使命已经完成,可以删除该预发布分支:
$ git branch -d release-v0.1
Deleted branch release-v0.1 (was 7942f36).

BUG修复分支(Hotfix branches)

Branch offMerge back
masterdevelop and master
BUG修复分支类似与预发布分支,都是用来准备下一个版本的发布。不同的是,预发布版本是我们计划好的功能与特性集合的发布,而BUG修复版本是为了紧急修复当前发布版本的BUG而存在的。
一般我们开发中遇到的非致命、非紧急的BUG往往选择随同下个版本一起发布,只有遇到致命问题才需要立即在当前发布版本基础上分出分支,修改BUG,并立即发布下一个版本。因此BUG修复分支一般是从master分支分出的。修复完成后要合并到masterdevelop分支。

创建BUG修复分支

master分支创建BUG修复分支hotfix-1.0.1
$ git checkout -b hotfix-0.1 master
Switched to a new branch 'hotfix-0.1'
hotfix-1.0.1上升级版本号、修复BUG,然后提交修改。

完成BUG修复

首先合并到master并打tag
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-0.1
Merge made by the 'recursive' strategy.
 src/main/java/me/daemon/Feature.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git tag -a v0.2 -m "tag v0.2"
然后合并到develop
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-0.1
Merge made by the 'recursive' strategy.
 src/main/java/me/daemon/Feature.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
这里要注意,如果此时存在一个预发布分支,准备发布下一个版本,那么BUG修复分支的修改要合并到预发布分支,而不是develop分支。而在预发布分支发布后,BUG修复的代码会随着发布的代码一起合并到develop
最终,可以删除BUG修复分支:
$ git branch -d hotfix-0.1
Deleted branch hotfix-0.1 (was 17dd42a).

推送远程仓库

将分支都推送到远程仓库:
$ git push --all
推送tag:
$ git push origin v0.1
$ git push origin v0.2
也可以一次推送所有tag:
$ git push origin --tags

示例

我将示例放到我的github上,地址为:https://github.com/daemon369/git-flow-demo。为了演示,辅助分支没有删除。

图示

参看

2017年6月28日星期三

Android唯一标识

Android设备用来唯一标示用户或设备的方法很多,没有统一的完美的方案。
有以下常用方案:
  1. IMEI/MEID
  2. ANDROID_ID
  3. Mac address
  4. Serial number
  5. Installation / GUID

IMEI/MEID

介绍

下面是维基百科上IMEI的介绍:
国际移动设备识别码(International Mobile Equipment Identity,IMEI),即通常所说的手机序列号、手机“串号”,用于在移动电话网络中识别每一部独立的手机等行动通讯装置,相当于移动电话的身份证。序列号共有15位数字,前6位(TAC)是型号核准号码,代表手机类型。接着2位(FAC)是最后装配号,代表产地。后6位(SNR)是串号,代表生产顺序号。最后1位(SP)一般为0,是检验码,备用。国际移动设备识别码一般贴于机身背面与外包装上,同时也存在于手机内存中,通过输入*#06#即可查询。
下面是百度百科里MEID的介绍:
MEID 移动设备识别码(Mobile Equipment Identifier)是CDMA手机的身份识别码,也是每台CDMA手机或通讯平板唯一的识别码。通过这个识别码,网络端可以对该手机进行跟踪和监管。用于CDMA制式的手机。MEID的数字范围是十六进制的,和IMEI的格式类似。
MEID's are also manged by the TIA. These are 56 bits long, and like ESN's, identify the manufacturer of a mobile device as well as the serial number assigned to the device by that manufacturer.
The MEID consists of 32 bits to specify the manufacturer and 24 bits to specify the serial number: The first 4 bits of the manufacturer code are "reserved," and restricted to just a few values. There can also be a 4 bit "check digit" appended to the end, but this is not transmitted between the mobile device and the CDMA system (according to 3GPP2 S.R0048-A) and is not common to see.

获取

Android中获取IMEI的方法,首先需要在AndroidManifest.xml文件中增加android.permission.READ_PHONE_STATE权限:
<manifest ...>
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <application ...>
    ...
    </application>
</manifest>
然后获取IMEI
final TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
final String imei = tm.getDeviceId();
我们看下TelephonyManager中的getDeviceId方法:
/**
 * Returns the unique device ID, for example, the IMEI for GSM and the MEID
 * or ESN for CDMA phones. Return null if device ID is not available.
 *
 * <p>Requires Permission:
 *   {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
 */
public String getDeviceId();
下面是API level 23新增的方法:
/**
 * Returns the unique device ID of a subscription, for example, the IMEI for
 * GSM and the MEID for CDMA phones. Return null if device ID is not available.
 *
 * <p>Requires Permission:
 *   {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
 *
 * @param slotId of which deviceID is returned
 */
public String getDeviceId(int slotId);
这两个接口可以返回GSM手机的IMEI或者CDMA手机的MEIDslotId是卡槽编号。
这两个接口在API level O中都标记为@Deprecated,取而代之的是如下接口:
/**
 * Returns the IMEI (International Mobile Equipment Identity). Return null if IMEI is not
 * available.
 *
 * <p>Requires Permission:
 *   {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
 */
public String getImei();

/**
 * Returns the IMEI (International Mobile Equipment Identity). Return null if IMEI is not
 * available.
 *
 * <p>Requires Permission:
 *   {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
 *
 * @param slotIndex of which IMEI is returned
 */
public String getImei(int slotIndex);

/**
 * Returns the MEID (Mobile Equipment Identifier). Return null if MEID is not available.
 *
 * <p>Requires Permission:
 *   {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
 */
public String getMeid();

/**
 * Returns the MEID (Mobile Equipment Identifier). Return null if MEID is not available.
 *
 * <p>Requires Permission:
 *   {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
 *
 * @param slotIndex of which MEID is returned
 */
public String getMeid(int slotIndex);

存在的问题

  1. 需要android.permission.READ_PHONE_STATE权限,用户可以禁止授权给应用
  2. 非手机设备没有IMEI/MEID
  3. IMEI/MEID理论上应该没有重复,实际却不是这样的,有些厂商生产的设备会有重复

ANDROID_ID

下面是ANDROID_ID的解释:
A 64-bit number (as a hex string) that is randomly generated when the user first sets up the device and should remain constant for the lifetime of the user's device. The value may change if a factory reset is performed on the device.

获取

import android.provider.Settings.Secure;

final String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);

存在的问题

  1. 设备重置后会重新生成ANDROID_ID
  2. 部分厂商的设备存在ANDROID_ID重复的情况,同样不是绝对可靠
  3. Android O 版本中,对于设备上的每个应用和用户,都有不同的ANDROID_ID

Mac address

mac地址或蓝牙地址也可以用作设备标识

获取

获取mac地址需要android.permission.ACCESS_WIFI_STATE权限:
<manifest ...>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

    <application ...>
        ...
    </application>
</manifest>
获取mac代码:
String mac = null;
final WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
if (wifiManager != null) {
    final WifiInfo info = wifiManager.getConnectionInfo();
    if (info != null) {
        mac = info.getMacAddress();
    }
}

存在的问题

下面是 Android 官网的说明
MAC 地址具有全局唯一性,无法由用户重置,在恢复出厂设置后也不会发生变化。一般不建议使用 MAC 地址进行任何形式的用户标识。因此,从 Android M 开始,无法再通过第三方 API 获得本地设备 MAC 地址(例如,WLAN 和蓝牙)。WifiInfo.getMacAddress() 方法和 BluetoothAdapter.getDefaultAdapter().getAddress() 方法都会返回 02:00:00:00:00:00。
  1. Android M开始无法获取 Mac 地址
  2. 没有 WIFI 的设备无法获取
  3. Wifi 没有打开时也无法获取 Mac 地址

Serial number

获取

final String sn =  android.os.Build.SERIAL;
这是API level 9添加的,在 Android O 中这个字段被置为"unknown",需要使用android.os.Build.getSerial()方法来获取:
public static getSerial();

存在的问题

  1. Android O 中,android.os.Build.SERIAL标记为@deprecated,使用android.os.Build.getSerial()需要android.permission.READ_PHONE_STATE权限

Installation ID / GUID

Installation IDAndroid Developers Blog提供的方案,原理是应用自己生成一个UUID字符串存储在本地缓存里来作为标识。

获取

public class Installation {
    private static String sID = null;
    private static final String INSTALLATION = "INSTALLATION";

    public synchronized static String id(Context context) {
        if (sID == null) {  
            File installation = new File(context.getFilesDir(), INSTALLATION);
            try {
                if (!installation.exists())
                    writeInstallationFile(installation);
                sID = readInstallationFile(installation);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return sID;
    }

    private static String readInstallationFile(File installation) throws IOException {
        RandomAccessFile f = new RandomAccessFile(installation, "r");
        byte[] bytes = new byte[(int) f.length()];
        f.readFully(bytes);
        f.close();
        return new String(bytes);
    }

    private static void writeInstallationFile(File installation) throws IOException {
        FileOutputStream out = new FileOutputStream(installation);
        String id = UUID.randomUUID().toString();
        out.write(id.getBytes());
        out.close();
    }
}

存在的问题

应用卸载重装,或者清理缓存,会清除掉这个标识,下次只能重新生成一个新的UUID字符串标识

参看

Android logcat

Log等级 Android log 等级在 android/log.h 中定义如下: typedef   enum   android_LogPriority {    /** For internal use only. */ ANDROID_LOG_UNKNOWN =...