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)。

摄像头预览 三种方式

摄像头预览有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,能成功检测到内存泄漏。

配置 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

implementation和api

implementation 可以在编译时隐藏自己的依赖,但运行时这个依赖对所有模块可见。目的是为了减少编译时间,依赖自己的模块不会因为这个依赖变更重新编译

A- – ->B- – ->C 当C修改后,只要重新编译B和C就好了,A不需要编译

Test

我的车 tradexportprerelease://ads/9d79cd6e-83be-4efa-b8b8-00b63ada76ea/
二手车 tradexportprerelease://ads/ce8976e5-7820-488d-891f-5782caf0bb66/
新车 tradexportprerelease://ads/05bbad0e-e593-4f16-961a-07a5cce39b38/
购买 tradexportprerelease://ads
订单 tradexportprerelease://orders
聊天 tradexportprerelease://messages/83e34e5b-7772-4b6a-ba96-8556486d9eb4/


tradex://tradexport.com/ads
https://tradexport.com/ads

https://u9814280.ct.sendgrid.net/wf/click?upn=yHLBMl79VtwLkvdSNg6rt1wuIcEZxidcfSDB5uJIFdnuOOYw-2FvfTFi6o8R3CtFED-2FxTTphkzGF9-2Fvf0JlSiLvXwr6hz3sDPzwX7A8g8lFA8-3D_rSqg-2FyCuwG-2Bl-2BFw25GPTXspAEz4yJrsYIBrJX-2F9K-2FDLQQScoyA7rjiufIwG3Yz2Xua3FVhCn-2FioTzyqwVi8syHoeJEsK-2BR8RyCPF8bgNYXPFOHaes5aYB7mHrhbBFPGRjYlTIL596PDvYTw6RKiwrrJlMwai76XdmyE4G8qcxS3R4RcypwZSuQUwiu88k-2B413f1FcYGIXejhMLTCu1AMU71a-2FYS736tQcU4YWrP9kZ3iZe0p3Pbr0Fe6U0t9-2F5bELHplcrpFrOjB16kMEgRPMIAEPlma8vN0Yrp8CnoThKc-3D

JVM内存区域的划分,哪些区域会发生 OOM

JVM 的内存区域可以分为两类:线程私有的区域和线程共有的区域。 线程私有的区域:程序计数器、JVM 虚拟机栈、本地方法栈 线程共有的区域:堆、方法区、运行时常量池

  • 程序计数器。 每个线程有有一个私有的程序计数器,任何时间一个线程都只会有一个方法正在执行,也就是所谓的当前方法。程序计数器存放的就是这个当前方法的JVM指令地址
  • JVM虚拟机栈。 创建线程的时候会创建线程内的虚拟机栈,栈中存放着一个个的栈帧,对应着一个个方法的调用。JVM 虚拟机栈有两种操作,分别是压栈和出站。栈帧中存放着局部变量表、方法返回值和方法的正常或异常退出的定义等等。
  • 本地方法栈。 跟 JVM 虚拟机栈比较类似,只不过它支持的是 Native 方法。
  • 堆。 堆是内存管理的核心区域,用来存放对象实例。几乎所有创建的对象实例都会直接分配到堆上。所以堆也是垃圾回收的主要区域,垃圾收集器会对堆有着更细的划分,最常见的就是把堆划分为新生代和老年代
  • 方法区。方法区主要存放类的结构信息,比如静态属性和方法等等
  • 运行时常量池。运行时常量池位于方法区中,主要存放各种常量信息

其实除了程序计数器,其他的部分都会发生 OOM

  • 堆。 通常发生的 OOM 都会发生在堆中,最常见的可能导致 OOM 的原因就是内存泄漏
  • JVM虚拟机栈和本地方法栈。 当我们写一个递归方法,这个递归方法没有循环终止条件,最终会导致 StackOverflow 的错误。当然,如果栈空间扩展失败,也是会发生 OOM 的
  • 方法区。方法区现在基本上不太会发生 OOM,但在早期内存中加载的类信息过多的情况下也是会发生 OOM 的

GC机制

垃圾回收需要完成两件事:找到垃圾,回收垃圾。 找到垃圾一般的话有两种方法:

  • 引用计数法:当一个对象被引用时,它的引用计数器会加一,垃圾回收时会清理掉引用计数为0的对象。但这种方法有一个问题,比方说有两个对象 A 和 B,A 引用了 B,B 又引用了 A,除此之外没有别的对象引用 A 和 B,那么 A 和 B 在我们看来已经是垃圾对象,需要被回收,但它们的引用计数不为 0,没有达到回收的条件。正因为这个循环引用的问题,Java 并没有采用引用计数法。
  • 可达性分析法: 我们把 Java 中对象引用的关系看做一张图,从根级对象不可达的对象会被垃圾收集器清除。根级对象一般包括 Java 虚拟机栈中的对象、本地方法栈中的对象、方法区中的静态对象和常量池中的常量。

回收垃圾的话有这么四种方法:

  • 标记清除算法: 顾名思义分为两步,标记和清除。首先标记到需要回收的垃圾对象,然后回收掉这些垃圾对象。标记清除算法的缺点是清除垃圾对象后会造成内存的[碎片化]。
  • 复制算法: 复制算法是将存活的对象复制到另一块内存区域中,并做相应的内存整理工作。复制算法的优点是可以避免内存碎片化,缺点也显而易见,它需要[两倍的内存]。
  • 标记整理算法: 标记整理算法也是分两步,先标记后整理。它会标记需要回收的垃圾对象,清除掉垃圾对象后会[将存活的对象压缩],避免了内存的碎片化。
  • 分代算法:将对象分为新生代和老年代对象。那么为什么做这样的区分呢?主要是在Java运行中会产生大量对象,这些对象的生命周期会有很大的不同,有的生命周期很长,有的甚至使用一次之后就不再使用。所以针对不同生命周期的对象采用不同的回收策略,这样可以提高GC的效率

新生代对象分为三个区域:Eden 区和两个 Survivor 区。

新创建的对象都放在 Eden区,当 Eden 区的内存达到阈值之后会触发 Minor GC,这时会将存活的对象复制到一个 Survivor 区中,这些存活对象的生命存活计数会加一。这时 Eden 区会闲置,当再一次达到阈值触发 Minor GC 时,会将Eden区和之前一个 Survivor 区中存活的对象复制到另一个 Survivor 区中,采用的是我之前提到的复制算法,同时它们的生命存活计数也会加一。

这个过程会持续很多遍,直到对象的存活计数达到一定的阈值后会触发一个叫做晋升的现象:新生代的这个对象会被放置到老年代中。 老年代中的对象都是经过多次 GC 依然存活的生命周期很长的 Java 对象。当老年代的内存达到阈值后会触发 Major GC,采用的是标记整理算法

介绍下Android应用程序启动过程

一. :Launcher通过Binder进程间通信机制通知ActivityManagerService,它要启动一个Activity;

二.:ActivityManagerService通过Binder进程间通信机制通知Launcher进入Paused状态;

三.:Launcher通过Binder进程间通信机制通知ActivityManagerService,它已经准备就绪进入Paused状态,于是ActivityManagerService就创建一个新的进程,用来启动一个ActivityThread实例,即将要启动的Activity就是在这个ActivityThread实例中运行;

四. :ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象传递给ActivityManagerService,以便以后ActivityManagerService能够通过这个Binder对象和它进行通信;

五 :ActivityManagerService通过Binder进程间通信机制通知ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了。