Android—混淆與打包

我們都希望自己的代碼足夠"安全",即使別人反編譯了我們的應(yīng)用噪裕,他們也很難從反編譯的代碼中找出漏洞蹲盘。這時候我們就依賴編譯器的混淆功能,混淆會將大部分(下面會解釋為什么是大部分)類和成員的名稱重命名為沒有意義的短名膳音,例如aa召衔、ab這種,此時的代碼基本沒有可讀性祭陷,也就不容易找到漏洞苍凛。想要從代碼的角度分析混淆做了什么,我們就得查看混淆后的代碼兵志,本文通過反編譯來分析混淆前后的代碼有何不同醇蝴。

一、混淆與反編譯

1.1 混淆想罕、縮減與優(yōu)化應(yīng)用

混淆并不是單獨(dú)使用的悠栓,當(dāng)你啟用混淆時,編譯器還會同時縮減和優(yōu)化你的應(yīng)用按价,以盡可能地減小應(yīng)用的大小惭适。當(dāng)發(fā)布應(yīng)用的release版本時就需要開啟混淆,在build.gradle中添加以下代碼即可啟用楼镐。

    android {
        buildTypes {
            release { // 用于應(yīng)用的release版本
                // 啟用 代碼縮減癞志、混淆、代碼優(yōu)化
                minifyEnabled true

                // 資源縮減
                shrinkResources true

                // 這里引入了Android插件自帶的混淆規(guī)則
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
                        'proguard-rules.pro'
            }
        }
        ...
    }
1.1.1 minifyEnabled

minifyEnabled為true表示啟用代碼縮減框产、混淆處理和優(yōu)化凄杯。

  1. 代碼縮減:也稱"搖樹優(yōu)化"错洁,指從應(yīng)用及其依賴庫中檢測并安全地移除未使用的類、字段戒突、方法和屬性屯碴。如果應(yīng)用僅使用某個依賴庫的少數(shù)幾個 API,縮減功能可以識別應(yīng)用未使用的庫代碼并僅從應(yīng)用中移除這部分代碼妖谴。
    搖樹優(yōu)化
  2. 混淆處理:通過縮短類和成員變量的名稱窿锉,減小dex包的大小。寫代碼時膝舅,我們?yōu)榱舜a的可讀性嗡载,會為類、方法和變量定義通俗的名稱仍稀。例如boolean isDataLoadFinished洼滚,一看就知道是判斷數(shù)據(jù)是否加載完畢的,但是混淆之后就會變?yōu)轭愃?code>boolean aa這樣的名稱技潘。
    當(dāng)然并不是所有的類和成員都能被混淆遥巴,上方配置的第3項中的proguard-rules.pro是用戶自定義的混淆規(guī)則,用戶可以自行決定哪些類不該被混淆享幽。例如反射或自定義View這些需要用到原始類名或者方法名的類和成員就不該被混淆铲掐,之后會詳細(xì)介紹如何自定義混淆規(guī)則。

  3. 代碼優(yōu)化:檢查并重寫代碼值桩。例如摆霉,如果檢測到if/else語句中的else{...}代碼塊從未被執(zhí)行,那么編譯器會移除該部分代碼奔坟,以進(jìn)一步縮減dex包的大行啊;或者檢測到某個方法只被調(diào)用一次咳秉,可能會將該方法移除并內(nèi)嵌在調(diào)用的地方婉支。

1.1.2 shrinkResources

資源縮減:從封裝應(yīng)用中移除不使用的資源,包括依賴庫中不使用的資源澜建。此功能可與代碼縮減功能結(jié)合使用向挖,這樣一來,移除不使用的代碼后炕舵,也可以安全地移除不再引用的所有資源户誓。

不過這并非萬無一失,我同事之前遇到過這樣一種情況:我們的應(yīng)用分為淺色模式與深色模式幕侠,淺色模式下的資源名為xxx,深色模式下的資源名為xxx_dark碍彭。當(dāng)從淺色模式切換至深色模式時晤硕,代碼沒有直接引用深色模式下的資源圖片悼潭,而是在資源名xxx后面拼接_dark,修改資源名字達(dá)到替換圖片的效果舞箍。但是編譯器在資源縮減階段發(fā)現(xiàn)xxx_dark沒有被引用舰褪,就將所有深色模式的圖刪掉了。

此時我們只能自定義資源保留的規(guī)則:修改res/raw/keep.xml疏橄,在 tools:keep 屬性中指定每個要保留的資源占拍,在 tools:discard 屬性中指定每個要舍棄的資源。這兩個屬性都接受以逗號分隔的資源名稱列表捎迫,可以將星號字符用作通配符晃酒。如下所示,指定保留以_dark結(jié)尾的資源窄绒。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@drawable/*_dark />

1.2 反編譯APK

1.2.1 反編譯步驟

查看反編譯后的代碼主要依賴dex2jar和jd-gui這2個工具贝次,具體步驟也很簡單:
① 將APK后綴改為rar,解壓得到dex文件彰导。
② 通過dex2jar將dex文件轉(zhuǎn)為jar文件蛔翅。以Windows系統(tǒng)為例,下載dex-tools后解壓位谋,將dex文件復(fù)制到該目錄下山析,執(zhí)行d2j-dex2jar.bat classes.dex命令即可得到classes-dex2jar.jar文件。不過網(wǎng)絡(luò)上的dex-tools不一定是最新版掏父,最好在github下載源碼笋轨,編譯成功后執(zhí)行gradlew assemble,執(zhí)行完畢后可在dex2jar-2.x\dex-tools\build\distributions目錄下得到dex-tools损同。
③ 下載jd-gui翩腐,解壓后打開jd-gui.exe,選擇jar文件查看源碼即可膏燃。

1.2.2 反編譯實(shí)踐

新建一個測試混淆的項目茂卦,由于AndroidStudio打包默認(rèn)是debug包,先啟用debug包的混淆组哩。

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

項目中添加以下文件等龙。
① MainActivity和SecondActivity
② 自定義視圖TestView(繼承自View)
③ WebView交互類CommonJSApi
④ 工具類SizeUtils

項目結(jié)構(gòu).png

此時項目使用的是默認(rèn)的混淆規(guī)則,來看一下反編譯后的項目結(jié)構(gòu)伶贰,我們發(fā)現(xiàn)MainActivity蛛砰、SecondActivity和TestView還保留著原本的名字,而CommonJSApi和SizeUtils的類名已經(jīng)被混淆成了a和b黍衙。

反編譯項目結(jié)構(gòu).png

下面來看每個類混淆前后的具體代碼泥畅。
① MainActivity
原始代碼如下,定義了2個沒有使用的變量mUnuesed和mUnUsedString琅翻,onCreate(...)中有一段if/else代碼位仁,很明顯else{}代碼塊中的內(nèi)容不會被執(zhí)行柑贞。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private int mUnuesed;
    private String mUnUsedString = "hahaha";
    private boolean mShouldShowDensity = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn_test).setOnClickListener(this);

        if (mShouldShowDensity) {
            Log.e("TAG", "density: " + SizeUtils.getDensity(this));
        } else {
            Log.e("TAG", "nothing to show");
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_test:
                startActivity(new Intent(this, SecondActivity.class));
                break;
        }
    }
}

混淆后代碼如下,沒有使用到的變量mUnuesed和mUnUsedString已經(jīng)被刪除聂抢,但是不可能被執(zhí)行到的else{}代碼塊卻并沒有被刪除钧嘶。帶著疑惑,我改成了if (true) {...}琳疏,再次打包反編譯有决,發(fā)現(xiàn)else{}代碼塊被刪除了......
希望有朋友交流一下這個優(yōu)化規(guī)則的觸發(fā)條件,非要寫成if (true)這樣才進(jìn)行代碼優(yōu)化的話空盼,這就顯得有點(diǎn)雞肋书幕。

public class MainActivity extends d implements View.OnClickListener {
  public boolean s = true;
  
  public void onClick(View paramView) {
    if (paramView.getId() == 2131165250)
      startActivity(new Intent((Context)this, SecondActivity.class)); 
  }
  
  public void onCreate(Bundle paramBundle) {
    super.onCreate(paramBundle);
    setContentView(2131361820);
    findViewById(2131165250).setOnClickListener(this);
    if (this.s) {
      StringBuilder stringBuilder = new StringBuilder();
      stringBuilder.append("density: ");
      stringBuilder.append(b.a((Activity)this));
      Log.e("TAG", stringBuilder.toString());
    } else {
      Log.e("TAG", "nothing to show"); // 為何沒有被刪除?
    } 
  }
}

② 自定義視圖TestView
這個自定義View比較簡單我注,原始代碼就不貼了按咒,直接看混淆后的代碼,只是變量名和方法名被修改了但骨。

public class TestView extends View {
  public Paint b;
  public int c; (原始: private int mWidth;)
  public int d; (原始: private int mHeight;)
  
  public TestView(Context paramContext) {
    this(paramContext, null);
  }
  
  public TestView(Context paramContext, AttributeSet paramAttributeSet) {
    this(paramContext, paramAttributeSet, 0);
  }
  
  public TestView(Context paramContext, AttributeSet paramAttributeSet, int paramInt) {
    super(paramContext, paramAttributeSet, paramInt);
    a();
  }
  
  public final void a() {
    this.b = new Paint(1);
    this.b.setStyle(Paint.Style.FILL_AND_STROKE);
    this.b.setColor(-16776961);
  }
  
  public void onDraw(Canvas paramCanvas) {
    super.onDraw(paramCanvas);
    paramCanvas.drawRect(0.0F, 0.0F, this.c, this.d, this.b);
  }
  
  public void onSizeChanged(int paramInt1, int paramInt2, int paramInt3, int paramInt4) {
    super.onSizeChanged(paramInt1, paramInt2, paramInt3, paramInt4);
    this.c = paramInt1;
    this.d = paramInt2;
  }
}

③ WebView交互類CommonJSApi
原始代碼如下,構(gòu)造函數(shù)中雖然傳入了Context變量掠抬,但是并未被使用两波。
還有1個被@JavascriptInterface注解的getVersion()方法。

public class CommonJSApi {
    public CommonJSApi(Context context) {}

    @JavascriptInterface
    public String getVersion() {
        return "1";
    }
}

混淆后的代碼如下劣坊,由于構(gòu)造函數(shù)中的Context沒有用到局冰,因此我們自己添加的構(gòu)造函數(shù)被刪除了模蜡,可以直接使用原本的無參構(gòu)造函數(shù)谆焊。而getVersion()的方法名沒有被更改,因?yàn)镴S會通過方法名進(jìn)行調(diào)用烫罩。

public class a {
  @JavascriptInterface
  public String getVersion() {
    return "1";
  }
}

④ 工具類SizeUtils
原始代碼如下,getDensity(Activity activity)在MainActivity中被使用過时甚,而getString()沒有被用到過。

public class SizeUtils {
    public static float getDensity(Activity activity) {
        DisplayMetrics dm = new DisplayMetrics();
        activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
        return dm.density;
    }

    public static String getString() {
        return "HAHA";
    }
}

混淆后的代碼如下刀诬,類名和方法名都被混淆了陕壹,而沒有用到的方法被刪除了糠馆。

public class b {
  public static float a(Activity paramActivity) {
    DisplayMetrics displayMetrics = new DisplayMetrics();
    paramActivity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
    return displayMetrics.density;
  }
}

二、混淆規(guī)則

2.1 默認(rèn)混淆規(guī)則

雖然'proguard-rules.pro'文件中還沒有添加任何混淆規(guī)則毕匀,但是編譯器已經(jīng)知道哪些類和變量一定不能被混淆,例如Activity凤薛、自定義View缤苫、JavascriptInterface等等活玲,這些屬于默認(rèn)的混淆規(guī)則舒憾。如果想告訴編譯器還有哪些類和變量也不該被混淆镀迂,就需要用戶自己添加規(guī)則探遵,項目中的以下內(nèi)容都不該被混淆箱季。
① 枚舉
② 第三方庫
③ 運(yùn)用了反射的類
④ 網(wǎng)絡(luò)數(shù)據(jù)解析的JavaBean實(shí)體類
⑤ Parcelable的子類和 Creator 靜態(tài)成員變量
⑥ 四大組件拷况、自定義的Application
⑦ JNI中調(diào)用的類

2.2 自定義混淆規(guī)則

先來看混淆規(guī)則的通配符赚瘦。

通配符 描述
<field> 匹配類中的所有字段
<method> 匹配類中所有的方法
<init> 匹配類中所有的構(gòu)造函數(shù)
* 匹配任意長度字符蚤告,不包含包名分隔符(.)
** 匹配任意長度字符,包含包名分隔符(.)
*** 匹配任意參數(shù)類型

再來看制定混淆規(guī)則的關(guān)鍵字心褐,這些關(guān)鍵字指定了混淆規(guī)則的粒度逗爹。
① keep: 保留類名或整個類不被混淆

// 直接將keep作用于類掘而,只是保證類名不被混淆,如下所示斑胜,成員還是會被混淆
-keep public class com.lister.autopacktest.SizeUtils
// 如果不混淆整個類的話止潘,規(guī)則如下所示
-keep public class com.lister.autopacktest.SizeUtils { *; }

// * 通配符表示保持該包下的類名涧狮,但是子包的類名還是會被混淆
-keep public class com.lister.autopacktest.utils.*
// ** 通配符表示保持該包及其子包下的類名
-keep public class com.lister.autopacktest.utils.**
// 如果想保留該包下的所有類名與方法,需要加上{ *; }
-keep public class com.lister.autopacktest.utils.** { *; }

② keepnames: 保留類和類中的成員的命名钢颂,成員沒有被引用會被移除
注意keepnames只是防止類和成員被重命名殊鞭,沒有被引用的成員還是會被移除锯仪。

③ keepclassmembers: 保留類中的成員,防止被移除和重命名

// 類似之前示例中的CommonJSApi,雖然類名被混淆了斥扛,但是方法未被混淆稀颁。
// 如果想保留特定的方法,可以定義如下的規(guī)則粘昨。
// 1. 不混淆某個類的構(gòu)造方法
-keepclassmembers class com.lister.autopacktest.SizeUtils { 
    public <init>(); 
}
// 2. 不混淆某個類的特定的方法
-keepclassmembers class com.lister.autopacktest.SizeUtils { 
    public void test(java.lang.String); 
}

④ keepclassmembernames: 保留類中成員的命名,成員沒有引用會被移除

⑤ keepclasseswithmembers: 保留指明的類和成員馁启,防止被移除和重命名

⑥ keepclasseswithmembernames: 保留指明的類和成員妖啥,防止被重命名荆虱,成員沒有引用會被移除

上面提到诉位,keep關(guān)鍵字可以保留單個類或者某個包下的類,如果不被混淆的類不在一個包下岳瞭,就需要一個一個添加到混淆規(guī)則中枫耳。那么有沒有什么辦法能夠簡化這個流程呢钻心?這里介紹一種通過注解定義混淆規(guī)則的方法,在不被混淆的類和成員上加上注解即可说墨,先定義@Keep@KeepAll兩個注解。
@Keep注解可添加在類熄捍、變量碟贾、方法上,表示不混淆當(dāng)前被注解的內(nèi)容缕陕。

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface Keep {
   String value() default "";
}

@KeepAll注解添加在類上,表示不混淆當(dāng)前類的所有內(nèi)容疙挺。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface KeepAll {
    String value() default "";
}

在proguard-rules添加如下規(guī)則即可扛邑,可以發(fā)現(xiàn)@Keep注解對方法、變量和類名做了約束铐然,而@KeepAll注解對整個類做了約束蔬崩。

-keep @interface <packagename>.Keep
-keep @interface <packagename>.KeepAll

-keepclassmembers class * {
  @<packagename>.Keep <methods>;
  @<packagename>.Keep <fields>;
}
-keep @<packagename>.Keep class *
-keep @<packagename>.KeepAll class * { *; }

當(dāng)需要對內(nèi)部類操作時,通過$指明內(nèi)部類搀暑;同時可以用private沥阳、public進(jìn)一步指定需要保留的內(nèi)容。例如當(dāng)你需要保留某個內(nèi)部類的public構(gòu)造函數(shù)時:

-keep class Test$T {
    public <init>;
}

三自点、mapping文件

當(dāng)應(yīng)用崩潰或者發(fā)生錯誤時桐罕,我們會得到方法調(diào)用棧來分析問題,而混淆過的應(yīng)用提供的是混淆后的調(diào)用棧桂敛,此時我們就需要解混淆功炮。打開...build/outputs/mapping目錄下的mapping.txt文件,可以找到項目中所有類和變量混淆前后的名字與對應(yīng)關(guān)系术唬。

來看看測試項目的mapping文件薪伏,不僅有類和成員的對應(yīng)關(guān)系,還很貼心地為你標(biāo)出了方法所在的行數(shù)粗仓。

com.lister.autopacktest.CommonJSApi -> b.a.a.a:
    9:10:void <init>(android.content.Context) -> <init>
    14:14:java.lang.String getVersion() -> getVersion
com.lister.autopacktest.MainActivity -> com.lister.autopacktest.MainActivity:
    boolean mShouldShowDensity -> s
    11:15:void <init>() -> <init>
    33:38:void onClick(android.view.View) -> onClick
    19:29:void onCreate(android.os.Bundle) -> onCreate
com.lister.autopacktest.SecondActivity -> com.lister.autopacktest.SecondActivity:
    android.webkit.WebView mWebView -> t
    android.widget.FrameLayout mWebViewContainer -> s
    11:11:void <init>() -> <init>
    18:42:void onCreate(android.os.Bundle) -> onCreate
    46:49:void onDestroy() -> onDestroy
com.lister.autopacktest.SizeUtils -> com.lister.autopacktest.SizeUtils:
    7:7:void <init>() -> <init>
    10:12:float getDensity(android.app.Activity) -> a
com.lister.autopacktest.TestView -> com.lister.autopacktest.TestView:
    android.graphics.Paint mPaint -> b
    int mHeight -> d
    int mWidth -> c
    20:21:void <init>(android.content.Context) -> <init>
    24:25:void <init>(android.content.Context,android.util.AttributeSet) -> <init>
    28:30:void <init>(android.content.Context,android.util.AttributeSet,int) -> <init>
    33:36:void init() -> a
    47:49:void onDraw(android.graphics.Canvas) -> onDraw
    40:43:void onSizeChanged(int,int,int,int) -> onSizeChanged

四嫁怀、Gradle打包

4.1 實(shí)現(xiàn)debug與release包不同包名

商業(yè)化的應(yīng)用都分為debug版和release版设捐,debug版用于快速調(diào)試錯誤,release版本用于外發(fā)塘淑。在AS中直接run app或者build apk生成的就是debug版本萝招。為了兩個版本的應(yīng)用在手機(jī)上共存,可以修改debug版應(yīng)用的包名朴爬,加一個.debug后綴即寒,如下所示。

android {
    ......
    buildTypes {
        debug {
            applicationIdSuffix ".debug"
            ......
        }
    }
}

4.2 簽名

APK的打包都需要簽名召噩,平時打debug包時如果沒有指定簽名母赵,默認(rèn)使用debug.keystore作為debug包的簽名,可以通過keytool -list -v -keystore xxx命令查看某個簽名的信息具滴。
也可以通過gradle的signingReport這個Task查看凹嘲,在命令行運(yùn)行gradlew signingReport即可在命令行看到debug.keystore的信息,如下所示构韵。

> Task :app:signingReport
  Variant: debug
  Config: debug
  Store: C:\Users\win10\.android\debug.keystore
  Alias: AndroidDebugKey
  MD5: ......
  SHA1: ......
  SHA-256: ......
  Valid until: 2049年9月14日 星期二

而打release肯定不能用debug簽名周蹭,首先需要新建一個簽名文件。點(diǎn)擊AS的build->Generated signed Bundle/APK疲恢,選擇APK凶朗,點(diǎn)擊Create new...新建簽名文件,我這里已經(jīng)新建過了显拳,存放在項目里app目錄下棚愤。隨后點(diǎn)擊next,選擇release版本杂数,在下方選擇v1宛畦、v2兩個簽名,點(diǎn)擊finish即可打包揍移。

新建簽名.png

我們也可以在gradle.build中配置打包所使用的簽名次和,如下所示。在signingConfigs中配置release版本的簽名路徑那伐、密碼等信息后踏施,在buildTypes中通過signingConfig signingConfigs.release指定簽名配置為signingConfigs中的信息。隨后在命令行運(yùn)行gradlew assembleRelease即可罕邀。

    signingConfigs {
        release {
            storeFile file('../app/autoKey.jks')
            storePassword "123456"
            keyAlias "autoKey"
            keyPassword "123456"
            v1SigningEnabled true
            v2SigningEnabled true
        }
    }

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

上面的配置中直接聲明了簽名的密碼等信息读规,如果應(yīng)用開源,這些信息很容易被獲取燃少。因此官方更推薦通過properties文件的形式聲明簽名信息束亏,新建keystore.properties文件。

storePassword=......
keyPassword=......
keyAlias=......
storeFile=......

之后在build.gradle中讀取keystore.properties文件即可阵具,注意keystore.properties應(yīng)該存儲于安全的地方碍遍,不應(yīng)該隨著應(yīng)用的代碼一起上傳上去定铜,否則它就沒有意義了。

......
    def keystorePropertiesFile = rootProject.file("keystore.properties")
    def keystoreProperties = new Properties()
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

    android {
        signingConfigs {
            release {
                keyAlias keystoreProperties['keyAlias']
                keyPassword keystoreProperties['keyPassword']
                storeFile file(keystoreProperties['storeFile'])
                storePassword keystoreProperties['storePassword']
            }
        }
    }

如果要查看APK的簽名信息怎么辦怕敬?
將APK解壓后進(jìn)入META-INF目錄揣炕,其中的CERT.RSA文件中就存放著簽名信息。在該目錄下運(yùn)行命令keytool -printcert -file CERT.RSA即可查看該APK的簽名信息东跪。

五畸陡、Jenkins打包

在項目過程中,測試經(jīng)常需要研發(fā)去打某個分支的包虽填,研發(fā)人員需要進(jìn)行保存代碼丁恭、切換分支、修改配置......等一系列操作斋日,影響開發(fā)效率牲览。而使用Jenkins進(jìn)行遠(yuǎn)程打包就沒有這個煩惱了,只要輸入對應(yīng)的分支名即可打包恶守。

Jenkins可以去官網(wǎng)下載第献,不過速度很慢,windows版本我上傳到了CSDN兔港,有需要的同學(xué)自扔购痢:Jenkins
具體打包流程具體見參考5衫樊、6岔绸,本來想寫一下這塊的踩坑經(jīng)歷,但是發(fā)現(xiàn)大神們都寫的很詳細(xì)了橡伞,就不畫蛇添足了。實(shí)踐中唯一的問題是插件下載太慢晋被,可以在官網(wǎng)jenkins插件手動下載插件后安裝兑徘。

六、參考

  1. 縮減羡洛、混淆處理和優(yōu)化您的應(yīng)用
  2. Android混淆
  3. 深入理解 Android(一):Gradle 詳解
  4. Android Gradle學(xué)習(xí)(一):Gradle基礎(chǔ)入門
  5. Android Jenkins+Git+Gradle持續(xù)集成
  6. Android 使用 Jenkins 實(shí)現(xiàn)自動化打包
  7. Jenkins安裝插件方法
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挂脑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子欲侮,更是在濱河造成了極大的恐慌崭闲,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件威蕉,死亡現(xiàn)場離奇詭異刁俭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)韧涨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門牍戚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侮繁,“玉大人,你說我怎么就攤上這事如孝∠芰ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵第晰,是天一觀的道長锁孟。 經(jīng)常有香客問我,道長茁瘦,這世上最難降的妖魔是什么品抽? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮腹躁,結(jié)果婚禮上桑包,老公的妹妹穿的比我還像新娘。我一直安慰自己纺非,他們只是感情好哑了,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著烧颖,像睡著了一般弱左。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炕淮,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天拆火,我揣著相機(jī)與錄音,去河邊找鬼涂圆。 笑死们镜,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的润歉。 我是一名探鬼主播模狭,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼踩衩!你這毒婦竟也來了嚼鹉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤驱富,失蹤者是張志新(化名)和其女友劉穎锚赤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體褐鸥,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡线脚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酒贬。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡又憨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锭吨,到底是詐尸還是另有隱情蠢莺,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布零如,位于F島的核電站躏将,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏考蕾。R本人自食惡果不足惜祸憋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肖卧。 院中可真熱鬧蚯窥,春花似錦、人聲如沸塞帐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽葵姥。三九已至荷鼠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間榔幸,已是汗流浹背允乐。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留削咆,地道東北人牍疏。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像拨齐,于是被迫代替她去往敵國和親鳞陨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353