安装WordPress中文包四步曲

使用国外空间如Godaddy等写blog的朋友,相比都会直接安装空间商提供的英文版WordPress,理由很简单,一个按钮,就能自动完成。这样,问题就来了,如果想让blog前台后台都显示中文,那就还要装个中文包了。

方法就两步:

  1. 下载wordpress中文团队开发维护的简体中文语言包
  2. 在wordpress/wp-content下创建languages目录;
  3. 将中文包中的文件zh_CN.po与zh_CN.mo解压至此目录;
  4. 修改网站根目录下的wp-config.php文件:
    将 define(’DB_CHARSET’, ‘utf8‘); 修改为 define(’DB_CHARSET’, ”); ;
    将 define (’WPLANG’, ”); 修改为 define (’WPLANG’, ‘zh_CN’);

现在可以去刷新一下你的页面,试一下效果吧!

Position属性四个值:static、fixed、absolute和relative的区别和用法

在用CSS+DIV进行布局的时候,一直对position的四个属性值relative,absolute,static,fixed分的不是很清楚,以致经常会出现让人很郁闷的结果。今天研究了一下,总算有所了解。在此总结一下:

先看下各个属性值的定义:

1、static(静态定位):默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明)。

2、relative(相对定位):生成相对定位的元素,通过top,bottom,left,right的设置相对于其正常(原先本身)位置进行定位。可通过z-index进行层次分级。

3、absolute(绝对定位):生成绝对定位的元素,相对于 static 定位以外的第一个父元素进行定位。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。可通过z-index进行层次分级。

4、fixed(固定定位):生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。可通过z-index进行层次分级。

  • 定位属性详解

    1、relative

含义:定位为relative的元素脱离正常的文本流中,但其在文本流中的位置依然存在。

  他是默认参照父级的原始点为原始点,无父级则以文本流的顺序在上一个元素的底部为原始点,配合TRBL进行定位,当父级内有padding等CSS属性时,当前级的原始点则参照父级内容区的原始点进行定位,有以下属性:

  ① 如果没有TRBL,以父级的左上角,在没有父级的时候,他是参照浏览器左上角(到这里和absolute第一条一样),如果在没有父级元素的情况下,存在文本,则以文本的底部为原始点进行定位并将文字断开(和absolut不同)。

  ② 如果设定TRBL,并且父级没有设定position属性,仍旧以父级的左上角为原点进行定位(和absolut不同)。

  ③ 如果设定TRBL,并且父级设定position属性(无论是absolute还是relative),则以父级的左上角为原点进行定位,位置 由TRBL决定(前半段和absolute一样)。如果父级有Padding属性,那么就以内容区域的左上角为原点,进行定位(后半段和absolut不 同)。

  以上三点可以总结出,无论父级存在不存在,无论有没有TRBL,均是以父级的左上角进行定位,但是父级的Padding属性会对其影响。

综合上面对relative的叙述,我们就可以将position属性为relative的DIV视成可以用TRBL进行定位的的普通DIV,或者说 只要将我们平时布局页面的div的CSS属性中加上position:relative后,就不只是用float布局页面了,还可以用TRBL进行布局页 面 了,或者说加上position:relative的DIV也可以像普通的DIV进行布局页面了,只不过还可以用TRBL进行布局页面。但是 position属性为absolute不可以用来布局页面,因为如果用来布局的话,所有的DIV都相对于浏览器的左上角定位了,所以只能用于将某个元素 定位于属性为absolute的元素的内部某个位置。

Top的值表示对象相对原位置向下偏移的距离,bottom的值表示对象相对原位置向上偏移的距离,两者同时存在时,只有Top起作用。

left的值表示对象相对原位置向右偏移的距离,right的值表示对象相对原位置向左偏移的距离,两者同时存在时,只有left起作用。

如图1:

黄色背景的层定位为relative,红色边框区域为其在正常流中的位置。在通过top、left对其定位后,从灰色背景层的位置可以看出其正常位置依然存在

2、absolute
定位为absolute的层脱离正常文本流,但与relative的区别是其在正常流中的位置不再存在

这个属性总是有人给出误导。说当position属性设为absolute后,总是按照浏览器窗口来进行定位的,这其实是错误的。实际上,这是fixed属性的特点。

① 如果没有TRBL(top、right、bottom、left),以父级的左上角,在没有父级的时候,他是参照浏览器左上角,如果在没有父级元素的情况下,存在文本,则以它前面的最后一个文字的右上角为原点进行定位但是不断开文字,覆盖于上方。

② 如果设定TRBL,并且父级没有设定position属性,那么当前的absolute则以浏览器左上角为原始点进行定位,位置将由TRBL决定。

③ 如果设定TRBL,并且父级设定position属性(无论是absolute还是relative),则以父级的左上角为原点进行定位,位置由 TRBL决定。即使父级有Padding属性,对其也不起作用,说简单点就是:它只坚持一点,就以父级左上角为原点进行定位,父级的padding对其根 本没有影响。

以上三点可以总结出,若想把一个定位属性为absolute的元素定位于其父级元素内,只有满足两个条件:
第一:设定TRBL

第二:父级设定Position属性
上面的这个总结非常重要,可以保证你在用absolue布局页面的时候,不会错位,并且随着浏览器的大小或者显示器分辨率的大小,而不发生改变。


初学者很容易犯错的是,不清楚Position属性为absolute的板块,若想定位到父级板块中,并且当浏览器的大小改变或显示器的分辨率改变,布局不发生改变,是需要满足两个条件的,只要有一点不满足,元素就会以浏览器左上角为原点,从而导致页面布局错

位。
  Top的值表示对象上边框与浏览器窗口顶部的距离,bottom的值表示对象下边框与浏览器窗口底部的距离,两者同时存在时,只有Top起作用;如果两者都未指定,则其顶端将与原文档流位置一致,即垂直保持位置不变。

  left的值表示对象左边框与浏览器窗口左边的距离,right的值表示对象右边框与浏览器窗口右边的距离,两者同时存在时,只有left起作用;如果两者都未指定,则其左边将与原文档流位置一致,即水平保持位置不变。

在Position属性值为absolute的同时,如果有一级父对象(无论是父对象还是祖父对象,或者再高的辈分,一样)的Position属性值为Relative时,则上述的相对浏览器窗口定位将会变成相对父对象定位,这对精确定位是很有帮助的。

3、relative与absolute的主要区别
首先,是上面已经提到过的在正常流中的位置存在与否。
其次,relative定位的层总是相对于其最近的父元素,无论其父元素是何种定位方式。如图3:

图中,红色背景层为relative定位,其直接父元素绿色背景层为默认的static定位。红色背景层的位置为相对绿色背景层top、left个20元素。而如果红色背景层定位为absolute,则情形如下:

可 以看到,红色背景层依然定义top:20px;left:20px;但其相对的元素变为定位方式为absolute或relative的黄色背景层。因 此,对于absolute定位的层总是相对于其最近的定义为absolute或relative的父层,而这个父层并不一定是其直接父层。如果其父层中都未定义absolute或relative,则其将相对body进行定位,如图:

除top、left、right、bottom定位外,margin属性值的定义也符合上述规则。

总结:
属性为relative的元素可以用来布局页面,属性为absolute的元素用来定位某元素在父级中的位置,既然属性为absolute的元素用来定位某元素在父级中位置,就少不了TRBL,这时候根据一开始讲的absolute的第三条,如果父级元素没有position属性那么 absolute元素就会脱离父级元素,但是如果是布局页面,父级元素position的属性又不能为absolute,不然就会以浏览器左上角为原点 了,所以父级元素的position属性只能为relative!
如果用position来布局页面,父级元素的position属性必须为relative,而定位于父级内部某个位置的元素,最好用 absolute,因为它不受父级元素的padding的属性影响,当然你也可以用position,不过到时候计算的时候不要忘记padding的值

Note : 绝对(absolute)定位对象在可视区域之外会导致滚动条出现。而放置相对(relative)定位对象在可视区域之外,滚动条不会出现。

什么是文档流?
将窗体自上而下分成一行行, 并在每行中按从左至右的顺序排放元素,即为文档流。
只有三种情况会使得元素脱离文档流,分别是:浮动绝对定位和相对定位。

z-index属性
z-index,又称为对象的层叠顺序,它用一个整数来定义堆叠的层次,整数值越大,则被层叠在越上面,当然这是指同级元素间的堆叠,如果两个对象的此属 性具有同样的值,那么将依据它们在HTML文档中流的顺序层叠,写在后面的将会覆盖前面的。需要注意的是,父子关系是无法用z-index来设定上下关系 的,一定是子级在上父级在下。
Note:使用static 定位或无position定位的元素z-index属性是无效的。

Android导航组件Navigation

相关类图

一、前言

传统应用开发,一般都是采用一个界面一个Activity的形式,但是大家都知道,Activity在Android中是属于重量级的组件,从而导致程序资源消耗大,用户体验不佳。而导航组件Navigation采用的是Fragment轻量级的组件实现的,可以节省资源,提高用户体验。

二、导航简介

导航组件是Android Jetpack的一部分,主要用途是实现用户导航、进入和退出应用中不同内容片段的交互,不论是普通按钮点击,还是应用栏、抽屉导航栏等复杂的模式,它都能轻松应对,当然,导航组件也有它既定的导航原则来确保一致且可预测的用户体验。

2.1 导航组件的组成

导航组件主要有三部分组成:

  • 导航图(NavGraph): 这是包含所有导航相关信息的XML资源,这些信息包括应用内所有内容区域个体(称为目标,一般都是Fragment),以及用户可以通过应用跳转的可能路径。
  • 导航宿主(NavHost):这是用来显示导航图中声明的目标的一个空白容器。导航组件包含一个默认的NavHost实现(NavHostFragment),可用来显示Fragment目标。
  • 导航控制器(NavController):在NavHost中管理应用导航的目标,当用户在应用中进行操作时,导航控制器会控制目标的切换。

使用导航组件有各种优势,包括以下方面:

  • 自动处理Fragment事务
  • 在默认情况下,能够正确处理往返操作
  • 支持动画和转场动画
  • 支持导航界面模式(例如:抽屉式导航栏和底部导航栏)
  • Safe Args支持(一种可在导航和目标之间传递数据时,提供类型安全的Gradle插件)
  • ViewModel支持
  • 可以使用Android Studio的Navigation Editor来编辑和查看导航图(必须使用Android Studio 3.3及以上版本)

2.2 导航的原则

在使用导航组件时,应当遵循一些原则,以提高用户体验

注意:即使您为在项目中使用Navigation组件,您的应用也应遵循这些设计原则

2.2.1 固定的起始目的地

顾名思义,您构建的应用必须有一个固定的起始目的地,这个起始目的地就是指当应用启动时显示的第一个屏幕。起始目的地也是用户按返回按钮后,在回到启动器前看到的最后一个屏幕。

以上示例中,用户登录页面就是起始目的地,点击启动器图标打开应用,第一个启动的页面就是用户登录页面,在返回过程中,最后一个呈现的页面也是登录页面。

2.2.2 导航状态表现为目的地堆栈

在用户启动应用时,系统会启动一个新任务,并且显示起始目的地,这个起始目的地是应用导航的基础。当用户在应用中进行导航时,栈顶的目标就是显示在屏幕上的,而栈内的所有目标都是历史记录。

导航组件会为你管理所有返回栈的顺序,当然你也可以自行管理,已达到某些目的。

2.2.3 在应用的任务中向上按钮和返回按钮行为相同

首先,说下什么事向上按钮,向上按钮是指在应用中的返回上一级的按钮(一般是在用户导航栏中),返回按钮则是系统导航中的返回按钮。在应用的任务中,向上按钮和返回按钮的行为相同,都是将栈顶的目标移除,返回到上一个目标。

2.2.4 向上按钮不会退出应用

在应用的任务中,向上按钮可以返回到上一个目标,但是绝不会退出应用

2.2.5 深度链接可以模拟手动导航

无论是通过深度链接至特定的目的地,还是手动导航到特定目的地,都可以使用向上按钮通过各个目的地导航回到起始目的地。当深度链接至特定的目的地时,会移除所有返回栈中的任务,并替换为深度链接的返回栈。值得注意的是,深度链接合成的返回栈是一个完整的返回栈,他跟手动导航至特定目的地具有相同的返回栈,这个是非常重要的,因为合成的返回栈必须是真实的。

三、 使用入门

3.1 添加依赖

在应用模块目录下的build.gradle文件中添加dependencies依赖声明:

dependencies {
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'

    implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
    implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
    implementation 'androidx.navigation:navigation-dynamic-features-fragment:2.3.5'

    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    androidTestImplementation 'androidx.navigation:navigation-testing:2.3.5'
}

3.2 创建导航图资源文件

导航是发生在各个目的地之间的,而这些目的地通过操作连接在一起。导航图是一种资源文件,它包含了所有的目的地和操作的声明。

创建导航图资源文件,可以按一下不走进行:

  1. 在项目程序模块下的res目录下,右键-》New-》Android Resource File
  2. 在弹出的窗口中输入文件名
  3. 在Resource type中选择Navigation
  4. 点击确定创建资源文件

新建的导航图资源文件时一个XML资源文件,以navigation为根节点,大致内容如下:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_graph">

</navigation>

<navigation>元素是导航图的根元素。当你向图表添加目的地和连接操作时,可以看到相应的<destination>和<action>子元素。如果有嵌套图表,也会出现<navigation>子元素。

如果是首次添加导航图资源,Android Studio会在res目录内创建一个navigation资源目录,该目录包含你的导航图资源文件。当然,如果你已经够熟练,可以直接创建目录和文件的方式创建。

3.2.1 Android Studio中的Navigation Editor

Android Studio提供了强大的导航编辑器,在这里不但可以预览你所添加的目标,还可以修改导航图,可以通过拖动的方式或者直接编码修改底层XML的方式修改导航图。为了方便项目的维护和代码可读性,但更加建议使用修改底层XML的方式,或者结合修改底层XML的方式。

温馨提示:不同版本的Android Studio的界面操作有些不一样,不少从旧版本升级到3.6之后,发现打开资源文件的时候,默认是Design模式(包括layout布局资源),一时间找不到北了,不知道如何切换成修改底层XML的模式。其实这是3.6版本之后的小改动,在旧版本只有code和design两种模式,新版本code、split、design三种模式,而且模式切换位置也变了,旧版本是在左下角,而新版本是在右上角(如下图)

3.3 向Activity添加导航宿主(HavHost)

导航宿主是导航组件的核心部分之一,导航宿主是一个空容器,用来存放和处理目的地。导航宿主必须派生于NavHost,NavHostFragment是导航组件的默认宿主实现,负责处理Fragment目的地的交互。

注意:导航组件的设计理念是用于具有一个主Activity和多个Fragment目的地的应用,主Activity与导航图相关联,并且包含一个负责根据需要交换目的地的HavHostFragment。如果你的应用需要再多个Activity上实现导航,就需要为每个Activity添加导航宿主,并在每个Activity关联其自己的导航图。

3.3.1 通过XML添加NavHostFragment

在主Activity的布局文件中,添加<fragment>标签,并在内部指定导航图,如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />

    <fragment
        android:id="@+id/nav_host_fragment_activity_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/nav_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

详细解说:

  • android:name属性包含NavHost实现类的名称(示例中使用的是默认实现HAVHostFragment,如果有需要,可以使用自定义的Fragment类,但是必须实现HavHost或者继承NavHostFragment)
  • app:navGraph属性将导航宿主(NavHostFragment)与导航图关联,指向包含所有导航目的地的导航图资源文件
  • app:defaultNavHost=”true” 属性确保导航宿主会拦截系统返回按钮。请注意,只能有一个默认导航宿主,如果同一布局中有多个导航宿主,请务必仅指定一个默认导航宿主。

说明:导航组件是Android Jetpack的一部分,不属于Android系统组件,所以需要在布局中添加属性引入,如:xmlns:app="http://schemas.android.com/apk/res-auto"

除此之外,还可以使用Layout Editor向Activity添加导航宿主,具体步骤如下:

  1. 打开Activity布局文件,切换到Design窗口
  2. 在Palette窗口选择Containers,然后找到NavHostFragment(可直接搜索)
  3. 将NavHostFragment拖向布局中
  4. 在弹出的窗口中选择导航图,然后确定
  5. 在Properties窗口设置相关属性

3.3.2 向导航图中添加目的地

对于不想编码的小伙伴可以使用Navigation Editor向导航中添加目的地,因为这些都是用户引导模式的,没什么可说,在这里主要讲一下手动添加目的地的步骤:

  1. 新建Fragment类和布局文件,并实现相关逻辑代码
  2. 在导航图XML中新增<fragment>标签
  3. 配置<fragment>标签的相关属性,如:android:id、android:name、android:label等
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.shijiusui.p.ppjoke.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.shijiusui.p.ppjoke.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.shijiusui.p.ppjoke.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

目的地属性详解

  • Type:即标签名称,指示在源代码中,该目的地是作为Fragment、Activity还是其他自定义实现的
  • android:label:这个属性指定目的地的名称
  • android:id:这个属性指定该目的地的ID,用于在代码中引用该目的地
  • android:name:这个属性用来指定目的地所关联的类

除此之外,还可以通过tools:layout属性指定预览布局文件,这样就可以在导航图编辑中看到对应的布局预览了。

3.3.3 将某个目的地指定为起始目的地

导航的原则之一就是固定的起始目的地,指定起始目的地的方法有两种:

一种是使用Navigation Editor,在Design窗口中,选中需要指定为起始目的地的目标,点击“房子”图标(如下图)即可。

另一种方法就是在XML源代码中,在<navigation>标签中添加app:startDestination属性进行指定,属性值为需要指定的目的地的ID(如下示例)

示例:通过XML代码指定起始目的地

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.shijiusui.p.ppjoke.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.shijiusui.p.ppjoke.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.shijiusui.p.ppjoke.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

3.3.4 连接目的地

目的地之间的逻辑连接也叫做操作,操作一般是将一个目的地连接到另一个目的地,当然,你也可以定义全局操作,这类操作可以在任意位置跳转到指定目的地,这个在后面详细讲到。

我们可以使用Navigation Editor连接两个目的地,直接拖动箭头即可,在这里就不多介绍这种方式,直接介绍通过修改XML源码的方式(其实使用Navigation Editor也会自动修改XML源码),具体步骤如下:

  1. 在<fragment>标签内部新增<action>标签
  2. 配置android:id和app:destnation属性
  3. 如果需要,可以配置app:enterAnim、app:exitAnim、app:popEnterAnim、app:popExitAnim属性定义动画

详细解说:

  • Type:即<action>标签
  • android:id:这个字段是操作ID,代码中通过这个ID执行操作
  • app:destnation:这个字段是操作的目的地,用来指定操作跳转的目的地
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.shijiusui.p.ppjoke.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_dashboard"
            app:destination="@+id/navigation_dashboard"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
            app:popExitAnim="@anim/nav_default_pop_exit_anim" />
    </fragment>

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.shijiusui.p.ppjoke.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.shijiusui.p.ppjoke.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

 

3.3.5 导航到目的地

完成了导航图的各种配置,那么就需要在代码中实现导航到目的地了。导航到目的地是使用NavController完成,这是在导航宿主中管理导航的对象的,每个导航宿主都有自己的相应导航控制器(NavController)。导航到目的地的步骤如下:

  1. 检索导航控制器
  2. 导航到目的地

3.3.5.1 检索导航控制器

检索导航宿主的导航控制器,可以通过以下方法:

Kotlin:

  • Fragment.findNavController()
  • View.findNavController()
  • Activity.findNavController(viewId: int)

Java:

  • NavHostFragment.findNavController(Fragment)
  • Navigation.findNavController(Activity, int viewId)
  • Navigation.findNavController(View)

说明:Kotlin可以直接在Fragment、View以及Activity使用findNavController是因为使用了扩展方法,当然,也可以直接跟Java那样调用对应的接口。

class HomeFragment : Fragment() {

    private lateinit var homeViewModel: HomeViewModel
    private var _binding: FragmentHomeBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        homeViewModel =
            ViewModelProvider(this).get(HomeViewModel::class.java)

        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        val root: View = binding.root

        val textView: TextView = binding.textHome
        homeViewModel.text.observe(viewLifecycleOwner, Observer {
            textView.text = it
        })

        textView.setOnClickListener { 
            val navController = findNavController()
        }
        
        return root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

3.3.5.2 导航到目的地

检索到导航控制器之后,使用导航控制器类的NavController.navigation() API导航到指定的目的地,NavController.navigation()有多个变体,这里就以使用目的地ID进行导航为例:

class HomeFragment : Fragment() {

    private lateinit var homeViewModel: HomeViewModel
    private var _binding: FragmentHomeBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        homeViewModel =
            ViewModelProvider(this).get(HomeViewModel::class.java)

        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        val root: View = binding.root

        val textView: TextView = binding.textHome
        homeViewModel.text.observe(viewLifecycleOwner, Observer {
            textView.text = it
        })

        textView.setOnClickListener { 
            val navController = findNavController()
            navController.navigate(R.id.action_homeFragment_to_dashboard)
        }
        
        return root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

3.3.5.3 返回到指定目的地

返回到指定目的地,是指返回到之前导航过的目的地,这些目的地必须是在任务栈内的,可以通过NavController.popBackStack()接口返回上一级,或者通过NavController.popBackStack(int destinationId, boolean inclusive)返回到指定某个目的地。

到这里,已经基本掌握了导航组件的使用,后续的将会进行更加深入地介绍导航组件的使用。

四、进阶之路

4.1 创建不同类型的导航目的地

在导航图中,导航目的地不局限与Fragment,其实还可以是Activity、DialogFragment甚至前台的导航图navigation(即<navigation>内部再嵌套一个<navigation>)。详细的添加导航目的地的方法已经在3.3.2 向导航图中添加目的地详细说明了,这里就不再累赘。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.shijiusui.p.ppjoke.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_dashboard"
            app:destination="@+id/navigation_dashboard"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
            app:popExitAnim="@anim/nav_default_pop_exit_anim" />
    </fragment>
    
    <dialog 
        android:id="@+id/tips"
        ...
        />
    <activity 
        android:id="@+id/xxxx"
        ...
        />
</navigation>

注意:

  1. 如果目的地是Activity类型,转场动画必须结合Activity处理,仅仅在导航连接目的地中声明动画,弹出动画将达不到预期的效果(参考:将弹出动画应用与Activity目的地过度)
  2. 如果目的地是Activity类型,实际上就是已经离开了当前的导航组件范围
  3. 如果使用<dialog>声明导航目的地,必须是DialogFragment类型(包括其子类)

4.2 嵌套导航图

所谓的嵌套导航图,就是在导航图内再嵌入一个导航图,外部的称为父导航图,内部的叫子导航图。嵌套的导航图封装着自己的目的地,且必须标识起始目的地,父导航图访问子导航图只能通过子导航图的起始目的地(不能直接访问子导航图中的目的地),因为子导航图拥有不一样的导航控制器(NavController)。使用嵌套导航图可以对导航目的地进行分类封装,防止错误的访问。

嵌套导航图有两种表现形式,一种是在导航图<navigation>标签内部嵌套一个<navigation>标签;另一种是使用include标签引入导航图资源文件。

注意事项:

  1. 两种表现形式效果是一样的,如果导航图比较复杂,使用第二种会使得导航图资源显得更加简洁
  2. 父导航图中访问子导航图,不能直接访问子导航图中的目的地,只能通过子导航图ID访问子导航图的起始目的地

4.2.1 在导航图<navigation>标签内部嵌套一个<navigation>标签

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <navigation
        android:id="@+id/Settings"
        app:startDestination="@id/settingsFragment">
        ....
    </navigation>
</navigation>

4.2.2 使用include标签引入导航图资源文件

  • 定义一个导航图资源文件(nav_graph_settings.xml)
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/settingsFragment">

    ...
    
</navigation>
  • 在主导航图中使用导航图资源
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_home">
    
    <include app:graph="@navigation/nav_graph_settings" />

</navigation>

4.3 全局操作

操作就是目的地之间的跳转(<action>),全局操作就是指在导航图内所有目的地都能执行的操作。通常我们在目的地标签内部声明操作(<action>),但是全局操作是在导航图内声明(<navigation>标签下)。全局操作的使用跟普通的操作一样,不同的是它可以在当前导航图下所有的目的地内都可以使用。

注意事项:

  1. 全局操作只能再同一导航图内的目的地中调用,不可在所声明的导航图外部使用,即使是子导航图中的目的地也不允许
  2. 全局操作的目的地必须是当前导航图下的目的地或子导航图入口(子导航图中的目的地也是不允许的)
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_graph">

    <!-- 
        全局操作,在navigation标签下声明
        目的地必须是当前导航图下的目的地或子导航图入口
     -->
    <action android:id="@+id/action_to_settings_more"
        app:destination="@+id/commonFragment"
        ...
        />

    <fragment
        android:id="@+id/commonFragment"
        ...
        />

</navigation>

4.4 在目的地之间传递数据

导航支持你通过定义目的地参数传递数据附加到导航操作,在不同目的地之间实现数据传递。

提示:建议仅在目的地之间传递最少量的数据,因为在Android上用于保存所有状态的总空间是有限的,如果需要传递大量数据,可以考虑其他替代方案。

4.4.1 定义参数

在导航图的操作中可以定义参数,在操作中定义的参数,有几个属性:

  • android:name: 参数名
  • android:defaultValue: 参数默认值
  • app:argType: 参数类型
  • app:nullable: 是否可为空
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph_login"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/loginFragment"
        android:name="com.shijiusui.p.ppjoke.navigationdemo.LoginFragment"
        android:label="LoginFragment"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/action_loginFragment_to_registerFragment"
            app:destination="@+id/registerFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
        <argument
            android:name="type"
            app:argType="integer"
            android:defaultValue="0"
            app:nullable="false"/>
        <argument
            android:name="uname"
            app:argType="string"
            android:defaultValue="@null"
            app:nullable="true"/>

        <deepLink
            android:id="@+id/settingsDeepLink"
            app:uri="https://blog.shijiusui.com/?type={type}&amp;uname={uname}"
            android:autoVerify="false"/>
    </fragment>

</navigation>

导航库支持的参数类型包括:

类型 app:argType语法 是否支持默认值 是否支持null值
整数 app:argType=”integer”
浮点数 app:argType=”float”
长整数 app:argType=”long”
默认值必须始终以“L”后缀结尾
例如:“123L”
布尔值 app:argType=”boolean”
“true”或“false”
字符串 app:argType=”string”
资源引用 app:argType=”reference”
默认值必须为“@resourceType/resourceName”格式
例如:“@style/myCustomStyle”或“0”
自定义Parcelable app:argType=””
其中是Parcelable的完全限定类名称
支持默认值“@null”
不支持其他默认值
自定义Serializable app:argType=””
其中是Serializable的完全限定类名称
支持默认值“@null”
不支持其他默认值
自定义Enum app:argType=””
其中是Enum的完全限定名称

默认值必须与非限定名称匹配
例如:“SUCCESS”匹配MyEnum.SUCCESS

注意:

  1. 如果参数传递的是Parcelable、Serializable和Enum时,注意所传递的参数类型的类在代码混淆时需要做排除处理
  2. <argument>可以在<fragment>和<action>标签内,如果需要通过深层链接传参,务必配置在<fragment>标签内声明(更多信息参考:创建隐式深层链接)

4.4.2 在导航时传递参数

在导航时,也可以实现在目的地之间传递参数,只需要调用带有参数传递的NaviController.navigate()接口即可。

val navController = findNavController()
navController.navigate(R.id.action_homeFragment_to_loginFragment, Bundle().also {
    it.putInt("type", 2)
})

注意事项:

  1. 在导航图的操作中定义的参数,参数值是固定的,但是这个值可以被导航时传递的参数覆盖
  2. 导航的参数传递是单向的,无法实现往回传递,如果需要往回传递参数,可以通过目的地所有者Activity进行

4.5 在目的地之间添加动画效果

导航支持在目的地之间添加动画,以提高用户体验。导航动画在定义操作是添加,动画包括以下类型:

  • app:enterAnim: 进入目的地的动画(新的目的地进入的动画)
  • app:exitAnim: 退出目的地的动画(新的目的地进入,旧目的地退出的动画)
  • app:popEnterAnim: 通过弹出操作进入的目的地的动画(弹出操作时,进入的目的地进入的动画)
  • app:popExitAnim:通过弹出操作退出的目的地的动画(弹出操作时,退出的目的地退出的动画)
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph_login"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.shijiusui.p.ppjoke.navigationdemo.HomeFragment"
        android:label="HomeFragment"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_loginFragment"
            app:destination="@+id/loginFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" >
        </action>
    </fragment>

4.5.1 将弹出动画应用于Activity目的地过度

当导航的目的地是Activity类型时,,在操作添加的过度动画,会出现进入(打开Activity)时动画正常,但是返回时(从Activity中返回)的动画却失效了。针对目的地是Activity类型时,需要对弹出动画做特殊处理,你需要重写Activity的finish()方法,在内部调用ActivityNaviagtor.applyPopAnimationsToPendingTransition(Activity)接口即可。

    override fun finish() {
        super.finish()
        ActivityNavigator.applyPopAnimationsToPendingTransition(this)
    }

4.6 为目的地创建深层链接

在Android中,深层链接是指将用户直接转到应用内特定目的地的链接。借助导航组件,你可以创建两种不同类型的深层链接:显式深层链接和隐式深层链接。

4.6.1 创建显式深层链接

显式深层链接是深层链接的一个实例,该实例使用PendingIntent将用户转到应用内的特定位置(例如,可以在通知、应用快捷方式或应用微件中显式深层链接)。

当用户通过显式深层链接打开应用时,任务返回堆栈会被清除,并被替换为相应的深层链接目的地。当嵌套图表时,每个嵌套级别的起始目的地(即层次结构中每个<navigation>元素的起始目的地)也会添加到相应堆栈中。也就是说,当用户从深层链接目的地按下返回按钮时,就像从应用入口一步步进入到指定目的地的返回一样的效果。

创建显式深层链接,可以使用NavDeepLinkBuilder类构建PendingIntent,如下示例:

val pendingIntent = NavDeepLinkBuilder(requireContext())
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.accountSettingFragment)
    .createPendingIntent()

如果已有NavController,则还可以通过NavController.createDeepLink() API创建深层链接,如下所示:

val pendingIntent = findNavController()
    .createDeepLink()
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.accountSettingFragment)
    .createPendingIntent()

注意事项:

  1. 第一种创建显式深层链接的方式中,如果提供的上下文不是Activity,构造函数会使用PackageManager.getLaunchIntentForPackage()作为默认Activity来启动(如果有)
  2. 显式深层链接生成的对象时PendingIntent,适合的场景有通知、快捷方式启动、桌面小部件等

显式深层链接的使用示例(在通知中使用):

private fun showNotification(context: Context) {
    var notificationManager:NotificationManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        notificationManager = context.getSystemService<NotificationManager>(NotificationManager::class.java)
        if (null != notificationManager) {
            val channel = NotificationChannel("default", "default", NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationManager.createNotificationChannel(channel)
        }
    } else {
        notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    }

    val pendingIntent = findNavController().createDeepLink()
        .setGraph(R.navigation.nav_graph)
        .setDestination(R.id.accountSettingFragment)
        .createPendingIntent()

    val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, "default")
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentTitle("测试深层链接")
        .setContentText("测试显示深层链接打开应用")
        .setContentIntent(pendingIntent) //                .setVibrate(new long[] { 1000, 1000, 1000, 1000, 1000 })
        .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
        .setAutoCancel(true)


    notificationManager.notify(1, builder.build())
}

4.6.2 创建隐式深层链接

隐式深层链接是指应用中特定目的地的URI。调用URI(例如用户点击某个链接)时,Android可以将应用打开并自动导航到相应的目的地。

当用户触发隐式深层链接时,返回堆栈的状态取决于是否使用Intent.FLAG_ACTIVITY_NEW_TASK标记启动隐式Intent:

  • 如果改标记已设置,则任务返回堆栈会被清除,并被替换为相应的深层链接指定的目的地。就像显示深层链接,当嵌套图表时,每个嵌套级别的起始目的地(即层次结构中每个<navigation>元素的起始目的地)也会添加到相应堆栈中。也就是说,当用户从深层链接目的地按下返回按钮时,就像从应用入口一步步进入到指定目的地的返回一样的效果
  • 如果该标记未设置,则仍然位于上一个应用的任务堆栈中,该应用中的隐式深层链接已触发。在这种情况下,如果按下返回按钮,则会返回到上一个应用;如果按下向上按钮,则会在导航图中的层次父级目的地上启动应用的任务。

创建隐式深层链接请按下面步骤:

第一步:在导航图中添加隐式深层链接声明

在导航图中添加隐式深层链接声明,只需要再导航图内目的地中添加<deepLink>标签,配置相关属性:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph_login"
    app:startDestination="@id/homeFragment">
    
    <fragment
        android:id="@+id/loginFragment"
        android:name="com.shijiusui.p.ppjoke.navigationdemo.LoginFragment"
        android:label="LoginFragment"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/action_loginFragment_to_registerFragment"
            app:destination="@+id/registerFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
        <argument
            android:name="type"
            app:argType="integer"
            android:defaultValue="0"
            app:nullable="false"/>
        <argument
            android:name="uname"
            app:argType="string"
            android:defaultValue="@null"
            app:nullable="true"/>

        <deepLink
            android:id="@+id/settingsDeepLink"
            app:uri="https://blog.shijiusui.com/?type={type}&amp;uname={uname}"
            android:autoVerify="false"/>
    </fragment>

</navigation>

<deepLink>标签属性说明:

  • android:id:深层链接ID
  • app:uri:深层链接Uri
  • android:autoVerify:要求Google验证你是相应URI的所有者(可选),API 23开始有效

创建隐式深层链接需要注意的几点:

  • 没有协议的URI会假定为同时支持http和https。例如blog.shijiusui.com,会同时和http://blog.shijiusui.com与https://blog.shijiusui.com匹配
  • 深层链接的后缀中可以包含形式为{placeholder_name}的占位符,用来匹配一个或多个字符。例如,https://blog.shijiusui.com/users/{id}与https://blog.shijiusui.com/9匹配。导航件通过将占位符与深层链接所指向的目的地中已定义的参数相匹配,并尝试将占位符值解析为相应的类型。如果目的地中没有定义具有相同名称的参数,则使用默认的String类型传参数。
  • 在深层链接的后缀中,可以使用.*通配符匹配0个或多个字符
  • 如果目的地参数列表中定义了不能为null的参数,则深层链接必须包含该参数且不能为空,否则打开应用会有异常

第二步:启动导航图中的隐式深层链接

声明了隐式深层链接,接下来必须启用隐式深层链接,在应用的AndroidManifest.xml文件中,在导航图所关联的<activity>声明中添加<nav-graph>元素,如下所示:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!-- 启用导航图中的隐式深层链接 -->
    <nav-graph android:value="@navigation/nav_graph" />
</activity>

构建项目时,导航件会将<nav-graph>元素进行转换生成<intent-filter>元素,以匹配导航图中的所有深层链接。

注意事项:

  1. 启用隐式深层链接必须在导航图关联的Activity声明中进行
  2. <nav-graph>元素在Android Studio 3.0或3.1中不支持,使用这些版本时,必须改为手动添加intent-filter元素

第三步:测试隐式深层链接

隐式深层链接是URI,可以编写一个含有深层链接跳转的html文件到存储中,使用浏览器访问该html文件,如下所示:

<!DOCTYPE html>
<head>
    <meta charset="UTF-8" />
    <meta id="viewport" 
          name="viewport" 
          content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,minimal-ui">
</head>
<html>
    <input type="button" 
           value="点击我打开Deeplink" 
           onclick="javascrtpt:window.location.href='https://blog.shijiusui.com/?type=1&uname=sjs'">
</html>

将以上文件存储到手机存储中,在浏览器的地址中输入file:///<html_path>(<html_path>为html文件再存储中的路径,例如:file:///sdcard/test_deeplink.html),访问html之后(如下图),点击按钮就可以打开应用并进入到相应的目的地。

注意事项:

  1. 如果深层链接中使用的协议头在其他应用中也声明了,打开深层链接系统时,可能会弹出应用选择列表,这个问题可以在定义深层链接时,通过定义应用独有的协议头规避
  2. 如果导航组件自动生成的intent-filter无法正确进入到目的地,请确认深层链接是否跟自动生成的intent-filter一致,如果不一致,可以修改深层链接或采用手动添加intent-filter(如果遇到了https://blog.shijiusui.com的深层链接,生成的intent-filter中包含了android:path=”/” 导致无法正确访问到指定目的地,修改深层链接为 https://blog.shijiusui.com/,就能解决)。

手动添加intent-filter元素示例:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!-- 启用导航图中的隐式深层链接 -->
<!--    <nav-graph android:value="@navigation/nav_graph" />-->
    <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:scheme="https"
            android:host="blog.shijiusui.com"
            android:path="/" />
    </intent-filter>
</activity>

总结:

导航组件有非常大的优势,不但使用轻量级Fragment,还能在使用深层链接打开应用时,能自动构建返回栈(如果是隐式深层链接,启动务必使用Intent.FLAG_ACTIVITY_NEW_TASK)。

P1-Race walking

Race walking shares many fitness benefits with running, research shows, while most likely contributing to fewer injuries. It does, however, have its own problem.

翻译
尽管很可能竞走对身体的损伤更少,但是研究表明,竞走和跑步一样对健身有很多好处,然而它确实也有自身的问题

Race walkers are conditioned athletes. The longest track and field event at the Summer Olympics is the 50-kilometer race walk, which is about five miles longer than the marathon. But the sport’s rules require that a race walker’s knees stay straight through most of the leg swing and one foot remain in contact with the ground at all times. It’s this strange form that makes race walking such an attractive activity, however, says Jaclyn Norberg, an assistant professor of exercise science at Salem State University in Salem, Mass.

翻译
竞走运动员是受规则限制的运动员。夏季奥运会上最长的田径比赛项目是50公里竞走,比马拉松长约5英里。但这项运动的规则要求竞走运动员在几乎整个摆腿过程中让膝盖保持笔直,并且需要一只脚一直与地面保持接触状态。不过,麻省塞勒姆州立大学运动科学助理教授贾克林·诺伯格说,正是这种奇特的形式使得竞走成为一项如此吸引人的活动。

Like running, race walking is physically demanding, she says, According to most calculations, race walkers moving at a pace of six miles per hour would burn about 800 calories per hour, which is approximately twice as many as they would burn walking, although fewer than running, which would probably burn about 1,000 or more calories per hour.

翻译
她说,和跑步一样,竞走也需要体能。根据大多数的计算结果,竞走运动员以每小时6英里的速度竞走,每小时大约会消耗800卡路里,尽管比跑步的消耗要少,但却大约是走路时卡路里消耗量的两倍,跑步可能每小时消耗1000卡路里或更多。

However, race walking does not pound the body as much as running does, Dr. Norberg says. According to her research, runners hit the ground with as much as four times their body weight per step, while race walkers, who do not leave the ground, create only about 1.4 times their body weight with each step.

As a result, she says, some of the injuries associated with running, such as runner’s knee, are uncommon among race walkers. But the sport’s strange form does place considerable stress on the ankles and hips, so people with a history of such injuries might want to be cautious in adopting the sport. In fact, anyone wishing to try race walking should probably first consult a coach or experienced racer to learn proper technique, she says. It takes some practice.

继续阅读P1-Race walking

摄像头预览 三种方式

摄像头预览有3种方式:

  1. setPreviewDisplay(holder)
  2. setPreviewTexture(surfaceTexture)
  3. 自定义

第一种会给摄像头绑定一个视窗(最终的型态是一个Surface,Egl会对转换成Native的Window与之对应),摄像头采集完数据,再绘制到视窗上,这都是操作系统自发处理的。
第二种是输出到纹理,这个纹理可以做一些处理,加上其他一些OpenGL的操作,最终输出到视窗上。
第三种,也可以使用自定义的方式通过PreviewCallback拿到Frame数据,装换到2D纹理,再输出到视窗。
后两者是可以进行自定义的OpenGL渲染的,比如特效,滤镜等。

Android 推流–camera后置切前置不生效

七牛android端推流的摄像头id (CAMERA_FACING_ID) 目前一共有三种模式:

CAMERA_FACING_ID.CAMERA_FACING_FRONT ==> 前置摄像头

CAMERA_FACING_ID.CAMERA_FACING_BACK ==> 后置摄像头

CAMERA_FACING_ID.CAMERA_FACING_3RD ==> 后置副摄像头,即双摄像头的副摄像头

目前开发者会遇到某些机型,后置摄像头切换之前不成功的情况,这一类问题确认下来是

该机型只有后置摄像头(需要前置的话,直接翻转摄像头)
eg. 华为荣耀7i
所以可以针对这类机型,在调用camera切换的API的时候,给予用户一个提示信息即可。

Android高版本联网失败报错:Cleartext HTTP traffic to xxx not permitted解决方法

2020-09-08 18:29:53.519 18200-18200/com.shijiusui.p.screenlive I/Glide: Root cause (1 of 1)
     java.io.IOException: Cleartext HTTP traffic to images.cdn.xxxxxx.com not permitted
         at com.android.okhttp.HttpHandler$CleartextURLFilter.checkURLPermitted(HttpHandler.java:115)
         at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:458)
         at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:127)
         at com.bumptech.glide.load.data.HttpUrlFetcher.loadDataWithRedirects(HttpUrlFetcher.java:104)
         at com.bumptech.glide.load.data.HttpUrlFetcher.loadData(HttpUrlFetcher.java:59)
         at com.bumptech.glide.load.model.MultiModelLoader$MultiFetcher.loadData(MultiModelLoader.java:100)
         at com.bumptech.glide.load.model.MultiModelLoader$MultiFetcher.startNextOrFail(MultiModelLoader.java:164)
         at com.bumptech.glide.load.model.MultiModelLoader$MultiFetcher.onLoadFailed(MultiModelLoader.java:154)
         at com.bumptech.glide.load.data.HttpUrlFetcher.loadData(HttpUrlFetcher.java:65)
         at com.bumptech.glide.load.model.MultiModelLoader$MultiFetcher.loadData(MultiModelLoader.java:100)
         at com.bumptech.glide.load.engine.SourceGenerator.startNext(SourceGenerator.java:62)
         at com.bumptech.glide.load.engine.DecodeJob.runGenerators(DecodeJob.java:309)
         at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:279)
         at com.bumptech.glide.load.engine.DecodeJob.run(DecodeJob.java:235)
         at java.util.concurrent.ThreadPoolExecutor.processTask(ThreadPoolExecutor.java:1187)
         at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
         at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
         at java.lang.Thread.run(Thread.java:784)

前言:为保证用户数据和设备的安全,Google针对下一代 Android 系统(Android P) 的应用程序,将要求默认使用加密连接,这意味着 Android P 将禁止 App 使用所有未加密的连接,因此运行 Android P 系统的安卓设备无论是接收或者发送流量,未来都不能明码传输,需要使用下一代(Transport Layer Security)传输层安全协议,而 Android Nougat 和 Oreo 则不受影响。

因此在Android P 使用HttpUrlConnection进行http请求会出现以下异常

W/System.err: java.io.IOException: Cleartext HTTP traffic to **** not permitted

使用OKHttp请求则出现

java.net.UnknownServiceException: CLEARTEXT communication ** not permitted by network security policy

在Android P系统的设备上,如果应用使用的是非加密的明文流量的http网络请求,则会导致该应用无法进行网络请求,https则不会受影响,同样地,如果应用嵌套了webview,webview也只能使用https请求。

针对这个问题,有以下三种解决方法:

(1)APP改用https请求

(2)targetSdkVersion 降到27以下

(3)更改网络安全配置

前面两个方法容易理解和实现,具体说说第三种方法,更改网络安全配置。

1.在res文件夹下创建一个xml文件夹,然后创建一个network_security_config.xml文件,文件内容如下:

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <network-security-config>
  3. <base-config cleartextTrafficPermitted=”true” />
  4. </network-security-config>

2.接着,在AndroidManifest.xml文件下的application标签增加以下属性:

  1. <application
  2. android:networkSecurityConfig=”@xml/network_security_config”
  3. />

完成,这个时候App就可以访问网络了。

方法四:在AndroidManifest.xml配置文件的<application>标签中直接插入

android:usesCleartextTraffic=”true”

error=86, Bad CPU type in executable

最近在维护一个N久的项目时,发现在mac升级系统为10.15.5后(Android Studio 4.0,gradle 2.3.1),编译失败了,报错如下:

Cannot run program “/Users/xxxx/Android/sdk/build-tools/23.0.1/aapt”: error=86, Bad CPU type in executable

原因是最新版本的macOS Catalina(10.15.5)已经不支持32位的应用了,只能运行64位的应用。

解决方法:升级工程的buildToolsVersion,本例中将23.0.1 升级成25.0.3

leakcanary内存泄露检测工具报错 Dumping memory, app will freeze. Brrr

报错信息:Dumping memory, app will freeze. Brrr

点击查看leaks小黄标,看到报错信息:
java.lang.UnsupportedOperationException: Could not find char array in java.lang.String@334750520 (0x13f3e338)
at com.squareup.leakcanary.HahaHelper.asString(HahaHelper.java:108)
at com.squareup.leakcanary.HeapAnalyzer.findLeakingReference(HeapAnalyzer.java:161)
at com.squareup.leakcanary.HeapAnalyzer.checkForLeak(HeapAnalyzer.java:115)
at com.squareup.leakcanary.internal.HeapAnalyzerService.onHandleIntent(HeapAnalyzerService.java:58)
at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:76)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.os.HandlerThread.run(HandlerThread.java:65)

查看我本地的集成信息:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'

原因是我leakcanary集成版本过低,推荐使用1.5.4或者1.6版本

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6'
testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6'

修改以后重新运行,一切OK,能成功检测到内存泄漏。

SSH上传本地文件到linux服务器

在linux下一般用scp这个命令来通过ssh传输文件。

1、从服务器上下载文件

scp username@servername:/path/filename /var/www/local_dir(本地目录)

 例如scp root@192.168.0.101:/var/www/test.txt  #把192.168.0.101上的/var/www/test.txt 的文件下载到/var/www/local_dir(本地目录)

2、上传本地文件到服务器

scp /path/filename username@servername:/path

例如scp /var/www/test.php root@192.168.0.101:/var/www/  #把本机/var/www/目录下的test.php文件上传到192.168.0.101这台服务器上的/var/www/目录中

3、从服务器下载整个目录

scp -r username@servername:/var/www/remote_dir/(远程目录) /var/www/local_dir(本地目录)

例如:scp -r root@192.168.0.101:/var/www/test  /var/www/  

4、上传目录到服务器

scp  -r local_dir username@servername:remote_dir

例如:scp -r test  root@192.168.0.101:/var/www/   #把当前目录下的test目录上传到服务器的/var/www/ 目录

注:目标服务器要开启写入权限。

配置 Docker + gitlab-runner 实现线上自动编译

Last login: Mon Apr 20 15:44:24 on ttys002
xxxs-MacBook-Pro:~ kenny$ ssh -l kenny 10.211.55.3
kenny@10.211.55.3's password: 
Last login: Sun Apr 26 14:41:31 2020
[kenny@centos-linux ~]$ ls
android  Desktop  Documents  Downloads  fontconfig  gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm  hhhha.txt  Music  Pictures  Public  Templates  Videos
[kenny@centos-linux ~]$ uname -r
3.10.0-1062.el7.x86_64
[kenny@centos-linux ~]$ sudo yum update
[sudo] kenny 的密码:
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirror.shastacoe.net
 * extras: mirror.teklinks.com
 * updates: ftp.usf.edu
.
.
.

完毕!
[kenny@centos-linux ~]$ sudo yum remove docker  docker-common docker-selinux docker-engin
已加载插件:fastestmirror, langpacks
参数 docker 没有匹配
参数 docker-common 没有匹配
参数 docker-selinux 没有匹配
参数 docker-engin 没有匹配
不删除任何软件包
[kenny@centos-linux ~]$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirror.fileplanet.com
 * extras: mirrors.xmission.com
 * updates: centos.mirror.constant.com
软件包 yum-utils-1.1.31-52.el7.noarch 已安装并且是最新版本
软件包 device-mapper-persistent-data-0.8.5-1.el7.x86_64 已安装并且是最新版本
软件包 7:lvm2-2.02.185-2.el7_7.2.x86_64 已安装并且是最新版本
无须任何处理
[kenny@centos-linux ~]$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
已加载插件:fastestmirror, langpacks
adding repo from: https://download.docker.com/linux/centos/docker-ce.repo
grabbing file https://download.docker.com/linux/centos/docker-ce.repo to /etc/yum.repos.d/docker-ce.repo
repo saved to /etc/yum.repos.d/docker-ce.repo
[kenny@centos-linux ~]$ yum list docker-ce --showduplicates | sort -r
已加载插件:fastestmirror, langpacks
可安装的软件包
 * updates: mirrors.ocf.berkeley.edu
 * extras: mirrors.xmission.com
docker-ce.x86_64            3:19.03.8-3.el7                     docker-ce-stable
docker-ce.x86_64            3:19.03.7-3.el7                     docker-ce-stable
docker-ce.x86_64            3:19.03.6-3.el7                     docker-ce-stable
.
.
.
docker-ce.x86_64            17.03.0.ce-1.el7.centos             docker-ce-stable
Determining fastest mirrors
 * base: mirror.keystealth.org
[kenny@centos-linux ~]$ sudo yum install docker-ce-18.03.1.ce
已加载插件:fastestmirror, langpacks
.
.
.                                                                                                                                                                                                                                                                                                                            

作为依赖被安装:
  container-selinux.noarch 2:2.107-3.el7                                                                                                                                                  pigz.x86_64 0:2.3.3-1.el7.centos                                                                                                                                                 

完毕!
[kenny@centos-linux ~]$ sudo systemctl start docker
[kenny@centos-linux ~]$ sudo systemctl enable docker
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.
[kenny@centos-linux ~]$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  6904  100  6904    0     0   6197      0  0:00:01  0:00:01 --:--:--  6203
Detected operating system as centos/7.
Checking for curl...
Detected curl...
Downloading repository file: https://packages.gitlab.com/install/repositories/runner/gitlab-runner/config_file.repo?os=centos&dist=7&source=script
done.
Installing pygpgme to verify GPG signatures...
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: repos.lax.quadranet.com
 * extras: mirrors.xmission.com
 * updates: bay.uchicago.edu
runner_gitlab-runner-source/signature                                                                                                                                                                                                                                                                                                                |  862 B  00:00:00     
从 https://packages.gitlab.com/runner/gitlab-runner/gpgkey 检索密钥
导入 GPG key 0x51312F3F:
 用户ID     : "GitLab B.V. (package repository signing key) <packages@gitlab.com>"
 指纹       : f640 3f65 44a3 8863 daa0 b6e0 3f01 618a 5131 2f3f
 来自       : https://packages.gitlab.com/runner/gitlab-runner/gpgkey
从 https://packages.gitlab.com/runner/gitlab-runner/gpgkey/runner-gitlab-runner-366915F31B487241.pub.gpg 检索密钥
runner_gitlab-runner-source/signature                                                                                                                                                                                                                                                                                                                |  951 B  00:00:00 !!! 
runner_gitlab-runner-source/primary                                                                                                                                                                                                                                                                                                                  |  175 B  00:00:02     
软件包 pygpgme-0.3-9.el7.x86_64 已安装并且是最新版本
无须任何处理
Installing yum-utils...
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirror.centos.lax1.serverforge.org
 * extras: mirrors.xmission.com
 * updates: mirrors.ocf.berkeley.edu
软件包 yum-utils-1.1.31-52.el7.noarch 已安装并且是最新版本
无须任何处理
Generating yum cache for runner_gitlab-runner...
导入 GPG key 0x51312F3F:
 用户ID     : "GitLab B.V. (package repository signing key) <packages@gitlab.com>"
 指纹       : f640 3f65 44a3 8863 daa0 b6e0 3f01 618a 5131 2f3f
 来自       : https://packages.gitlab.com/runner/gitlab-runner/gpgkey
Generating yum cache for runner_gitlab-runner-source...

The repository is setup! You can now install packages.
[kenny@centos-linux ~]$ sudo yum install gitlab-runner
[sudo] kenny 的密码:
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: repos.lax.quadranet.com
 * extras: mirrors.xmission.com
 * updates: centos.mirror.constant.com
runner_gitlab-runner/x86_64/signature                                                                                                                                                                                                                                                                                                                |  862 B  00:00:00     
runner_gitlab-runner/x86_64/signature                                                                                                                                                                                                                                                                                                                | 1.0 kB  00:00:00 !!! 
runner_gitlab-runner-source/signature                                                                                                                                                                                                                                                                                                                |  862 B  00:00:00     
runner_gitlab-runner-source/signature                                                                                                                                                                                                                                                                                                                |  951 B  00:00:00 !!! 


完毕!
[kenny@centos-linux ~]$ sudo gitlab-runner register
[sudo] kenny 的密码:
Runtime platform                                    arch=amd64 os=linux pid=29719 revision=ce065b93 version=12.10.1
Running in system-mode.                            
                                                   
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
http://10.211.55.3:8888/
Please enter the gitlab-ci token for this runner:
qusy3u_yhLNZzMBHgz9p
Please enter the gitlab-ci description for this runner:
[centos-linux.shared]: android test runner
Please enter the gitlab-ci tags for this runner (comma separated):
android
Registering runner... succeeded                     runner=qusy3u_y
Please enter the executor: custom, docker-ssh, virtualbox, docker+machine, docker-ssh+machine, docker, parallels, shell, ssh, kubernetes:
docker
Please enter the default Docker image (e.g. ruby:2.6):
jangrewe/gitlab-ci-android
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded! 
[kenny@centos-linux ~]$ cd /etc/gitlab-runner/
-bash: cd: /etc/gitlab-runner/: 权限不够
[kenny@centos-linux ~]$ su
密码:
[root@centos-linux kenny]# cd /etc/gitlab-runner/
[root@centos-linux gitlab-runner]# ls
config.toml
[root@centos-linux gitlab-runner]# vi config.toml 
[root@centos-linux gitlab-runner]# cd -
/home/kenny
[root@centos-linux kenny]# pwd
/home/kenny
[root@centos-linux kenny]# vi /etc/gitlab-runner/config.toml 
[root@centos-linux kenny]# ls
android  Desktop  Documents  Downloads  fontconfig  gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm  hhhha.txt  Music  Pictures  Public  Templates  Videos
[root@centos-linux kenny]# pwd
/home/kenny
[root@centos-linux kenny]# exit
exit
[kenny@centos-linux ~]$ pwd
/home/kenny
[kenny@centos-linux ~]$ ls
android  Desktop  Documents  Downloads  fontconfig  gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm  hhhha.txt  Music  Pictures  Public  Templates  Videos
[kenny@centos-linux ~]$ mkdir android-cache
[kenny@centos-linux ~]$ cd android-cache/
[kenny@centos-linux android-cache]$ ls
[kenny@centos-linux android-cache]$ cd ..
[kenny@centos-linux ~]$ cd android
[kenny@centos-linux android]$ ls
[kenny@centos-linux android]$ cd ..
[kenny@centos-linux ~]$ cd android-cache/
[kenny@centos-linux android-cache]$ mkdir keystore
[kenny@centos-linux android-cache]$ ls
keystore

https://blog.csdn.net/Captive_Rainbow_/article/details/90407356

Android通过add添加多个Fragment事件透传问题

在通过add添加多个Fragment的过程中,如果新fragment某空白区域对应上一fragment的某个控件,点击该空白区域会响应上一fragment控件点击事件,也就是事件透传过去了,解决该问题最简单的方法即为在fragment的根布局文件中加入android:clickable = true属性即可,或者对根布局设个空的点击事件也行。

Android V1及V2签名原理简析

Android为了保证系统及应用的安全性,在安装APK的时候需要校验包的完整性,同时,对于覆盖安装的场景还要校验新旧是否匹配,这两者都是通过Android签名机制来进行保证的,本文就简单看下Android的签名与校验原理,分一下几个部分分析下:

  • APK签名是什么
  • APK签名如何保证APK信息完整性
  • 如何为APK签名
  • APK签名怎么校验

Android的APK签名是什么

签名是摘要与非对称密钥加密相相结合的产物,摘要就像内容的一个指纹信息,一旦内容被篡改,摘要就会改变,签名是摘要的加密结果,摘要改变,签名也会失效。Android APK签名也是这个道理,如果APK签名跟内容对应不起来,Android系统就认为APK内容被篡改了,从而拒绝安装,以保证系统的安全性。目前Android有三种签名V1、V2(N)、V3(P),本文只看前两种V1跟V2,对于V3的轮密先不考虑。先看下只有V1签名后APK的样式:

再看下只有V2签名的APK包样式:

同时具有V1 V2签名:

可以看到,如果只有V2签名,那么APK包内容几乎是没有改动的,META_INF中不会有新增文件,按Google官方文档:在使用v2签名方案进行签名时,会在APK文件中插入一个APK签名分块,该分块位于zip中央目录部分之前并紧邻该部分。在APK签名分块内,签名和签名者身份信息会存储在APK签名方案v2分块中,保证整个APK文件不可修改,如下图:

而V1签名是通过META-INF中的三个文件保证签名及信息的完整性:

APK签名如何保证APK信息完整性

V1签名是如何保证信息的完整性呢?V1签名主要包含三部分内容,如果狭义上说签名跟公钥的话,仅仅在.rsa文件中,V1签名的三个文件其实是一套机制,不能单单拿一个来说事,

MANIFEST.MF:摘要文件,存储文件名与文件SHA1摘要(Base64格式)键值对,格式如下,其主要作用是保证每个文件的完整性

如果对APK中的资源文件进行了替换,那么该资源的摘要必定发生改变,如果没有修改MANIFEST.MF中的信息,那么在安装时候V1校验就会失败,无法安装,不过如果篡改文件的同时,也修改其MANIFEST.MF中的摘要值,那么MANIFEST.MF校验就可以绕过。

CERT.SF:二次摘要文件,存储文件名与MANIFEST.MF摘要条目的SHA1摘要(Base64格式)键值对,格式如下

CERT.SF个人觉得有点像冗余,更像对文件完整性的二次保证,同绕过MANIFEST.MF一样,.SF校验也很容易被绕过。

CERT.RSA 证书(公钥)及签名文件,存储keystore的公钥、发行信息、以及对CERT.SF文件摘要的签名信息(利用keystore的私钥进行加密过)

CERT.RSA与CERT.SF是相互对应的,两者名字前缀必须一致,不知道算不算一个无聊的标准。看下CERT.RSA文件内容:

CERT.RSA文件里面存储了证书公钥、过期日期、发行人、加密算法等信息,根据公钥及加密算法,Android系统就能计算出CERT.SF的摘要信息,其严格的格式如下:

从CERT.RSA中,我们能获的证书的指纹信息,在微信分享、第三方SDK申请的时候经常用到,其实就是公钥+开发者信息的一个签名:

除了CERT.RSA文件,其余两个签名文件其实跟keystore没什么关系,主要是文件自身的摘要及二次摘要,用不同的keystore进行签名,生成的MANIFEST.MF与CERT.SF都是一样的,不同的只有CERT.RSA签名文件。也就是说前两者主要保证各个文件的完整性,CERT.RSA从整体上保证APK的来源及完整性,不过META_INF中的文件不在校验范围中,这也是V1的一个缺点。V2签名又是如何保证信息的完整性呢?

V2签名块如何保证APK的完整性

前面说过V1签名中文件的完整性很容易被绕过,可以理解单个文件完整性校验的意义并不是很大,安装的时候反而耗时,不如采用更加简单的便捷的校验方式。V2签名就不针对单个文件校验了,而是针对APK进行校验,将APK分成1M的块,对每个块计算值摘要,之后针对所有摘要进行摘要,再利用摘要进行签名。

也就是说,V2摘要签名分两级,第一级是对APK文件的1、3 、4 部分进行摘要,第二级是对第一级的摘要集合进行摘要,然后利用秘钥进行签名。安装的时候,块摘要可以并行处理,这样可以提高校验速度。

简单的APK签名流程(签名原理)

APK是先摘要,再签名,先看下摘要的定义:Message Digest:摘要是对消息数据执行一个单向Hash,从而生成一个固定长度的Hash值,这个值就是消息摘要,至于常听到的MD5、SHA1都是摘要算法的一种。理论上说,摘要一定会有碰撞,但只要保证有限长度内碰撞率很低就可以,这样就能利用摘要来保证消息的完整性,只要消息被篡改,摘要一定会发生改变。但是,如果消息跟摘要同时被修改,那就无从得知了。

而数字签名是什么呢(公钥数字签名),利用非对称加密技术,通过私钥对摘要进行加密,产生一个字符串,这个字符串+公钥证书就可以看做消息的数字签名,如RSA就是常用的非对称加密算法。在没有私钥的前提下,非对称加密算法能确保别人无法伪造签名,因此数字签名也是对发送者信息真实性的一个有效证明。不过由于Android的keystore证书是自签名的,没有第三方权威机构认证,用户可以自行生成keystore,Android签名方案无法保证APK不被二次签名。

知道了摘要跟签名的概念后,再来看看Android的签名文件怎么来的?如何影响原来APK包?通过sdk中的apksign来对一个APK进行签名的命令如下:

 ./apksigner sign  --ks   keystore.jks  --ks-key-alias keystore  --ks-pass pass:XXX  --key-pass pass:XXX  --out output.apk input.apk

其主要实现在 android/platform/tools/apksig 文件夹中,主体是ApkSigner.java的sign函数,函数比较长,分几步分析

private void sign(
        DataSource inputApk,
        DataSink outputApkOut,
        DataSource outputApkIn)
                throws IOException, ApkFormatException, NoSuchAlgorithmException,
                        InvalidKeyException, SignatureException {
    // Step 1. Find input APK's main ZIP sections
    ApkUtils.ZipSections inputZipSections;
    <!--根据zip包的结构,找到APK中包内容Object-->
    try {
        inputZipSections = ApkUtils.findZipSections(inputApk);
    ...

先来看这一步,ApkUtils.findZipSections,这个函数主要是解析APK文件,获得ZIP格式的一些简单信息,并返回一个ZipSections,

 public static ZipSections findZipSections(DataSource apk)
            throws IOException, ZipFormatException {
        Pair<ByteBuffer, Long> eocdAndOffsetInFile =
                ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
        ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
        long eocdOffset = eocdAndOffsetInFile.getSecond();
        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
        long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
        ...
        long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
        long cdEndOffset = cdStartOffset + cdSizeBytes;
        int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
        return new ZipSections(
                cdStartOffset,
                cdSizeBytes,
                cdRecordCount,
                eocdOffset,
                eocdBuf);
    }

ZipSections包含了ZIP文件格式的一些信息,比如中央目录信息、中央目录结尾信息等,对比到zip文件格式如下:

获取到 ZipSections之后,就可以进一步解析APK这个ZIP包,继续走后面的签名流程,

    long inputApkSigningBlockOffset = -1;
    DataSource inputApkSigningBlock = null;
    <!--检查V2签名是否存在-->
    try {
        Pair<DataSource, Long> apkSigningBlockAndOffset =
                V2SchemeVerifier.findApkSigningBlock(inputApk, inputZipSections);
        inputApkSigningBlock = apkSigningBlockAndOffset.getFirst();
        inputApkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
    } catch (V2SchemeVerifier.SignatureNotFoundException e) {
    <!--V2签名不存在也没什么问题,非必须-->
}
 <!--获取V2签名以外的信息区域-->
 DataSource inputApkLfhSection =
            inputApk.slice(
                    0,
                    (inputApkSigningBlockOffset != -1)
                            ? inputApkSigningBlockOffset
                            : inputZipSections.getZipCentralDirectoryOffset());

可以看到先进行了一个V2签名的检验,这里是用来签名,为什么先检验了一次?第一次签名的时候会直接走这个异常逻辑分支,重复签名的时候才能获到取之前的V2签名,怀疑这里获取V2签名的目的应该是为了排除V2签名,并获取V2签名以外的数据块,因为签名本身不能被算入到签名中,之后会解析中央目录区,构建一个DefaultApkSignerEngine用于签名

      <!--解析中央目录区,目的是为了解析AndroidManifest-->
    // Step 2. Parse the input APK's ZIP Central Directory
    ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections);
    List<CentralDirectoryRecord> inputCdRecords =
            parseZipCentralDirectory(inputCd, inputZipSections);

    // Step 3. Obtain a signer engine instance
    ApkSignerEngine signerEngine;
    if (mSignerEngine != null) {
        signerEngine = mSignerEngine;
    } else {
        // Construct a signer engine from the provided parameters
        ...
        List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs =
                new ArrayList<>(mSignerConfigs.size());
        <!--一般就一个-->
        for (SignerConfig signerConfig : mSignerConfigs) {
            engineSignerConfigs.add(
                    new DefaultApkSignerEngine.SignerConfig.Builder(
                            signerConfig.getName(),
                            signerConfig.getPrivateKey(),
                            signerConfig.getCertificates())
                            .build());
        }
        <!--默认V1 V2都启用-->
        DefaultApkSignerEngine.Builder signerEngineBuilder =
                new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion)
                        .setV1SigningEnabled(mV1SigningEnabled)
                        .setV2SigningEnabled(mV2SigningEnabled)
                        .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved);
        if (mCreatedBy != null) {
            signerEngineBuilder.setCreatedBy(mCreatedBy);
        }
        signerEngine = signerEngineBuilder.build();
    }

先解析中央目录区,获取AndroidManifest文件,获取minSdkVersion(影响签名算法),并构建DefaultApkSignerEngine,默认情况下V1 V2签名都是打开的。

    // Step 4. Provide the signer engine with the input APK's APK Signing Block (if any)
    <!--忽略这一步-->
    if (inputApkSigningBlock != null) {
        signerEngine.inputApkSigningBlock(inputApkSigningBlock);
    }

    // Step 5. Iterate over input APK's entries and output the Local File Header + data of those
    // entries which need to be output. Entries are iterated in the order in which their Local
    // File Header records are stored in the file. This is to achieve better data locality in
    // case Central Directory entries are in the wrong order.
    List<CentralDirectoryRecord> inputCdRecordsSortedByLfhOffset =
            new ArrayList<>(inputCdRecords);
    Collections.sort(
            inputCdRecordsSortedByLfhOffset,
            CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
    int lastModifiedDateForNewEntries = -1;
    int lastModifiedTimeForNewEntries = -1;
    long inputOffset = 0;
    long outputOffset = 0;
    Map<String, CentralDirectoryRecord> outputCdRecordsByName =
            new HashMap<>(inputCdRecords.size());
    ...

    // Step 6. Sort output APK's Central Directory records in the order in which they should
    // appear in the output
    List<CentralDirectoryRecord> outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10);
    for (CentralDirectoryRecord inputCdRecord : inputCdRecords) {
        String entryName = inputCdRecord.getName();
        CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName);
        if (outputCdRecord != null) {
            outputCdRecords.add(outputCdRecord);
        }
    }

第五步与第六步的主要工作是:apk的预处理,包括目录的一些排序之类的工作,应该是为了更高效处理签名,预处理结束后,就开始签名流程,首先做的是V1签名(默认存在,除非主动关闭):

    // Step 7. Generate and output JAR signatures, if necessary. This may output more Local File
    // Header + data entries and add to the list of output Central Directory records.
    ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
            signerEngine.outputJarEntries();
    if (outputJarSignatureRequest != null) {
        if (lastModifiedDateForNewEntries == -1) {
            lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
            lastModifiedTimeForNewEntries = 0;
        }
        for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
                outputJarSignatureRequest.getAdditionalJarEntries()) {
            String entryName = entry.getName();
            byte[] uncompressedData = entry.getData();
            ZipUtils.DeflateResult deflateResult =
                    ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
            byte[] compressedData = deflateResult.output;
            long uncompressedDataCrc32 = deflateResult.inputCrc32;

            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
                    signerEngine.outputJarEntry(entryName);
            if (inspectEntryRequest != null) {
                inspectEntryRequest.getDataSink().consume(
                        uncompressedData, 0, uncompressedData.length);
                inspectEntryRequest.done();
            }

            long localFileHeaderOffset = outputOffset;
            outputOffset +=
                    LocalFileRecord.outputRecordWithDeflateCompressedData(
                            entryName,
                            lastModifiedTimeForNewEntries,
                            lastModifiedDateForNewEntries,
                            compressedData,
                            uncompressedDataCrc32,
                            uncompressedData.length,
                            outputApkOut);


            outputCdRecords.add(
                    CentralDirectoryRecord.createWithDeflateCompressedData(
                            entryName,
                            lastModifiedTimeForNewEntries,
                            lastModifiedDateForNewEntries,
                            uncompressedDataCrc32,
                            compressedData.length,
                            uncompressedData.length,
                            localFileHeaderOffset));
        }
        outputJarSignatureRequest.done();
    }

    // Step 8. Construct output ZIP Central Directory in an in-memory buffer
    long outputCentralDirSizeBytes = 0;
    for (CentralDirectoryRecord record : outputCdRecords) {
        outputCentralDirSizeBytes += record.getSize();
    }
    if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
        throw new IOException(
                "Output ZIP Central Directory too large: " + outputCentralDirSizeBytes
                        + " bytes");
    }
    ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
    for (CentralDirectoryRecord record : outputCdRecords) {
        record.copyTo(outputCentralDir);
    }
    outputCentralDir.flip();
    DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir);
    long outputCentralDirStartOffset = outputOffset;
    int outputCentralDirRecordCount = outputCdRecords.size();

    // Step 9. Construct output ZIP End of Central Directory record in an in-memory buffer
    ByteBuffer outputEocd =
            EocdRecord.createWithModifiedCentralDirectoryInfo(
                    inputZipSections.getZipEndOfCentralDirectory(),
                    outputCentralDirRecordCount,
                    outputCentralDirDataSource.size(),
                    outputCentralDirStartOffset);

步骤7、8、9都可以看做是V1签名的处理逻辑,主要在V1SchemeSigner中处理,其中包括创建META-INFO文件夹下的一些签名文件,更新中央目录、更新中央目录结尾等,流程不复杂,不在赘述,简单流程就是:

这里特殊提一下重复签名的问题:对一个已经V1签名的APK再次V1签名不会有任何问题,原理就是:再次签名的时候,会排除之前的签名文件。

  public static boolean isJarEntryDigestNeededInManifest(String entryName) {
        // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File

        // Entries which represent directories sould not be listed in the manifest.
        if (entryName.endsWith("/")) {
            return false;
        }

        // Entries outside of META-INF must be listed in the manifest.
        if (!entryName.startsWith("META-INF/")) {
            return true;
        }
        // Entries in subdirectories of META-INF must be listed in the manifest.
        if (entryName.indexOf('/', "META-INF/".length()) != -1) {
            return true;
        }

        // Ignored file names (case-insensitive) in META-INF directory:
        //   MANIFEST.MF
        //   *.SF
        //   *.RSA
        //   *.DSA
        //   *.EC
        //   SIG-*
        String fileNameLowerCase =
                entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
        if (("manifest.mf".equals(fileNameLowerCase))
                || (fileNameLowerCase.endsWith(".sf"))
                || (fileNameLowerCase.endsWith(".rsa"))
                || (fileNameLowerCase.endsWith(".dsa"))
                || (fileNameLowerCase.endsWith(".ec"))
                || (fileNameLowerCase.startsWith("sig-"))) {
            return false;
        }
        return true;
    }

可以看到目录、META-INF文件夹下的文件、sf、rsa等结尾的文件都不会被V1签名进行处理,所以这里不用担心多次签名的问题。接下来就是处理V2签名。

    // Step 10. Generate and output APK Signature Scheme v2 signatures, if necessary. This may
    // insert an APK Signing Block just before the output's ZIP Central Directory
    ApkSignerEngine.OutputApkSigningBlockRequest outputApkSigingBlockRequest =
            signerEngine.outputZipSections(
                    outputApkIn,
                    outputCentralDirDataSource,
                    DataSources.asDataSource(outputEocd));
    if (outputApkSigingBlockRequest != null) {
        byte[] outputApkSigningBlock = outputApkSigingBlockRequest.getApkSigningBlock();
        outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
        ZipUtils.setZipEocdCentralDirectoryOffset(
                outputEocd, outputCentralDirStartOffset + outputApkSigningBlock.length);
        outputApkSigingBlockRequest.done();
    }

    // Step 11. Output ZIP Central Directory and ZIP End of Central Directory
    outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
    outputApkOut.consume(outputEocd);
    signerEngine.outputDone();
}

V2SchemeSigner处理V2签名,逻辑比较清晰,直接对V1签名过的APK进行分块摘要,再集合签名,V2签名不会改变之前V1签名后的任何信息,签名后,在中央目录前添加V2签名块,并更新中央目录结尾信息,因为V2签名后,中央目录的偏移会再次改变:

APK签名怎么校验

签名校验的过程可以看做签名的逆向,只不过覆盖安装可能还要校验公钥及证书信息一致,否则覆盖安装会失败。签名校验的入口在PackageManagerService的install里,安装官方文档,7.0以上的手机优先检测V2签名,如果V2签名不存在,再校验V1签名,对于7.0以下的手机,不存在V2签名校验机制,只会校验V1,所以,如果你的App的miniSdkVersion<24(N),那么你的签名方式必须内含V1签名:

校验流程就是签名的逆向,了解签名流程即可,本文不求甚解,有兴趣自己去分析,只是额外提下覆盖安装,覆盖安装除了检验APK自己的完整性以外,还要校验证书是否一致只有证书一致(同一个keystore签名),才有可能覆盖升级。覆盖安装同全新安装相比较多了几个校验

  • 包名一致
  • 证书一致
  • versioncode不能降低

这里只关心证书部分:

    // Verify: if target already has an installer package, it must
    // be signed with the same cert as the caller.
    if (targetPackageSetting.installerPackageName != null) {
        PackageSetting setting = mSettings.mPackages.get(
                targetPackageSetting.installerPackageName);
        // If the currently set package isn't valid, then it's always
        // okay to change it.
        if (setting != null) {
            if (compareSignatures(callerSignature,
                    setting.signatures.mSignatures)
                    != PackageManager.SIGNATURE_MATCH) {
                throw new SecurityException(
                        "Caller does not have same cert as old installer package "
                        + targetPackageSetting.installerPackageName);
            }
        }
    }

V1、V2签名下美团多渠道打包的切入点

  • V1签名:META_INFO文件夹下增加文件不会对校验有任何影响,则是美团V1多渠道打包方案的切入点
  • V2签名:V2签名块中可以添加一些附属信息,不会对签名又任何影响,这是V2多渠道打包的切入点。

总结

V1签名靠META_INFO文件夹下的签名文件V2签名依靠中央目录前的V2签名快,ZIP的目录结构不会改变,当然结尾偏移要改。V1 V2签名可以同时存在(miniSdkVersion 7.0以下如果没有V1签名是不可以的)多去到打包的切入点原则:附加信息不影响签名验证

https://www.jianshu.com/p/95096ca209e1

Centos 7 搭建 Gitlab 服务器

Last login: Mon Mar 30 13:22:46 on ttys000
xxxs-MacBook-Pro:~ kenny$ ifconfig
lo0: flags=8049<up,loopback,running,multicast> mtu 16384
	options=1203<rxcsum,txcsum,txstatus,sw_timestamp>
	inet 127.0.0.1 netmask 0xff000000 
	inet6 ::1 prefixlen 128 
	。
    。
    。
	options=3<rxcsum,txcsum>
	ether 00:1c:42:00:00:09 
	inet 10.37.129.2 netmask 0xffffff00 broadcast 10.37.129.255
	media: autoselect
	status: active
xxxs-MacBook-Pro:~ kenny$ ifconfig | grep 192
xxxs-MacBook-Pro:~ kenny$ ifconfig | grep 10
gif0: flags=8010<pointopoint,multicast> mtu 1280
	media: autoselect (100baseTX )
	inet 10.66.105.48 netmask 0xfffff800 broadcast 10.66.111.255
		maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200
	inet 10.211.55.2 netmask 0xffffff00 broadcast 10.211.55.255
	inet 10.37.129.2 netmask 0xffffff00 broadcast 10.37.129.255
xxxs-MacBook-Pro:~ kenny$ ping 10.211.55.3
PING 10.211.55.3 (10.211.55.3): 56 data bytes
64 bytes from 10.211.55.3: icmp_seq=0 ttl=64 time=0.291 ms
64 bytes from 10.211.55.3: icmp_seq=1 ttl=64 time=0.306 ms
64 bytes from 10.211.55.3: icmp_seq=2 ttl=64 time=0.440 ms
^C
--- 10.211.55.3 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.291/0.346/0.440/0.067 ms
xxxs-MacBook-Pro:~ kenny$ ssh -l kenny 10.211.55.3
The authenticity of host '10.211.55.3 (10.211.55.3)' can't be established.
ECDSA key fingerprint is SHA256:3u3go1kEgAKFyUXQf+Bv9MmyCop4dUUgyuDSH0EJw3I.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '10.211.55.3' (ECDSA) to the list of known hosts.
kenny@10.211.55.3's password: 
Last login: Mon Apr 20 15:42:30 2020
[kenny@localhost ~]$ ls
Desktop  Documents  Downloads  fontconfig  hhhha.txt  Music  Pictures  Public  Templates  Videos
[kenny@localhost ~]$ ls
Desktop  Documents  Downloads  fontconfig  hhhha.txt  Music  Pictures  Public  Templates  Videos
[kenny@localhost ~]$ ls
Desktop  Documents  Downloads  fontconfig  hhhha.txt  Music  Pictures  Public  Templates  Videos
[kenny@localhost ~]$ mkdir android && cd android
[kenny@localhost android]$ wget https://dl.google.com/android/repository/tools_r26.1.1-linux.zip
--2020-04-20 16:06:33--  https://dl.google.com/android/repository/tools_r26.1.1-linux.zip
正在解析主机 dl.google.com (dl.google.com)... 203.208.39.225, 203.208.39.230, 203.208.39.238, ...
正在连接 dl.google.com (dl.google.com)|203.208.39.225|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 404 Not Found
2020-04-20 16:06:34 错误 404:Not Found。

[kenny@localhost android]$ ls
[kenny@localhost android]$ apt -get --quiet update --yes
bash: apt: 未找到命令...
[kenny@localhost android]$ ping www.baidu.com
PING www.a.shifen.com (180.101.49.12) 56(84) bytes of data.
64 bytes from 180.101.49.12 (180.101.49.12): icmp_seq=1 ttl=128 time=10.8 ms
64 bytes from 180.101.49.12 (180.101.49.12): icmp_seq=2 ttl=128 time=16.7 ms
64 bytes from 180.101.49.12 (180.101.49.12): icmp_seq=3 ttl=128 time=17.3 ms
^C
--- www.a.shifen.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 10.877/14.993/17.396/2.924 ms
[kenny@localhost android]$ sudo yum install -y curl policycoreutils-python openssh-server

我们信任您已经从系统管理员那里了解了日常注意事项。
总结起来无外乎这三点:

    #1) 尊重别人的隐私。
    #2) 输入前要先考虑(后果和风险)。
    #3) 权力越大,责任越大。

[sudo] kenny 的密码:
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirrors.aliyun.com
 * extras: mirrors.njupt.edu.cn
 * updates: mirrors.ustc.edu.cn
软件包 policycoreutils-python-2.5-33.el7.x86_64 已安装并且是最新版本
软件包 openssh-server-7.4p1-21.el7.x86_64 已安装并且是最新版本
正在解决依赖关系
--> 正在检查事务
---> 软件包 curl.x86_64.0.7.29.0-54.el7 将被 升级
---> 软件包 curl.x86_64.0.7.29.0-54.el7_7.2 将被 更新
--> 正在处理依赖关系 libcurl = 7.29.0-54.el7_7.2,它被软件包 curl-7.29.0-54.el7_7.2.x86_64 需要
--> 正在检查事务
---> 软件包 libcurl.x86_64.0.7.29.0-54.el7 将被 升级
---> 软件包 libcurl.x86_64.0.7.29.0-54.el7_7.2 将被 更新
--> 解决依赖关系完成

依赖关系解决

============================================================================================================================================================================================================================================================================================================================================================================
 Package                                                                                架构                                                                                  版本                                                                                             源                                                                                      大小
============================================================================================================================================================================================================================================================================================================================================================================
正在更新:
 curl                                                                                   x86_64                                                                                7.29.0-54.el7_7.2                                                                                updates                                                                                270 k
为依赖而更新:
 libcurl                                                                                x86_64                                                                                7.29.0-54.el7_7.2                                                                                updates                                                                                223 k

事务概要
============================================================================================================================================================================================================================================================================================================================================================================
升级  1 软件包 (+1 依赖软件包)

总计:493 k
Downloading packages:
警告:/var/cache/yum/x86_64/7/updates/packages/curl-7.29.0-54.el7_7.2.x86_64.rpm: 头V3 RSA/SHA256 Signature, 密钥 ID f4a80eb5: NOKEY
从 file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 检索密钥
导入 GPG key 0xF4A80EB5:
 用户ID     : "CentOS-7 Key (CentOS 7 Official Signing Key) <security@centos.org>"
 指纹       : 6341 ab27 53d7 8a78 a7c2 7bb1 24c6 a8a7 f4a8 0eb5
 软件包     : centos-release-7-7.1908.0.el7.centos.x86_64 (@anaconda)
 来自       : /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  正在更新    : libcurl-7.29.0-54.el7_7.2.x86_64                                                                                                                                                                                                                                                                                                                        1/4 
  正在更新    : curl-7.29.0-54.el7_7.2.x86_64                                                                                                                                                                                                                                                                                                                           2/4 
  清理        : curl-7.29.0-54.el7.x86_64                                                                                                                                                                                                                                                                                                                               3/4 
  清理        : libcurl-7.29.0-54.el7.x86_64                                                                                                                                                                                                                                                                                                                            4/4 
  验证中      : curl-7.29.0-54.el7_7.2.x86_64                                                                                                                                                                                                                                                                                                                           1/4 
  验证中      : libcurl-7.29.0-54.el7_7.2.x86_64                                                                                                                                                                                                                                                                                                                        2/4 
  验证中      : libcurl-7.29.0-54.el7.x86_64                                                                                                                                                                                                                                                                                                                            3/4 
  验证中      : curl-7.29.0-54.el7.x86_64                                                                                                                                                                                                                                                                                                                               4/4 

更新完毕:
  curl.x86_64 0:7.29.0-54.el7_7.2                                                                                                                                                                                                                                                                                                                                           

作为依赖被升级:
  libcurl.x86_64 0:7.29.0-54.el7_7.2                                                                                                                                                                                                                                                                                                                                        

完毕!
[kenny@localhost android]$ sudo systemctl enable sshd
[kenny@localhost android]$ sudo systemctl start sshd
[kenny@localhost android]$ sudo firewall-cmd --permanent --add-service=http
success
[kenny@localhost android]$ sudo systemctl reload firewalld
[kenny@localhost android]$ cd ..
[kenny@localhost ~]$ ls
android  Desktop  Documents  Downloads  fontconfig  hhhha.txt  Music  Pictures  Public  Templates  Videos
[kenny@localhost ~]$ sudo yum install postfix
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirrors.aliyun.com
 * extras: mirrors.njupt.edu.cn
 * updates: mirrors.ustc.edu.cn
软件包 2:postfix-2.10.1-7.el7.x86_64 已安装并且是最新版本
无须任何处理
[kenny@localhost ~]$ sudo systemctl enable postfix
[kenny@localhost ~]$ sudo systemctl start postfix
[kenny@localhost ~]$ sudo yum install wget
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirrors.aliyun.com
 * extras: mirrors.njupt.edu.cn
 * updates: mirrors.ustc.edu.cn
软件包 wget-1.14-18.el7_6.1.x86_64 已安装并且是最新版本
无须任何处理
[kenny@localhost ~]$ wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm
--2020-04-20 16:46:43--  https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm
正在解析主机 mirrors.tuna.tsinghua.edu.cn (mirrors.tuna.tsinghua.edu.cn)... 101.6.8.193
正在连接 mirrors.tuna.tsinghua.edu.cn (mirrors.tuna.tsinghua.edu.cn)|101.6.8.193|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 200 OK
长度:569226236 (543M) [application/x-redhat-package-manager]
正在保存至: “gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm”

 0% [=>                                              ] 5,070,497    177KB/s 剩余 37m 33s^C                                                                                                                                                                                                                                                                                                                 ] 12,811,545  3.76KB/s 用时 6m 8s  

2020-04-20 17:22:52 (15.2 KB/s) - 在 12811545/569226236 字节处发生读取错误 (Connection reset by peer)。[kenny@localhost ~]$ ls
android  Desktop  Documents  Downloads  fontconfig  gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm  hhhha.txt  Music  Pictures  Public  Templates  Videos
[kenny@localhost ~]$ rpm -i gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm
警告:gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm: 头V4 RSA/SHA1 Signature, 密钥 ID f27eab47: NOKEY
错误:can't create 事务 lock on /var/lib/rpm/.rpm.lock (权限不够)
[kenny@localhost ~]$ sudo rpm -i gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm
[sudo] kenny 的密码:
警告:gitlab-ce-11.9.9-ce.0.el7.x86_64.rpm: 头V4 RSA/SHA1 Signature, 密钥 ID f27eab47: NOKEY
It looks like GitLab has not been configured yet; skipping the upgrade script.

       *.                  *.
      ***                 ***
     *****               *****
    .******             *******
    ********            ********
   ,,,,,,,,,***********,,,,,,,,,
  ,,,,,,,,,,,*********,,,,,,,,,,,
  .,,,,,,,,,,,*******,,,,,,,,,,,,
      ,,,,,,,,,*****,,,,,,,,,.
         ,,,,,,,****,,,,,,
            .,,,***,,,,
                ,*,.
  


     _______ __  __          __
    / ____(_) /_/ /   ____ _/ /_
   / / __/ / __/ /   / __ `/ __ \
  / /_/ / / /_/ /___/ /_/ / /_/ /
  \____/_/\__/_____/\__,_/_.___/
  

Thank you for installing GitLab!
GitLab was unable to detect a valid hostname for your instance.
Please configure a URL for your GitLab instance by setting `external_url`
configuration in /etc/gitlab/gitlab.rb file.
Then, you can start your GitLab instance by running the following command:
  sudo gitlab-ctl reconfigure

For a comprehensive list of configuration options please see the Omnibus GitLab readme
https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md

[kenny@localhost ~]$ vi /etc/gitlab/gitlab.rb
</security@centos.org></pointopoint,multicast></rxcsum,txcsum></rxcsum,txcsum,txstatus,sw_timestamp></up,loopback,running,multicast>

找到 external_url 这个属性,把地址改为虚拟机 ip 并指定端口号:

[kenny@localhost ~]$ su
密码:
[root@localhost kenny]# cd /etc
[root@localhost etc]# ls
abrt                   auto.smb                   chromium      dbus-1                      environment   gcrypt       grub.d       ipa            latrace.d       logrotate.d               mtools.conf        ntp.conf        pcp.env         pulse       redhat-release    securetty          ssl                 target               virt-who.d
adjtime                avahi                      chrony.conf   dconf                       ethertypes    gdbinit      gshadow      iproute2       ld.so.cache     lsm                       multipath          numad.conf      pinforc         purple      request-key.conf  security           sssd                tcsd.conf            wgetrc
aliases                bash_completion.d          chrony.keys   default                     exports       gdbinit.d    gshadow-     ipsec.conf     ld.so.conf      lvm                       my.cnf             oddjob          pkcs11          python      request-key.d     selinux            statetab            terminfo             wpa_supplicant
aliases.db             bashrc                     cifs-utils    depmod.d                    exports.d     gdm          gss          ipsec.d        ld.so.conf.d    machine-id                my.cnf.d           oddjobd.conf    pki             qemu-ga     resolv.conf       services           statetab.d          tmpfiles.d           X11
alternatives           binfmt.d                   cron.d        dhcp                        favicon.png   geoclue      gssproxy     ipsec.secrets  libaudit.conf   magic                     nanorc             oddjobd.conf.d  plymouth        qemu-kvm    rhsm              sestatus.conf      subgid              trusted-key.key      xdg
anacrontab             bluetooth                  cron.daily    DIR_COLORS                  fcoe          GeoIP.conf   host.conf    iscsi          libblockdev     mailcap                   ndctl              openldap        pm              radvd.conf  rpc               setroubleshoot     subuid              tuned                xinetd.d
asound.conf            brltty                     cron.deny     DIR_COLORS.256color         festival      ghostscript  hostname     issue          libibverbs.d    mail.rc                   netconfig          opt             pnm2ppa.conf    rc0.d       rpm               setuptool.d        sudo.conf           udev                 xml
at.deny                brltty.conf                cron.hourly   DIR_COLORS.lightbgcolor     filesystems   gimp         hosts        issue.net      libnl           makedumpfile.conf.sample  NetworkManager     os-release      polkit-1        rc1.d       rsyncd.conf       sgml               sudoers             udisks2              yum
audisp                 centos-release             cron.monthly  dleyna-server-service.conf  firewalld     gitlab       hosts.allow  java           libpaper.d      man_db.conf               networks           PackageKit      popt.d          rc2.d       rsyslog.conf      shadow             sudoers.d           unbound              yum.conf
audit                  centos-release-upstream    crontab       dnsmasq.conf                flatpak       glvnd        hosts.deny   jvm            libreport       maven                     nfs.conf           pam.d           postfix         rc3.d       rsyslog.d         shadow-            sudo-ldap.conf      updatedb.conf        yum.repos.d
autofs.conf            certmonger                 cron.weekly   dnsmasq.d                   fonts         gnupg        hp           jvm-commmon    libuser.conf    mime.types                nfsmount.conf      papersize       ppp             rc4.d       rwtab             shells             sysconfig           UPower
autofs_ldap_auth.conf  cgconfig.conf              crypttab      dracut.conf                 fprintd.conf  GREP_COLORS  idmapd.conf  kdump.conf     libvirt         mke2fs.conf               nscd.conf          passwd          prelink.conf.d  rc5.d       rwtab.d           skel               sysctl.conf         usb_modeswitch.conf
auto.master            cgconfig.d                 csh.cshrc     dracut.conf.d               fstab         groff        ImageMagick  kernel         locale.conf     modprobe.d                nslcd.conf         passwd-         printcap        rc6.d       samba             smartmontools      sysctl.d            vconsole.conf
auto.master.d          cgrules.conf               csh.login     e2fsck.conf                 fuse.conf     group        init.d       krb5.conf      localtime       modules-load.d            nsswitch.conf      pbm2ppa.conf    profile         rc.d        sane.d            sos.conf           systemd             vimrc
auto.misc              cgsnapshot_blacklist.conf  cups          egl                         fwupd         group-       inittab      krb5.conf.d    login.defs      motd                      nsswitch.conf.bak  pcp             profile.d       rc.local    sasl2             speech-dispatcher  system-release      virc
auto.net               chkconfig.d                cupshelpers   enscript.cfg                gconf         grub2.cfg    inputrc      ksmtuned.conf  logrotate.conf  mtab                      ntp                pcp.conf        protocols       rdma        scl               ssh                system-release-cpe  virt-who.conf
[root@localhost etc]# cd gitlab/
[root@localhost gitlab]# ls
gitlab.rb
[root@localhost gitlab]# vi gitlab.rb 
[root@localhost gitlab]# gitlab-ctl reconfigure
Starting Chef Client, version 13.6.4
resolving cookbooks for run list: ["gitlab"]
Synchronizing Cookbooks:
  - gitlab (0.0.1)
  - package (0.1.0)
  - postgresql (0.1.0)
  - redis (0.1.0)
  - registry (0.1.0)
  - mattermost (0.1.0)
  - consul (0.1.0)
  - gitaly (0.1.0)
  - letsencrypt (0.1.0)
  - nginx (0.1.0)
  - runit (4.3.0)
  - acme (3.1.0)
  - crond (0.1.0)
  - compat_resource (12.19.1)
。
。
。
Recipe: <dynamically defined="" resource="">
  * service[alertmanager] action restart
    - restart service service[alertmanager]
  * service[postgres-exporter] action restart
    - restart service service[postgres-exporter]

Running handlers:
Running handlers complete
Chef Client finished, 475/1268 resources updated in 02 minutes 43 seconds
gitlab Reconfigured!

虽然 gitlab 指定了使用 8888 端口,但是这时候防火墙并没有开放这个端口,所以需要在防火墙配置一下。

[root@localhost gitlab]# firewall-cmd --list-ports

[root@localhost gitlab]# firewal-cmd --zone=public --add-port=8888/tcp --permanent
bash: firewal-cmd: 未找到命令...
[root@localhost gitlab]# firewall-cmd --zone=public --add-port=8888/tcp --permanent
success
[root@localhost gitlab]# firewall-cmd --reload
success
[root@localhost gitlab]# firewall-cmd --list-ports
8888/tcp
[root@localhost gitlab]# packet_write_wait: Connection to 10.211.55.3 port 22: Broken pipe
xxxs-MacBook-Pro:~ kenny$ 
https://blog.csdn.net/Captive_Rainbow_/article/details/90375937