Coder Social home page Coder Social logo

blog's Introduction

Tyhoo Wu 的个人博客

使用 GitHub Issues 写博客

博客分类

Repo Activity

Repo Activity

blog's People

Contributors

cnwutianhao avatar

Stargazers

 avatar  avatar

Watchers

 avatar

blog's Issues

View 的滑动

View 的滑动是 Android 实现自定义控件的基础,同时在开发中我们也难免会遇到 View 的滑动处理。其实不管是哪种滑动方式,其基本**都是类似的:当点击事件传到 View 时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。实现 View 滑动有很多种方法,在这里主要讲解6种滑动方法,分别是 layout()、offsetLeftAndRight() 与 offsetTopAndBottom()、LayoutParams、Animation、scollTo() 与 scollBy(),以及 Scroller。

一、layout() 方法

View 进行绘制的时候会调用 onLayout() 方法来设置显示的位置,因此我们同样也可以通过修改 View 的 left、top、right、bottom 这4种属性来控制 View 的坐标。首先我们要自定义一个 View,在 onTouchEvent() 方法中获取触摸点的坐标,代码如下所示:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    // 获取手指触摸点的横坐标和纵坐标
    val x = event?.x?.toInt()
    val y = event?.y?.toInt()

    when (event?.action) {
        MotionEvent.ACTION_DOWN -> {
            lastX = x ?: 0
            lastY = y ?: 0
        }

        ...
    }

    ...
}

接下来我们在 ACTION_MOVE 事件中计算偏移量,再调用 layout() 方法重新放置这个自定义 View 的位置即可。

override fun onTouchEvent(event: MotionEvent?): Boolean {
    ...

    when (event?.action) {
        ...

        MotionEvent.ACTION_MOVE -> {
            // 计算移动的距离
            val offsetX = x ?: (0 - lastX)
            val offsetY = y ?: (0 - lastY)

            // 调用 layout 方法来重新放置它的位置
            layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
        }
    }

    ...
}

在每次移动时都会调用 layout() 方法对屏幕重新布局,从而达到移动 View 的效果。自定义 View 的全部代码如下所示:

class CustomView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {

    private var lastX = 0
    private var lastY = 0

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        // 获取手指触摸点的横坐标和纵坐标
        val x = event?.x?.toInt()
        val y = event?.y?.toInt()

        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = x ?: 0
                lastY = y ?: 0
            }

            MotionEvent.ACTION_MOVE -> {
                // 计算移动的距离
                val offsetX = x ?: (0 - lastX)
                val offsetY = y ?: (0 - lastY)

                // 调用 layout 方法来重新放置它的位置
                layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
            }
        }

        return true
    }
}

随后,我们在布局中引用自定义 View 就可以了:

<com.tyhoo.android.demo.CustomView
    android:id="@+id/test_view"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@android:color/holo_red_light"
    ... />

运行程序,效果如图1所示:

图1

图1中的方块就是我们自定义的 View,它会随着我们手指的滑动改变自己的位置。

二、offsetLeftAndRight() 与 offsetTopAndBottom()

这两种方法和 layout() 方法的效果差不多,其使用方式也差不多。我们将 ACTION_MOVE 中的代码替换成如下代码:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    ...

    when (event?.action) {
        ...

        MotionEvent.ACTION_MOVE -> {
            // 计算移动的距离
            val offsetX = x ?: (0 - lastX)
            val offsetY = y ?: (0 - lastY)

            // 对 left 和 right 进行偏移
            offsetLeftAndRight(offsetX)
            // 对 top 和 bottom 进行偏移
            offsetTopAndBottom(offsetY)
        }
    }

    ...
}

三、LayoutParams

LayoutParams 主要保存了一个 View 的布局参数,因此我们可以通过 LayoutParams 来改变 View 的布局参数从而达到改变 View 位置的效果。同样,我们将 ACTION_MOVE 中的代码替换成如下代码:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    ...

    when (event?.action) {
        ...

        MotionEvent.ACTION_MOVE -> {
            // 计算移动的距离
            val offsetX = x ?: (0 - lastX)
            val offsetY = y ?: (0 - lastY)

            val layoutParams = layoutParams as ConstraintLayout.LayoutParams
            layoutParams.leftMargin = left + offsetX
            layoutParams.topMargin = top + offsetY
            setLayoutParams(layoutParams)
        }
    }

    ...
}

因为父控件是 ConstraintLayout,所以我们用了 ConstraintLayout.LayoutParams。如果父控件是 RelativeLayout,则要使用RelativeLayout.LayoutParams。除了使用布局的 LayoutParams 外,我们还可以用 ViewGroup.MarginLayoutParams 来实现:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    ...

    when (event?.action) {
        ...

        MotionEvent.ACTION_MOVE -> {
            // 计算移动的距离
            val offsetX = x ?: (0 - lastX)
            val offsetY = y ?: (0 - lastY)

            val layoutParams = layoutParams as ViewGroup.MarginLayoutParams
            layoutParams.leftMargin = left + offsetX
            layoutParams.topMargin = top + offsetY
            setLayoutParams(layoutParams)
        }
    }

    ...
}

四、Animation

可以采用 View 动画来移动,在 res 目录新建 anim 文件夹并创建 translate.xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="300" />
</set>

接下来在 Kotlin 代码中调用就好了,代码如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val testView = findViewById<CustomView>(R.id.test_view)
        testView.animation = AnimationUtils.loadAnimation(this, R.anim.translate)
    }
}

运行程序,效果如图2所示:

图2

运行程序,我们设置的方块会向右平移300像素,然后又会回到原来的位置。为了解决这个问题,我们需要在 translate.xml 中加上 fillAfter="true",代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="300" />
</set>

运行程序,效果如图3所示:

图3

运行代码后会发现,方块向右平移300像素后就停留在当前位置了。

需要注意的是,View 动画并不能改变 View 的位置参数。如果对一个 View 进行如上的平移动画操作,当 View 平移300像素停留在当前位置时,我们点击这个 View 并不会触发点击事件,但在我们点击这个 View 的原始位置时却触发了点击事件。对于系统来说这个 View 并没有改变原有的位置,所以我们点击其他位置当然不会触发这个 View 的点击事件。

五、scrollTo() 与 scollBy()

scrollTo(x, y) 表示移动到一个具体的坐标点,而 scrollBy(dx, dy) 则表示移动的增量为 dx、dy。其中,scollBy 最终也是要调用 scollTo 的。View 的 scollTo 和 scollBy 的源码如下所示:

public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

scollTo、scollBy 移动的是 View 的内容,如果在 ViewGroup 中使用,则是移动其所有的子 View。我们将 ACTION_MOVE 中的代码替换成如下代码:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    ...

    when (event?.action) {
        ...

        MotionEvent.ACTION_MOVE -> {
            // 计算移动的距离
            val offsetX = x ?: (0 - lastX)
            val offsetY = y ?: (0 - lastY)

            (parent as View).scrollBy(-offsetX, -offsetY)
        }
    }

    return true
}

这里若要实现自定义 View 随手指移动的效果,就需要将偏移量设置为负值。为什么要设置为负值呢?这是参考对象不同导致的差异。所以我们用 scrollBy 方法的时候要设置负数才会达到自己想要的效果。

六、Scroller

我们在用 scollTo/scollBy 方法进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可以使用 Scroller 来实现有过渡效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔内完成的。Scroller 本身是不能实现 View 的滑动的,它需要与 View 的 computeScroll() 方法配合才能实现弹性滑动的效果。在这里我们实现自定义 View 平滑地向右移动。首先我们要初始化 Scroller,代码如下所示:

class CustomView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {

    ...
    
    private var scroller: Scroller? = null

    init {
        scroller = Scroller(context)
    }

    ...
}

接下来重写 computeScroll() 方法,系统会在绘制 View 的时候在 draw() 方法中调用该方法。在这个方法中,我们调用父类的 scrollTo() 方法并通过 Scroller 来不断获取当前的滚动值,每滑动一小段距离我们就调用invalidate() 方法不断地进行重绘,重绘就会调用 computeScroll() 方法,这样我们通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果。

override fun computeScroll() {
    super.computeScroll()
    scroller?.let {
        if (it.computeScrollOffset()) {
            (parent as View).scrollTo(it.currX, it.currY)
            invalidate()
        }
    }
}

我们在自定义 View 中写一个 smoothScrollTo 方法,调用 Scroller 的 startScroll() 方法,在 2000ms 内沿 X 轴平移 delta 像素,代码如下所示:

fun smoothScrollTo(destX: Int, destY: Int) {
    val scrollX = scrollX
    val delta = destX - scrollX
    scroller?.startScroll(scrollX, 0, delta, 0, 2000)
    invalidate()
}

最后我们再调用自定义 View 的 smoothScrollTo() 方法。这里我们设定自定义 View 沿着 X 轴向右平移 400 像素。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val testView = findViewById<CustomView>(R.id.test_view)
        testView.smoothScrollTo(-400, 0)
    }
}

运行程序,效果如图4所示:

图4

Android 12 SystemUI 实现左侧导航栏

改变 Android 系统导航栏位置的简易教程

效果图:
效果图


修改代码:

frameworks/base/services/core/java/com/android/server/wm/DisplayPolicy.java

找到 navigationBarPosition 方法,原方法的内容:

@NavigationBarPosition
int navigationBarPosition(int displayWidth, int displayHeight, int displayRotation) {
    if (navigationBarCanMove() && displayWidth > displayHeight) {
        if (displayRotation == Surface.ROTATION_270) {
            return NAV_BAR_LEFT;
        } else if (displayRotation == Surface.ROTATION_90) {
            return NAV_BAR_RIGHT;
        }
    }
    return NAV_BAR_BOTTOM;
}

修改后的方法内容:

@NavigationBarPosition
int navigationBarPosition(int displayWidth, int displayHeight, int displayRotation) {
    return NAV_BAR_LEFT;
}

编译:

make services

输出:

.../out/target/product/你的平台/system/framework/services.jar
.../out/target/product/你的平台/system/framework/services.jar.bprof
.../out/target/product/你的平台/system/framework/services.jar.prof

将输出内容替换到板子里:

adb push ...\services.jar /system/framework/
adb push ...\services.jar.bprof /system/framework/
adb push ...\services.jar.prof /system/framework/

重启即可

ArkUI - 自定义组件

ArkUI - 列表布局(List) 那一篇文章的2024春节档电影新片票房榜列表例子为例:

@Entry
@Component
struct Index {
  ...

  build() {
    Column({ space: 8 }) {
      // 标题部分
      Row() {
        ...
      }

      // 电影列表部分
      List() {
        ForEach(
            ...
        )
      }
    }
  }
}

这是整体的代码结构,省略了细节部分。整个页面是一个从上到下的列式布局,所以我们使用了 Column 容器。然后页面分成了两部分,第一部分是顶部的标题,第二部分是电影列表,由于每一行的内容基本相似,所以我们使用 ForEach 在内部循环渲染电影对应的卡片。

一、创建自定义组件

以上面的标题部分为例,我们知道标题部分其实是一个标准化的功能,也就是说不仅这个页面需要这样的标题,其他页面也需要。比如产品在设计UE时,通常来说会把列表页面和列表详情页的标题部分设置成相似的。如果在每个页面里都写类似的标题代码,复用性会很差,所以为了解决这个问题我们可以把标题部分封装到自定义组件里。

@Component
struct Header {
  build() {
    // 标题部分
    Row() {
      ...
    }
  }
}

我们定义了一个结构体 Header,代表页面的头部。然后加上 @Component 装饰器,这样一个组件就声明出来了。紧接着,可以把标题部分的代码抽取到 builde() { } 里,这样一个可复用的标题的功能就封装好了。

将来在电影列表页面里,我需要写标题,不需要在重新写标题代码了,直接引用这个 Header 组件就行了:

@Entry
@Component
struct Index {
  ...

  build() {
    Column({ space: 8 }) {
      // 标题部分
      Header()

      // 电影列表部分
      List() {
        ForEach(
            ...
        )
      }
    }
  }
}

定义在页面内部的自定义组件完整代码如下:

@Component
struct Header {
  private title: string

  build() {
    Row() {
      Text(this.title)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
    }
  }
}

@Entry
@Component
struct Index {
  ...

  build() {
    Column({ space: 8 }) {
      // 标题部分
      Header({ title: '2024春节档新片票房榜' })
        .margin({ bottom: 20 })

      ...
    }
  }
}

但是,如果定义在页面内部,也就意味着只有在这个页面能用,换一个页面就用不了了。所以最佳的方案时定义在单独的文件里。

在 entry 的 ets 文件夹里新建 components 文件夹,并在里面新建一个 CommonComponents.ets 文件。将上面 Header 代码拷贝到这个文件里,为了能让别的文件使用这段代码,需要做一些修改:

@Component
export struct Header {
  build() {
    Row() {
      ...
    }
  }
}

使用 export 将 Header 导出,这样别的文件才能 import 导入使用:

import { Header } from '../components/CommonComponents'

定义在页面外部的自定义组件完整代码如下:

CommonComponents.ets

@Component
export struct Header {
  private title: string

  build() {
    Row() {
      Text(this.title)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
    }
  }
}

Index.ets

import { Header } from '../components/CommonComponents'

@Entry
@Component
struct Index {
  ...

  build() {
    Column({ space: 8 }) {
      // 标题部分
      Header({ title: '2024春节档新片票房榜' })
        .margin({ bottom: 20 })

      ...
    }
  }
}

二、自定义构建函数 @Builder

这是我们的 Index.ets 代码:

...

@Entry
@Component
struct Index {
  ...

  build() {
    Column({ space: 8 }) {
      ...

      // 电影列表部分
      List({ space: 8 }) {
        ForEach(
          this.items,
          (item: Item) => {
            ListItem() {
              Row({ space: 8 }) {
                Image(item.image)
                  .width(157)
                  .height(220)
                Column() {
                  Text(item.name)
                    .fontSize(20)
                    .fontWeight(FontWeight.Bold)
                  Text(item.box_office)
                    .fontSize(18)
                }
                .height('100%')
                .alignItems(HorizontalAlign.Start)
              }
              .width('100%')
              .height(220)
            }
          }
        )
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .padding(8)
  }
}

从这段代码可以发现,List 里面的代码可读性不高。最好是能把下面这段代码封装起来:

Row({ space: 8 }) {
  Image(item.image)
    .width(157)
    .height(220)
  Column() {
    Text(item.name)
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
    Text(item.box_office)
    .fontSize(18)
  }
  .height('100%')
  .alignItems(HorizontalAlign.Start)
}
.width('100%')
.height(220)

可以使用自定义组件,也可以使用自定构建函数 @Builder,它用来做这种内部的页面封装会更加合适一些。

自定构建函数顾名思义就是用来构建页面的一个函数,可以把相关代码封装进去:

  1. 全局自定义构建函数

    代码结构:

    @Builder function 函数名() {
      ...
    }
    

    完整代码:

    ...
    
    @Builder function ItemCard(item:Item) {
      Row({ space: 8 }) {
        Image(item.image)
          .width(157)
          .height(220)
        Column() {
          Text(item.name)
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
          Text(item.box_office)
            .fontSize(18)
        }
        .height('100%')
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .height(220)
    }
    
    @Entry
    @Component
    struct Index {
      ...
    
      build() {
        Column({ space: 8 }) {
          ...
          
          // 电影列表部分
          List({ space: 8 }) {
            ForEach(
              this.items,
              (item: Item) => {
                ListItem() {
                  ItemCard(item)
                }
              }
            )
          }
          .width('100%')
          .height('100%')
        }
        .width('100%')
        .height('100%')
        .padding(8)
      }
    }
    
  2. 组件内自定义构建函数

    代码结构:

    @Builder 函数名() {
      ...
    }
    

    完整代码:

    ...
    
    @Entry
    @Component
    struct Index {
      ...
    
      build() {
        Column({ space: 8 }) {
          ...
          
          // 电影列表部分
          List({ space: 8 }) {
            ForEach(
              this.items,
              (item: Item) => {
                ListItem() {
                  this.ItemCard(item)
                }
              }
            )
          }
          .width('100%')
          .height('100%')
        }
        .width('100%')
        .height('100%')
        .padding(8)
      }
    
      @Builder ItemCard(item: Item) {
        Row({ space: 8 }) {
          Image(item.image)
            .width(157)
            .height(220)
          Column() {
            Text(item.name)
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
            Text(item.box_office)
              .fontSize(18)
          }
          .height('100%')
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .height(220)
      }
    }
    

三、自定义公共样式 @Styles

@Entry
@Component
struct Index {
  ...

  build() {
    Column() {
      ...
    }
    .width('100%')
    .height('100%')
    .padding(8)
  }

  ...
}

如上代码所示,是一个 Column 容器,这个 Column 是有很多样式的,比如这里的宽100%、高100%等,这种样式可以认为是 App 的统一样式,也就是通用样式,如果每个页面都去写这些代码是不是也是浪费,这种也可以做抽取,这是对样式的抽取,就要用到 @Styles 装饰器。

  1. 全局公共样式

    代码结构:

    @Styles function 函数名() {
      ...
    }

    完整代码:

    @Styles function fillScreen() {
      .width('100%')
      .height('100%')
      .padding(8)
    }
    
    @Entry
    @Component
    struct Index {
      ...
    
      build() {
        Column() {
          ...
        }
        .fillScreen()
      }
    
      ...
    }
  2. 内部共样式

    代码结构:

    @Styles 函数名() {
      ...
    }

    完整代码:

    @Entry
    @Component
    struct Index {
      @Styles function fillScreen() {
        .width('100%')
        .height('100%')
        .padding(8)
      }
      
      ...
    
      build() {
        Column() {
          ...
        }
        .fillScreen()
      }
    
      ...
    }

四、自定义组件特有属性 @Extend

Text(item.name)
  .fontSize(20)
  .fontWeight(FontWeight.Bold)

如上代码所示,fontSize 和 fontWeight 是 Text 组件的特有属性,如果页面中有相同的代码,可以使用 @Extend 抽取。

代码结构:

@Extend(组件) function 函数名() {
  ...
}

完整代码:

@Extend(Text) function nameText() {
  .fontSize(20)
  .fontWeight(FontWeight.Bold)
}

Text(item.name)
  .nameText()

切记 @Extend 不能写在组件内,只能写在全局。

AOSP 架构

Android 官网对 AOSP 结构图进行了更新,如下所示:

  1. Android 应用(Android Apps)

    完全使用 Android API 开发的应用。在某些情况下,设备制造商可能希望预安装 Android 应用以支持设备的核心功能。

  2. 特权应用(Privileged Apps)

    使用 Android 和系统 API 组合创建的应用。这些应用必须作为特权应用预安装在设备上。

  3. 设备制造商应用(Device Manufacturer Apps)

    结合使用 Android API、系统 API 并直接访问 Android 框架实现而创建的应用。由于设备制造商可能会直接访问 Android 框架中的不稳定的 API,因此这些应用必须预安装在设备上,并且只能在设备的系统软件更新时进行更新。

  4. 系统 API(System API)

    系统 API 表示仅供合作伙伴和 OEM 纳入捆绑应用的 Android API。这些 API 在源代码中被标记为 @Systemapi

  5. Android API

    Android API 是面向第三方 Android 应用开发者的公开 API。

  6. Android 框架(Android Framework)

    构建应用所依据的一组 Java 类、接口和其他预编译代码。框架的某些部分可通过使用 Android API 公开访问。框架的其他部分只能由 OEM 通过系统 API 来访问。Android 框架代码在应用进程内运行。

    下面来看这一层所提供的主要组件:

    名称 功能描述
    Activity Manager(活动管理器) 管理各个应用程序生命周期,以及常用的导航回退功能
    Location Manager(位置管理器) 提供地理位置及定位功能服务
    Package Manager(包管理器) 管理所有安装在 Android 系统中的应用程序
    Notification Manager(通知管理器) 使得应用程序可以在状态栏中显示自定义的提示信息
    Resource Manager(资源管理器) 提供应用程序使用的各种非代码资源,如本地化字符、图片、布局文件、颜色文件等
    Telephony Manager(电话管理器) 管理所有的移动设备功能
    Window Manager(窗口管理器) 管理所有开启的窗口程序
    Content Provider(内容提供器) 使得不同应用程序之间可以共享数据
    View System(视图系统) 构建应用程序的基本组件
  7. 系统服务(System Services)

    系统服务是重点突出的模块化组件,例如 system_server、SurfaceFlinger 和 MediaService。Android 框架 API 提供的功能可以与系统服务进行通信,以访问底层硬件。

  8. Android 运行时 (Android Runtime)

    AOSP 提供的 Java 运行时环境。 ART 会将应用的字节码转换为由设备运行时环境执行的处理器专有指令。

  9. 硬件抽象层 (HAL)

    HAL 是一个抽象层,其中包含硬件供应商要实现的标准接口。借助 HAL,Android 可以忽略较低级别的驱动程序实现。借助 HAL,您可以顺利实现相关功能,而不会影响或更改更高级别的系统。

  10. 原生守护程序和库(Native Daemons and Libraries)

    该层中的原生守护程序包括 inithealthdlogdstoraged。这些守护程序直接与内核或其他接口进行交互,并且不依赖于基于用户空间的 HAL 实现。

    该层中的原生库包括 libclibloglibutilslibbinderlibselinux。这些原生库直接与内核或其他接口进行交互,并且不依赖于基于用户空间的 HAL 实现。

  11. 内核(Linux Kernel)

    内核是任何操作系统的中心部分,并与设备上的底层硬件进行通信。尽可能将 AOSP 内核拆分为与硬件无关的模块和特定于供应商的模块。

hdc 环境变量设置

hdc(HarmonyOS Device Connector)是 HarmonyOS 为开发人员提供的用于调试的命令行工具,通过该工具可以在 windows/linux/mac 系统上与真实设备或者模拟器进行交互。

hdc 工具通过 HarmonyOS SDK 获取,存放于 /Huawei/Sdk/openharmony/版本号/toolchains/ 目录下。

一、Windows 系统 hdc 环境变量设置方法

  1. 打开环境变量

    右键 此电脑 > 属性 > 高级系统设置 > 高级 > 环境变量
    
  2. 新建系统变量

    环境变量 > 系统环境 > 新建
    
    变量名:OHOS_HOME
    变量值:D:\Huawei\Sdk
    
  3. 添加到 Path

    找到 环境变量 > 系统环境 > Path 变量
    
    然后双击打开,点击新建
    
    在最后一行填写 %OHOS_HOME%\openharmony\版本号\toolchains
    
  4. 检查是否已经成功配置 hdc

    在终端输入 hdc version

    如果出现类似如下内容证明 hdc 配置成功:
    windows_hdc

二、macOS 系统 hdc 环境变量设置方法

  1. 编辑 .zshrc 文件

    vi ~/.zshrc
  2. 在 .zshrc 文件中添加以下内容

    export HMOS_HOME=/Users/用户/Library/Huawei/Sdk
    export PATH=${PATH}:${HMOS_HOME}/openharmony/版本号/toolchains
  3. 保存 .zshrc 文件,并退出

    点击 esc
    输入 :wq
    回车
  4. 加载并执行 .zshrc 文件

    source ~/.zshrc
  5. 检查是否已经成功配置 hdc

    在终端输入 hdc version

    如果出现类似如下内容证明 hdc 配置成功:
    macOS_hdc

三、Linux 系统 hdc 环境变量设置方法

未完待续。。。

了解 ArkTS 语言

一、传统网页开发(HTML、CSS、JavaScript)

  1. HTML 控制页面元素
  2. CSS 控制页面布局和样式
  3. JavaScript 控制页面逻辑和数据状态。

二、ArkTS

实现网页开发需要同时掌握三种不同的开发语言(HTML、CSS、JavaScript),这三种语言的语法完全不同,所以说开发体验完全不统一,但是现在有了 ArkTS:

  1. ArkTS 基于 TypeScript,而 TypeScript 又是基于 JavaScrpit,在 JavaScript 基础上进行了加强和拓展。因此,ArkTS 就具备 JavaScript 这些能力,像做页面逻辑控制、数据状态控制完全可以用 ArkTS 来实现。不仅如此,TypeScript 在 JavaScript 的基础上增加了静态类型定义等功能,因此,TypeScript 的拓展能力就变得更强了,而 ArkTS 又是基于 TypeScript 的,所以 TypeScript 的特性,ArkTS 都具备,因此 ArkTS 不仅仅能完成 JavaScript 的工作,而且还可能比 JavaScript 做的更好。

  2. ArkTS 在 TypeScript 的基础上拓展了新的功能,比如声明式UI、状态管理等。

    所谓的 声明式UI 简单来讲就是 我需要什么我就声明什么

    1-2

    • 声明一个按钮:

      build() {
          Button('点我0次')
      }
      
    • 修改按钮背景色

      build() {
          Button('点我0次')
          .backGroundColor('#36D')
      }
      
    • 修改按钮位置为居中

      build() {
          Button('点我0次')
          .backGroundColor('#36D')
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      
    • 实现点击效果、并修改按钮文字内容

      times: number = 0
      build() {
          Button('点我${this.times}次')
          .backGroundColor('#36D')
          .onClick(() => this.times++)
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      
  3. ArkTS 开发效率高、开发体验好

    ArkTS 尽管是用声明式UI来实现的前端,但是底层有一个方舟编译器,这个编译器会把我们写的 TypeScript 编译成字节码,最终转换成机器码去运行。而且,会把从字节码到机器码这样一个转译的动作从运行期提前到编译期,从而大大提高效率,这就是方舟编译器的AOT技术。不仅如此,它还有一套统一的UI后端引擎,提供了一些统一的页面渲染指令,当我们的不同应用在去渲染的时候,其实只需要调用这些指令就行,而这些指令会统一提交到渲染总线,最后传递给操作系统底层高效渲染引擎,这个引擎会对页面渲染的UI指令再次优化,从而大大提高页面渲染的效率。因此,ArkTS 尽管是用 JS 语言写的,但是执行的性能可以说是非常的好。不仅如此,鸿蒙系统底层为 ArkTS 提供了不同系统跨平台的适配层和桥接层,因此,利用 ArkTS 来开发应用还具备多系统适配和接入能力。

    2

三、总结

综上所属,用 ArkTS 来开发鸿蒙应用,不仅简单,而且运行性能更好,还能跨系统去做适配,可以说非常强大。

系统启动流程分析 —— SystemServer 处理过程

本文基于 Android 14.0.0_r2 的系统启动流程分析。

SystemServer 进程主要用于创建系统服务,我们熟知的 AMS、WMS 和 PMS 都是由它来创建的,因此掌握 SystemServer 进程是如何启动的,它在启动时做了哪些工作是十分必要的。

一、源码解析

  1. ZygoteInithandleSystemServerProcess 下面的子方法去调用了 com.android.server.SystemServermain 方法,至此 SystemServer 就创建和启动完毕了:

    路径:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    
    public static void main(String[] argv) {
        ...
    
        Runnable caller;
        try {
            ...
    
            if (startSystemServer) {
                Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);
    
                ...
            }
    
            ...
        }
    
        ...
    }
    路径:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    
    private static Runnable forkSystemServer(String abiList, String socketName,
            ZygoteServer zygoteServer) {
    
        ...
    
        if (pid == 0) {
            ...
    
            return handleSystemServerProcess(parsedArgs);
        }
    
        ...
    }
    路径:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    
    private static Runnable handleSystemServerProcess(ZygoteArguments parsedArgs) {
        ...
    
        if (parsedArgs.mInvokeWith != null) {
            ...
        } else {
            ...
    
            return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
                    parsedArgs.mDisabledCompatChanges,
                    parsedArgs.mRemainingArgs, cl);
        }
    }
    路径:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    
    public static Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
            String[] argv, ClassLoader classLoader) {
        ...
    
        return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
                classLoader);
    }
    路径:/frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
    
    protected static Runnable applicationInit(int targetSdkVersion, long[] disabledCompatChanges,
            String[] argv, ClassLoader classLoader) {
        ...
    
        return findStaticMain(args.startClass, args.startArgs, classLoader);
    }
    路径:/frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
    
    protected static Runnable findStaticMain(String className, String[] argv,
            ClassLoader classLoader) {
        Class<?> cl;
    
        try {
            // 反射拿到 SystemServer 类
            cl = Class.forName(className, true, classLoader);
        } catch (ClassNotFoundException ex) {
            ...
        }
    
        Method m;
        try {
            // 反射拿到 SystemServer.java 的 main 函数,并启动。
            m = cl.getMethod("main", new Class[] { String[].class });
        } catch (NoSuchMethodException ex) {
            ...
        } catch (SecurityException ex) {
            ...
        }
    
        ...
    
        return new MethodAndArgsCaller(m, argv);
    }
  2. SystemServer.run

    路径:/frameworks/base/services/java/com/android/server/SystemServer.java
    
    public static void main(String[] args) {
        new SystemServer().run();
    }
    路径:/frameworks/base/services/java/com/android/server/SystemServer.java
    
    private void run() {
        ...
    
        try {
            ...
    
            // 设置系统语言、国家、时区相关。
            if (!SystemProperties.get("persist.sys.language").isEmpty()) {
                final String languageTag = Locale.getDefault().toLanguageTag();
    
                SystemProperties.set("persist.sys.locale", languageTag);
                SystemProperties.set("persist.sys.language", "");
                SystemProperties.set("persist.sys.country", "");
                SystemProperties.set("persist.sys.localevar", "");
            }
    
            ...
    
            // 设置 main 线程的优先级,有此可得主线程就是 SystemServer 进程下的其中线程。
            android.os.Process.setThreadPriority(
                    android.os.Process.THREAD_PRIORITY_FOREGROUND);
            android.os.Process.setCanSelfBackground(false);
            // 开始主线程的运行,和 Looper.loop 配对使用。
            // 运行在 Looper.prepareMainLooper() ~ Looper.loop(),之间的就是运行在主线程中。
            Looper.prepareMainLooper();
            Looper.getMainLooper().setSlowLogThresholdMs(
                    SLOW_DISPATCH_THRESHOLD_MS, SLOW_DELIVERY_THRESHOLD_MS);
    
            ...
    
            // 初始化 native services,加载 android_servers 库(libandroid_servers.so)。
            System.loadLibrary("android_servers");
    
            ...
    
            // 通过 ActivityThread 来创建 system 上下文。
            createSystemContext();
    
            // 初始化 ActivityThread,创建 TelephonyServiceManager、StatsServiceManager、MediaServiceManager。
            ActivityThread.initializeMainlineModules();
    
            // 将 SystemServer 加入 ServiceManager(binder 线程池)。
            // 每个继承自 SystemServer 或属于 SystemServer 进程的服务都将加入到 ServiceManager 中的线程池中。
            ServiceManager.addService("system_server_dumper", mDumper);
            mDumper.addDumpable(this);
    
            // 每个 server 基本上对应了一个 manager,对外提供的 API 也是只能获取到 manager。
            // 创建 SystemServiceManager,它会对系统的服务进行创建、启动和生命周期管理,启动系统的各种服务。
            mSystemServiceManager = new SystemServiceManager(mSystemContext);
            mSystemServiceManager.setStartInfo(mRuntimeRestart,
                    mRuntimeStartElapsedTime, mRuntimeStartUptime);
            mDumper.addDumpable(mSystemServiceManager);
    
            // LocalServices 是 system_server 进程中各个服务提供的本地服务。
            // system_server 进程中每个服务都可以往 LocalServices 放对象。
            // 有些核心服务是继承自 SystemServer,LocalServices 是公开缓存池目的是解耦。
            LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);
            
            ...
    
        }
    
        ...
    
        // 启动 Services。
        try {
            t.traceBegin("StartServices");
            // 启动系统启动所需的一系列关键服务:AMS,P(power/package)MS,SensorService,DisplayManagerService,LightService 等。
            startBootstrapServices(t);
            // 启动核心服务:BatteryService,GpuService 等。
            startCoreServices(t);
            // 启动其他服务:VibratorManagerService,闹钟服务,相机服务,网络服务,输入法服务,存储服务等。
            startOtherServices(t);
            
            // 以上的所有服务都由 mSystemServiceManager 来启动,所以都是继承自 SystemServer。
            // 分别是引导服务、核心服务和其他服务
            // [引导服务]
            // Installer                   系统安装 apk 时的一个服务类,启动完成 Installer 服务之后才能启动其他的系统服务。
            // ActivityManagerService      负责四大组件的启动、切换、调度。
            // PowerManagerService         计算系统中和Power相关的计算,然后决策系统应该如何反应。
            // LightsService               管理和显示背光LED。
            // DisplayManagerService       用来管理所有显示设备。
            // UserManagerService          多用户模式管理。
            // SensorService               为系统提供各种感应器服务。
            // PackageManagerService       用来对 apk 进行安装、解析、删除、卸载等等操作。
            // [核心服务]
            // BatteryService              管理电池相关的服务。
            // UsageStatsService           收集用户使用每一个 APP 的频率、使用时常。
            // WebViewUpdateService        WebView 更新服务。
            // [其他服务]
            // CameraService               摄像头相关服务。
            // AlarmManagerService         全局定时器管理服务。
            // InputManagerService         管理输入事件。
            // WindowManagerService        窗口管理服务。
            // VrManagerService            VR模式管理服务。
            // BluetoothService            蓝牙管理服务。
            // NotificationManagerService  通知管理服务。
            // DeviceStorageMonitorService 存储相关管理服务。
            // LocationManagerService      定位管理服务。
            // AudioService                音频相关管理服务。
    
            ...
        }
    
        ...
    
        // 主线程
        Looper.loop();
        // 若执行到这里说明主线程意外退出了。
        // 主线程:Looper.prepareMainlooper ~ Looper.loop 之间。
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

    以上方法可以看出来关于其他服务的启动都是运行在主线程中的 Looper.prepareMainlooper ~ Looper.loop 之间,每个 SystemServer 中的服务都有一个 binder,会加入到 ServiceManager 的 binder 线程池中统一管理,这样拿到全局的 ServiceManager,根据 AIDL 获取到每个 Service 了。

    • startBootstrapServices(t)

      启动系统启动所需的一系列关键服务:AMS,P(power/package)MS,SensorService,DisplayManagerService,LightService 等。

    • startCoreServices(t)

      启动核心服务:BatteryService,GpuService 等。

    • startOtherServices(t)

      启动其他服务:VibratorManagerService,闹钟服务,相机服务,网络服务,输入法服务,存储服务等。

    在这些启动的服务里(调用了 onStart 启动服务),都会将服务存入 ServiceManager 用来管理系统中的各种 Service,用于系统 C/S 架构中的 Binder 机制通信:Client 端要使用某个 Service,则需要先到 ServiceManager 查询 Service 的相关信息,然后根据 Service 的相关信息与 Service 所在的 Server 进程建立通讯通路,这样 Client 端就可以使用 Service 了。

  3. 各类系统服务的作用

    引导服务(startBootstrapServices) 作用
    ActivityManagerService 负责四大组件的启动、切换、调度
    PowerManagerService 计算系统中和 Power 相关的计算,然后决策系统应该如何反应
    Installer 系统安装 apk 时的一个服务类,启动完成 Installer 服务之后才能启动其他的系统服务
    LightsService 管理和显示背光 LED
    DisplayManagerService 用来管理所有显示设备
    PackageManagerService 用来对 apk 进行安装、解析、删除、卸载等等操作
    SensorService 为系统提供各种感应器服务
    核心服务(startCoreServices) 作用
    BatteryService 管理电池相关的服务
    GpuService 硬件显示服务
    WebViewUpdateService WebView 更新服务
    其他服务(startOtherServices) 作用
    CameraService 摄像头相关服务
    AlarmManagerService 全局定时器管理服务
    InputManagerService 管理输入事件
    WindowManagerService 窗口管理服务
    BluetoothService 蓝牙管理服务
    PackageManagerService 用来对 apk 进行安装、解析、删除、卸载等等操作
    SensorService 为系统提供各种感应器服务
    LocationManagerService 定位管理服务
    ... ...
  4. SystemServer.startBootstrapServices

    路径:/frameworks/base/services/java/com/android/server/SystemServer.java
    
    private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) {
        ...
    
        // 尽早启动看门狗,以便在早期启动过程中出现死锁时使系统服务器崩溃。
        t.traceBegin("StartWatchdog");
        // 启动看门狗,看门狗需要定时喂狗,若喂狗超时则会触发重启,以便知道进程和服务是否正常运行。
        final Watchdog watchdog = Watchdog.getInstance();
        watchdog.start();
        mDumper.addDumpable(watchdog);
        t.traceEnd();
    
        ...
    
        t.traceBegin("StartInstaller");
        // 通过 mSystemServiceManager 来启动 Installer 服务,管理应用的安装与卸载。
        Installer installer = mSystemServiceManager.startService(Installer.class);
        t.traceEnd();
    
        ...
    
        // 通过 mSystemServiceManager 来启动 UriGrantsManagerService,管理 Uri。
        t.traceBegin("UriGrantsManagerService");
        mSystemServiceManager.startService(UriGrantsManagerService.Lifecycle.class);
        t.traceEnd();
    
        // 通过 mSystemServiceManager 来启动 PowerStatsService,管理电源状态。
        t.traceBegin("StartPowerStatsService");
        mSystemServiceManager.startService(PowerStatsService.class);
        t.traceEnd();
    
        ...
    
        t.traceBegin("StartActivityManager");
        // 通过 mSystemServiceManager 来启动 ActivityTaskManagerService,管理 Activity 任务栈。
        ActivityTaskManagerService atm = mSystemServiceManager.startService(
                ActivityTaskManagerService.Lifecycle.class).getService();
        // 启动 ActivityManagerService,管理 Activity 等。
        mActivityManagerService = ActivityManagerService.Lifecycle.startService(
                mSystemServiceManager, atm);
        // 让 ActivityManagerService 拿到 systemServer,例如可以通过 mSystemServiceManager 来判断系统是否启动完成。
        mActivityManagerService.setSystemServiceManager(mSystemServiceManager);
        mActivityManagerService.setInstaller(installer);
        mWindowManagerGlobalLock = atm.getGlobalLock();
        t.traceEnd();
    
        ...
    
        // 启用 PowerManagerService 服务,电源管理服务。
        t.traceBegin("StartPowerManager");
        mPowerManagerService = mSystemServiceManager.startService(PowerManagerService.class);
        t.traceEnd();
    
        ...
    
        // 启动屏幕亮度服务,比如亮度调整。
        t.traceBegin("StartLightsService");
        mSystemServiceManager.startService(LightsService.class);
        t.traceEnd();
    
        ...
    
        // 启动屏幕显示服务。
        t.traceBegin("StartDisplayManager");
        mDisplayManagerService = mSystemServiceManager.startService(DisplayManagerService.class);
        t.traceEnd();
    
        ...
    
        try {
            ...
    
            // 启动 PMS,包管理服务。
            mPackageManagerService = PackageManagerService.main(
                    mSystemContext, installer, domainVerificationService,
                    mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF);
        } finally {
            Watchdog.getInstance().resumeWatchingCurrentThread("packagemanagermain");
        }
    
        ...
    
        // 启动传感器服务。
        t.traceBegin("StartSensorService");
        mSystemServiceManager.startService(SensorService.class);
        t.traceEnd();
        t.traceEnd();
    }

    可以看到大多数服务都是通过 mSystemServiceManager.startService 来启动,接下来看看 startService 方法内容。

  5. SystemServiceManager.startService

    路径:/frameworks/base/services/core/java/com/android/server/SystemServiceManager.java
    
    public <T extends SystemService> T startService(Class<T> serviceClass) {
        try {
            final String name = serviceClass.getName();
            
            ...
    
            final T service;
            try {
                // 反射拿到该 Java 类
                Constructor<T> constructor = serviceClass.getConstructor(Context.class);
                service = constructor.newInstance(mContext);
            }
    
            ...
    
            // 将当前服务(Java类)加入 SystemService 服务队列中,统一管理。
            startService(service);
            return service;
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
        }
    }
    路径:/frameworks/base/services/core/java/com/android/server/SystemServiceManager.java
    
    public void startService(@NonNull final SystemService service) {
        ...
    
        // 将当前服务加入 mServices 队列中。
        mServices.add(service);
    
        long time = SystemClock.elapsedRealtime();
        try {
            // 调用当前服务的 onStart 来启动服务。
            service.onStart();
        } catch (RuntimeException ex) {
            ...
        }
        warnIfTooLong(SystemClock.elapsedRealtime() - time, service, "onStart");
    }

    可以看到 startService 方法就是反射拿到服务类,然后加入队列中,调用其 onStart 方法进行启动。

  6. ServiceManager 服务管理

    每个属于 SystemServer 的服务都将加入到 ServiceManager 的 binder 线程池中,以供后续直接获取和管理。就拿 BatteryService 服务来讲解:

    路径:/frameworks/base/services/java/com/android/server/SystemServer.java
    
    mSystemServiceManager.startService(BatteryService.class);

    已知 startService 后会调用 BatteryService 服务的 onStart 方法,继续看看 onStart 内部:

    路径:/frameworks/base/services/core/java/com/android/server/BatteryService.java
    
    @Override
    public void onStart() {
        ...
    
        mBinderService = new BinderService();
        // 将 BinderService 服务加入 ServiceManager 中。
        publishBinderService("battery", mBinderService);
        mBatteryPropertiesRegistrar = new BatteryPropertiesRegistrar();
        // 将 batteryproperties 服务加入 ServiceManager 中。
        publishBinderService("batteryproperties", mBatteryPropertiesRegistrar);
        // 将 BinderService 服务加入到 LocalServices 中。
        publishLocalService(BatteryManagerInternal.class, new LocalService());
    }

    继续看看 mBinderService 具体是什么,又是如何加入到 ServiceManager 中的:

    路径:/frameworks/base/services/core/java/com/android/server/BatteryService.java
    
    private final class BinderService extends Binder {
        ...
    }

    可以看到 mBinderService 就是一个 Binder,然后调用 publishBinderService 加入到 ServiceManager 中的 binder 线程池中:

    路径:/frameworks/base/services/core/java/com/android/server/SystemService.java
    
    protected final void publishBinderService(String name, IBinder service,
            boolean allowIsolated, int dumpPriority) {
        ServiceManager.addService(name, service, allowIsolated, dumpPriority);
    }

    调用 ServiceManager.addService 加入到 binder 线程池中,而 ServiceManager 服务早就在 servicemanager.rc 文件中作为核心服务启动了:

    路径:/frameworks/native/cmds/servicemanager/servicemanager.rc
    
    service servicemanager /system/bin/servicemanager
        class core animation
        user system
        group system readproc
        critical
        file /dev/kmsg w
        onrestart setprop servicemanager.ready false
        onrestart restart --only-if-running apexd
        onrestart restart audioserver
        onrestart restart gatekeeperd
        onrestart class_restart --only-enabled main
        onrestart class_restart --only-enabled hal
        onrestart class_restart --only-enabled early_hal
        task_profiles ServiceCapacityLow
        shutdown critical
    

二、总结

SystemServer 作为 Android 系统的核心组成部分之一,通过 fork 自 init 进程,在启动过程中创建并启动各类系统服务(核心服务、引导服务、其他服务),并将这些服务内建的 Binder 实例注册到 ServiceManager 的 binder 线程池中,从而实现了系统服务的高效管理和跨进程通信。

Ubuntu 用 VMware 安装 macOS

本教程使用 Ubuntu 20.04.6 LTS,VMware Workstation Pro 17.5.1,macOS Sonoma 14.4。文中所有需要的下载链接均以 Markdown 的形式体现在文字上。

  1. 下载 VMware Workstation Pro,目前最新版本是 17.5.1。

  2. 使用密钥,进行破解。

  3. VMware 默认不支持安装 macOS,需要下载解锁工具 Unlocker,目前最新版是 4.2.7,但是作者已经不更新了。

  4. 解压 Unlocker,找到 linux 文件夹,然后给 unlock 权限:

    chmod 777 unlock
  5. 使用超级用户权限运行 unlock

    sudo ./unlock
  6. 打开 VMware,添加虚拟机,就能看到 Apple OS X 选项。

  7. 下载 macOS 系统,记住一定要找 ISO 格式的,目前最新的系统是 macOS Sonoma 14.4

  8. 新建 macOS 虚拟机

    New Virtial Machine $\rightarrow$ Typical $\rightarrow$ I will install the operating system later $\rightarrow$ Apple OS X $\rightarrow$ macOS 14 $\rightarrow$ next

  9. 在虚拟机上配置 macOS 镜像

    Edit virtual machine settings $\rightarrow$ CD/DVD $\rightarrow$ Use ISO image

    Image
  10. 安装 macOS

    Image Image Image Image Image Image
  11. 成果

    Image

网络连接 - Http 请求数据

在日常开发应用当中,应用内部有很多数据并不是保存在应用内部,而是在服务端。所以就需要向服务端发起请求,由服务端返回数据。这种请求方式就是 Http 请求。

一、申请网络权限

module.json5 文件中,添加网络权限:

{
  "module": {
    
    ...

    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "usedScene": {
          "when": "always"
        }
      }
    ]
  }
}

二、导入 http 模块

import http from '@ohos.net.http';

三、使用 http 模块发送请求,处理响应

  1. 创建一个 http 的请求对象

    let httpRequest = http.createHttp()

    createHttp() 方法创建的对象是不可复用的,也就是说利用它创建的对象发请求,发一次就不能再发请求了,只能发一次,下次再发还得创建一个新的 request 对象。

  2. 发起网络请求

    httpRequest.request(
      'https://www.wutianhao.com',
      {
          method: http.RequestMethod.GET,
          extraData: { 'param1': 'value1' }
      }
    )

    request() 方法接收两大参数:

    • url:请求的 URL 路径
    • options:请求选项 HttpRequestOptions

    HttpRequestOptions 支持的字段:

    名称 类型 描述
    method RequestMethod 请求方式,GET、POST、PUT、DELETE 等
    extraData string 或 Object 请求参数
    header Object 请求头字段
    connectTimeout number 连接超时时间,单位 ms,默认是 60000 ms
    readTimeout number 读取超时时间,单位 ms,默认是 60000 ms
  3. 处理响应结果

    .then((resp: http.HttpResponse) => {
      if (resp.responseCode === 200) {
        // 请求成功
      }
    })
    .catch((err: Error) => {
       // 请求失败
    })

    整个请求是通过异步事件处理的,凡是这种异步任务都会返回一个 Promise 结果。Promise 顾名思义就是许诺,它里面存放的是未来会完成的结果。给 Promise 添加成功和失败的函数,将来,如果这个异步事件处理完,就会返回相应的回调。

    Promise 提供了两个方法:

    • then():添加成功回调函数,当异步事件处理成功时,会调用这个函数。
    • catch():添加失败回调函数,当异步事件处理失败时,会调用这个函数。

    异步任务发的是 Http 请求,因此,如果成功得到的自然就是 Http 的响应结果,也就是 HttpResponse。不过需要注意这个 response 并不是咱们想要的那个直接的数据,这里采用 Http 协议,所以它返回的是一种通用的 Http 响应结果。

    HttpResponse 支持的字段:

    名称 类型 描述
    responseCode ResponseCode 响应状态码
    header Object 响应头
    cookies string 响应返回的 cookies
    result string或Object 响应体,默认是 JSON 字符串
    resultType HttpDataType 返回值类型

源码中的工厂方法模式

本文是基于 Android 14 的源码解析

工厂方法模式应用很广泛,我们平时开发中经常会使用到的数据结构中其实也隐藏着对工厂方法模式的应用,以 List 和 Set 为例,List 和 Set 都继承于 Collection 接口,而 Collection 接口继承于 Iterable 接口,Iterable 接口如下:

public interface Iterable<T> {
    Iterator<T> iterator();
    
    ...
}

这意味着 List 和 Set 接口也会继承该方法,平时比较常用的两个间接实现类 ArrayList 和 HashSet 中 iterator 方法的实现就是构造并返回一个迭代器对象:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    public Iterator<E> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<E> {
        int cursor = 0;

        int lastRet = -1;

        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size();
        }

        public E next() {
            checkForComodification();
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException(e);
            }
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
}
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
}

HashSet 的 iterator 方法中会返回成员变量 map 中对应 HashSet 对象元素的迭代器对象,最终返回的是 KeySet 中的一个迭代器对象:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    final class KeySet extends AbstractSet<K> {
        public final Iterator<K> iterator() {
            return new KeyIterator();
        }
    }
}

ArrayList 和 HashSet 中的 iterator 方法其实就相当于一个工厂方法,专为 new 对象而生,这里 iterator 方法是构造并返回一个具体的迭代器。

Android 中对工厂方法模式的应用更多,相信以下代码对一个 Android 新手来说都不陌生:

class TestAActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

实质上,onCreate 方法就相当于一个工厂方法,在 TestAActivity 的 onCreate 方法中构造一个 View 对象,并设置为当前界面的 ContentView 返回给 Framework 处理,如果现在又有一个 TestBActivity,这时我们又在其 onCreate 方法中通过 setContentView 方法设置另外不同的 View,这是不是就是一个工厂模式的结构呢?其实设计模式离我们非常近!

源码中的单例模式

本文是基于 Android 14 的源码解析

在 Android 系统中,我们经常会通过 Context 获取系统级别的服务,如 WindowsManagerService、ActivityManagerService 等,更常用的是一个 LayoutInflater 的类,这些服务会在合适的时候以单例的形式注册在系统中,在我们需要的时候就通过 Context 的 getSystemService(String name) 获取。

我们以 LayoutInflater 为例来说明,平时我们使用 LayoutInflater 较为常见的地方是在 RecyclerView 的 onCreateViewHolder 方法中:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestViewHolder {
    return TestViewHolder(
        ListItemTestBinding.inflate(
            LayoutInflater.from(parent.context), parent, false
        )
    )
}

通常我们使用 LayoutInflater.from(Context context) 来获取 LayoutInflater 服务,下面看看源码实现:

public static LayoutInflater from(@UiContext Context context) {
    LayoutInflater LayoutInflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) {
        throw new AssertionError("LayoutInflater not found.");
    }
    return LayoutInflater;
}

可以看到 from(Context context) 函数内部调用的是 Context 类的 getSystemService(String name) 方法,我们跟踪到 Context 类看到,该类是抽象类:

public abstract class Context {
    ...
}

onCreateViewHolder 中使用的 Context 对象的具体实现类是什么呢?其实在 Application、Activity、Service 中都会存在一个 Context 对象,即 Context 的总个数为 Activity 个数 + Service 个数 + 1。而 RecyclerView 通常都是显示在 Activity 中,那么我们就以 Activity 中的 Context 来分析。

我们知道,一个 Activity 的入口是 ActivityThread 的 main 函数,在 main 函数中创建一个新的 ActivityThread 对象,并且启动消息循环(UI线程),创建新的 Activity、新的 Context 对象,然后将该 Context 对象传递给 Activity。下面看看 ActivityThread 源代码:

// 源码路径:/frameworks/base/core/java/android/app/ActivityThread.java

public static void main(String[] args) {
    ...

    // 主线程消息循环
    Looper.prepareMainLooper();

    ...

    // 创建ActivityThread对象
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    ...

    Looper.loop();

    ...
}
// 源码路径:/frameworks/base/core/java/android/app/ActivityThread.java

private void attach(boolean system, long startSeq) {
    ...

    // 不是系统应用的情况
    if (!system) {
        android.ddm.DdmHandleAppName.setAppName("<pre-initialized>",
                                                UserHandle.myUserId());
        RuntimeInit.setApplicationObject(mAppThread.asBinder());
        final IActivityManager mgr = ActivityManager.getService();
        try {
            // 关联 mAppThread
            mgr.attachApplication(mAppThread, startSeq);
        } catch (RemoteException ex) {
            ...
        }
        
        ...
    } else {
        ...
    }

    ...
}

在 main 方法中,创建一个 ActivityThread 对象后,调用了其 attach 函数,并且参数为 false。在 attach 函数中,参数为 false 的情况下(即非系统应用),会通过 Binder 机制与 ActivityManager Service 通信,并且最终调用 handleLaunchActivity 函数,我们看看该函数的实现:

// 源码路径:/frameworks/base/core/java/android/app/ActivityThread.java

public Activity handleLaunchActivity(ActivityClientRecord r,
        PendingTransactionActions pendingActions, int deviceId, Intent customIntent) {
    ...

    final Activity a = performLaunchActivity(r, customIntent);

    ...
}
// 源码路径:/frameworks/base/core/java/android/app/ActivityThread.java

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...

    ContextImpl appContext = createBaseContextForActivity(r);
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        // 创建Activity
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);

        ...
    } catch (Exception e) {
        ...
    }

    try {
        // 创建 Application 对象
        Application app = r.packageInfo.makeApplicationInner(false, mInstrumentation);

        ...

        if (activity != null) {
            // 获取 Context 对象
            CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
            Configuration config =
                    new Configuration(mConfigurationController.getCompatConfiguration());
            
            ...

            // 将 appContext 等对象 attach 到 activity 中
            activity.attach(appContext, this, getInstrumentation(), r.token,
                    r.ident, app, r.intent, r.activityInfo, title, r.parent,
                    r.embeddedID, r.lastNonConfigurationInstances, config,
                    r.referrer, r.voiceInteractor, window, r.activityConfigCallback,
                    r.assistToken, r.shareableActivityToken);

            ...
            
            if (r.isPersistable()) {
                ...
            } else {
                // 调用 Activity 的 onCreate 方法
                mInstrumentation.callActivityOnCreate(activity, r.state);
            }

            ...
        }
        r.setState(ON_CREATE);

    } catch (SuperNotCalledException e) {
        ...
    } catch (Exception e) {
        ...
    }

    return activity;
}
// 源码路径:/frameworks/base/core/java/android/app/ActivityThread.java

private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
    final int displayId = ActivityClient.getInstance().getDisplayId(r.token);
    // 创建 Context 对象, 可以看到实现类是 ContextImpl
    ContextImpl appContext = ContextImpl.createActivityContext(
            this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);

    final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
    String pkgName = SystemProperties.get("debug.second-display.pkg");
    if (pkgName != null && !pkgName.isEmpty()
            && r.packageInfo.mPackageName.contains(pkgName)) {
        for (int id : dm.getDisplayIds()) {
            if (id != DEFAULT_DISPLAY) {
                Display display =
                        dm.getCompatibleDisplay(id, appContext.getResources());
                appContext = (ContextImpl) appContext.createDisplayContext(display);
                break;
            }
        }
    }
    return appContext;
}

通过上面的代码分析可以知道,Context 的实现类为 ComtextImpl。我们继续跟踪 ContextImpl 类:

// 源码路径:/frameworks/base/core/java/android/app/ContextImpl.java

class ContextImpl extends Context {

    ...

    @UnsupportedAppUsage
    final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();

    ...

    @Override
    public Object getSystemService(String name) {
        ...

        return SystemServiceRegistry.getSystemService(this, name);
    }

    ...
}
// 源码路径:/frameworks/base/core/java/android/app/SystemServiceRegistry.java

public final class SystemServiceRegistry {
    ...

    // Service 容器
    private static final Map<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new ArrayMap<String, ServiceFetcher<?>>();
    private static int sServiceCacheSize;

    ...

    // 静态语句块,第—次加载该类时执行(只执行—次,保证实例的唯一性)
    static {
        registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});
    }

    ...

    public static Object[] createServiceCache() {
        return new Object[sServiceCacheSize];
    }

    // 根据 key 获取对应的服务
    public static Object getSystemService(ContextImpl ctx, String name) {
        ...
        
        final ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        
        ...

        final Object ret = fetcher.getService(ctx);
        
        ...

        return ret;
    }

    // 注册 Service
    private static <T> void registerService(@NonNull String serviceName,
            @NonNull Class<T> serviceClass, @NonNull ServiceFetcher<T> serviceFetcher) {
        ...

        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
        
        ...
    }

    ...
}

从 ContextImpl 类的部分代码中可以看到,在虚拟机第一次加载该类时会注册各种 ServiceFetcher,其中就包含了 LayoutInflater Service。将这些服务以键值对的形式存储在一个 Map 中,用户使用时只需要根据 key 来获取到对应的 ServiceFetcher,然后通过 ServiceFetcher 对象的 getService 函数来获取具体的服务对象。当第一次获取时,会调用 ServiceFetcher 的 createService 函数创建服务对象,然后将该对象缓存到一个列表中,下次再取时直接从缓存中获取,避免重复创建对象,从而达到单例的效果。

View 与 ViewGroup

View 是 Android 所有控件的基类,我们平常所用的 TextView 和 ImageView 都是继承自 View 的,源码如下:

public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
    ...
}

public class ImageView extends View {
    ...
}

接着看看我们平常用的布局控件 RelativeLayout,它继承自 ViewGroup,源码如下所示:

public class RelativeLayout extends ViewGroup {
    ...
}

ViewGroup 又是什么呢?ViewGroup 可以理解为 View 的组合,它可以包含很多 View 以及 ViewGroup,而它包含的 ViewGroup 又可以包含 View 和 ViewGroup,依此类推,形成一个 View 树,如图1所示:

图1 View 树


需要注意的是 ViewGroup 也继承自 View,源码如下所示:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    ...
}

ViewGroup 作为 View 或者 ViewGroup 这些组件的容器,派生了多种布局控件子类,比如 LinearLayout、RelativeLayout 等。一般来说,开发 Android 应用的 UI 界面都不会直接使用 View 和 ViewGroup,而是使用这两大基类的派生类。看图2我们就会有一个直观的了解:

图2 View 的部分继承关系


图2列举了 View 的部分继承关系,在这张图上我们看到 ViewGroup、TextView 等控件继承自 View,LinearLayout、RelativeLayout 等控件继承自 ViewGroup,TableLayout、RadioGroup 等控件继承自 LinearLayout,EditText、Button 等控件继承自 TextView,等等。

源码解析 View 的事件分发

本文是基于 Android 14 的源码解析。

当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent。而当这个 MotionEvent 产生后,那么系统就会将这个 MotionEvent 传递给 View 的层级,MotionEvent 在 View 中的层级传递过程就是点击事件分发。在了解了什么是事件分发后,我们还需要了解事件分发的3个重要方法。点击事件有3个重要的方法,它们分别是:

  • dispatchTouchEvent(MotionEvent event):用来进行事件的分发。
  • onInterceptTouchEvent(MotionEvent event):用来进行事件的拦截,在 dispatchTouchEvent() 中调用,需要注意的是 View 没有提供该方法。
  • onTouchEvent(MotionEvent event):用来处理点击事件,在 dispatchTouchEvent() 方法中进行调用。

一、机制

当点击事件产生后,事件首先会传递给当前的 Activity,这会调用 Activity 的 dispatchTouchEvent() 方法,当然具体的事件处理工作都是交由 Activity 中的 PhoneWindow 来完成的,然后 PhoneWindow 再把事件处理工作交给 DecorView,之后再由 DecorView 将事件处理工作交给根 ViewGroup。所以,我们从 ViewGroup 的 dispatchTouchEvent() 方法开始分析,源码如下所示:

路径:/frameworks/base/core/java/android/view/ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

        ...

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        ...

        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

        ...

    return handled;
}

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

private void clearTouchTargets() {
    TouchTarget target = mFirstTouchTarget;
    if (target != null) {
        do {
            TouchTarget next = target.next;
            target.recycle();
            target = next;
        } while (target != null);
        mFirstTouchTarget = null;
    }
}

这里首先判断事件是否为 DOWN 事件,如果是,则进行初始化,resetTouchState 方法中会把 mFirstTouchTarget 的值置为 null。这里为什么要进行初始化呢?原因就是一个完整的事件序列是以 DOWN 开始,以 UP 结束的。所以如果是 DOWN 事件,那么说明这是一个新的事件序列,故而需要初始化之前的状态。

接着往下看,上面代码 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { } 条件如果满足,则执行下面的句子,mFirstTouchTarget 的意义是:当前 ViewGroup 是否拦截了事件,如果拦截了,mFirstTouchTarget = null;如果没有拦截并交由子 View 来处理,则 mFirstTouchTarget != null。假设当前的 ViewGroup 拦截了此事件,mFirstTouchTarget != null 则为 false,如果这时触发 ACTION_DOWN 事件,则会执行 onInterceptTouchEvent(ev) 方法;如果触发的是 ACTION_MOVE、ACTION_UP 事件,则不再执行 onInterceptTouchEvent(ev) 方法,而是直接设置 intercepted = true,此后的一个事件序列均由这个 ViewGroup 处理。

再往下看,上面代码 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 又出现了一个 FLAG_DISALLOW_INTERCEPT 标志位,它主要是禁止 ViewGroup 拦截除了 DOWN 之外的事件,一般通过子 View 的 requestDisallowInterceptTouchEvent 来设置。所以总结一下就是,当 ViewGroup 要拦截事件的时候,那么后续的事件序列都将交给它处理,而不用再调用 onInterceptTouchEvent() 方法了。所以, onInterceptTouchEvent() 方法并不是每次事件都会调用的。

接下来查看 onInterceptTouchEvent() 方法:

路径:/frameworks/base/core/java/android/view/ViewGroup.java

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) {
        return true;
    }
    return false;
}

onInterceptTouchEvent() 方法默认返回 false,不进行拦截。如果想要让 ViewGroup 拦截事件,那么应该在自定义的 ViewGroup 中重写这个方法。

接着来看看 dispatchTouchEvent() 方法剩余的部分源码,如下所示:

路径:/frameworks/base/core/java/android/view/ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    ...

    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);

        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount;
        }

        if (!child.canReceivePointerEvents()
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            mLastTouchDownTime = ev.getDownTime();
            if (preorderedList != null) {
                for (int j = 0; j < childrenCount; j++) {
                    if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
                }
            } else {
                mLastTouchDownIndex = childIndex;
            }
            mLastTouchDownX = x;
            mLastTouchDownY = y;
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

        ev.setTargetAccessibilityFocus(false);
    }

    ...
}

在上面代码 for (int i = childrenCount - 1; i >= 0; i--) { } 我们看到了 for 循环。首先遍历 ViewGroup 的子元素,判断子元素是否能够接收到点击事件,如果子元素能够接收到点击事件,则交由子元素来处理。需要注意这个 for 循环是倒序遍历的,即从最上层的子 View 开始往内层遍历。

接着往下看 if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { },其意思是判断触摸点位置是否在子 View 的范围内或者子 View 是否在播放动画。如果均不符合则执行 continue 语句,表示这个子 View 不符合条件,开始遍历下一个子 View。

接下来查看 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { } 的 dispatchTransformedTouchEvent 方法做了什么,代码如下所示:

路径:/frameworks/base/core/java/android/view/ViewGroup.java

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {

    ...

    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    ...
}

如果有子 View,则调用子 View 的 dispatchTouchEvent(event) 方法。如果 ViewGroup 没有子 View,则调用 super.dispatchTouchEvent(event) 方法。

ViewGroup 是继承 View 的,再来查看 View 的 dispatchTouchEvent(event):

路径:/frameworks/base/core/java/android/view/View.java

public boolean dispatchTouchEvent(MotionEvent event) {
    
    ...

    boolean result = false;

    ...

    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    ...

    return result;
}

我们看到如果 mOnTouchListener 不为 null 并且 onTouch 方法返回 true,则表示事件被消费,就不会执行 onTouchEvent(event);否则就会执行 onTouchEvent(event)。可以看出 OnTouchListener 中的 onTouch() 方法优先级要高于 onTouchEvent(event)方法。

下面再来看看 onTouchEvent() 方法的部分源码:

路径:/frameworks/base/core/java/android/view/View.java

public boolean onTouchEvent(MotionEvent event) {
    
    ...

    final int action = event.getAction();

    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    ...

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                ...
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    boolean focusTaken = false;

                    ...

                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        removeLongPressCallback();

                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }

                    ...
                }

            ...
        }

        return true;
    }

    return false;
}

从上面的代码可以看到,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一个为 true,那么 onTouchEvent() 就会返回 true 消耗这个事件。CLICKABLE 和 LONG_CLICKABLE 代表 View 可以被点击和长按点击,可以通过 View 的 setClickable 和 setLongClickable 方法来设置,也可以通过 View 的 setOnClickListenter 和 setOnLongClickListener 来设置,它们会自动将 View 设置为 CLICKABLE 和 LONG_CLICKABLE。

接着在 ACTION_UP 事件中会调用 performClick() 方法:

public boolean performClick() {
    notifyAutofillManagerOnClick();

    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

从上面代码 if (li != null && li.mOnClickListener != null) { } 处可以看出,如果 View 设置了点击事件 OnClickListener,那么它的 onClick() 方法就会被执行。

二、传递规则

  1. 点击事件由上而下的传递规则

    当点击事件产生后会由 Activity 来处理,传递给 PhoneWindow,再传递给 DecorView,最后传递给顶层的 ViewGroup。一般在事件传递中只考虑 ViewGroup 的 onInterceptTouchEvent 方法,因为一般情况下我们不会重写 dispatchTouchEvent() 方法。对于根 ViewGroup,点击事件首先传递给它的 dispatchTouchEvent() 方法,如果该 ViewGroup 的 onInterceptTouchEvent() 方法返回 true,则表示它要拦截这个事件,这个事件就会交给它的 onTouchEvent() 方法处理;如果 onInterceptTouchEvent() 方法返回 false,则表示它不拦截这个事件,则这个事件会交给它的子元素的 dispatchTouchEvent() 来处理,如此反复下去。如果传递给底层的 View,View 是没有子 View 的,就会调用 View 的 dispatchTouchEvent() 方法,一般情况下最终会调用 View 的 onTouchEvent() 方法。

  2. 点击事件由下而上的传递规则

    当点击事件传给底层的 View 时,如果其 onTouchEvent() 方法返回 true,则事件由底层的 View 消耗并处理;如果返回 false 则表示该 View 不做处理,则传递给父 View 的 onTouchEvent() 处理;如果父 View 的 onTouchEvent() 仍旧返回 false,则继续传递给该父 View 的父 View 处理,如此反复下去。

在 Kotlin Coroutines 中使用 launch、async、Channel 和 Flow

在 Kotlin 中,有多种实现异步处理(线程)的方法。老实说,很多开发人员不知道在使用 Kotlin 开发应用程序时应该使用什么。下面,我将详细介绍并说明 launch、async、Channel 和 Flow 的用途。

一、操作环境

Kotlin:1.9.21

二、目标

阐明协程异步处理的正确使用

三、各种方法的定义

通过查阅资料,我整理了一个表格:

方法 方法定义 返回值 概述
launch
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job
Job 无法返回任意结果的协程。
async
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>
Deferred 返回任意单个结果。
Channel#send
public suspend fun send(element: E)
将结果发送到 Channel。
Channel#receive
public suspend fun receive(): E
Channel 实例化时在泛型中指定的类型 从 Channel 中检索数据。
flow function (Flow Builder)
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T>
Flow 对象 当构建器生成一个实例时,它会返回由泛型指定的数据类型的数据。
FlowCollector#emit
public suspend fun emit(value: T)
向 Flow 发送数值。
Flow#collect
public suspend fun Flow<*>.collect(): Unit
在 Flow 接收数据的那个地方,输出由 collect 扩展函数收集到的值。

下面通过实际情况来理解它们。

四、launch

  1. 概述

    launch函数会创建并执行一个线程。由于它在主线程之外运行,所以需要等待launch的线程完成。可以使用一个属性来检查线程的状态,需要巧妙地利用它来等待线程的结束。

    检查状态的属性如下:

    方法 解释
    Job#isActive 如果线程正在运行且尚未取消,则返回 true。
    Job#isCancelled 当线程被取消时返回 true。
    Job#isCompleted 当线程完成执行时返回 true。
  2. 流程图

    graph LR
        A[开始] --> B[启动协程]
        B --> C[执行协程代码块]
        C --> D[异步操作1]
        C --> E[异步操作2]
        D --> F[处理异步操作1结果]
        E --> G[处理异步操作2结果]
        F --> H[结束]
        G --> H[结束]
    
    Loading
  3. 示例代码

    import kotlinx.coroutines.CoroutineScope
    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    
    val stories = arrayOf(
        "庙里有个老和尚在讲故事",
        "1.从前有座山",
        "2.山里有座庙",
        "3.庙里有个盆",
        "4.盆里有个钵"
    )
    
    fun main() {
        val job = CoroutineScope(Dispatchers.Default).launch {
            repeat(5) { count ->
                delay(500)
                println(stories[count])
            }
        }
    
        while (!job.isCompleted) {
            Thread.sleep(100)
        }
    }
    庙里有个老和尚在讲故事
    1.从前有座山
    2.山里有座庙
    3.庙里有个盆
    4.盆里有个钵

五、async

  1. 概述

    与 launch 的区别在于它可以返回一个返回值。返回值类型没有特殊限制,因此可以返回任何值。此外,在使用 launch 的情况下,我们使用属性来判断处理是否已完成。而在使用 async 的情况下,返回值是一个 Deferred 对象,它提供了一种等待处理完成的方法。此外,还可以进行取消操作,这是一种协作式的取消。

    以下是接收方法结束和返回值的方法:

    方法 解释
    Deferred#await 等待异步处理完成。
    Deferred#getComplete 可以获取返回值。返回类型没有限制,因此可以指定任何类型。
  2. 流程图

    graph LR
        A[开始] --> B[启动协程]
        B --> C[执行异步操作]
        C --> D[等待异步操作完成]
        D --> E[获取异步操作结果]
        E --> F[结束]
    
    Loading
  3. 示例代码

    Basic:

    import kotlinx.coroutines.*
    
    val stories = arrayOf(
        "庙里有个老和尚在讲故事",
        "1.从前有座山",
        "2.山里有座庙",
        "3.庙里有个盆",
        "4.盆里有个钵"
    )
    
    fun main() {
        val job = CoroutineScope(Dispatchers.Default).launch {
            repeat(5) { count ->
                val deffer = async {
                    delay(500)
                    stories[count]
                }
    
                println("等待第${count + 1}个异步完成")
                deffer.await()
                println(deffer.getCompleted())
            }
        }
    
        // 等待协程完成
        while (!job.isCompleted) {
            Thread.sleep(100)
        }
    }
    等待第1个异步完成
    庙里有个老和尚在讲故事
    等待第2个异步完成
    1.从前有座山
    等待第3个异步完成
    2.山里有座庙
    等待第4个异步完成
    3.庙里有个盆
    等待第5个异步完成
    4.盆里有个钵

    Cancel:

    import kotlinx.coroutines.*
    
    val stories = arrayOf(
        "庙里有个老和尚在讲故事",
        "1.从前有座山",
        "2.山里有座庙",
        "3.庙里有个盆",
        "4.盆里有个钵"
    )
    
    fun main() {
        val job = CoroutineScope(Dispatchers.Default).launch {
            repeat(5) { count ->
    
                val deffer = async {
                    // 第二次尝试时被取消
                    if (count == 2) {
                        cancel("取消")
                    }
                    delay(500)
                    stories[count]
                }
    
                println("等待第${count + 1}个异步完成")
                deffer.await()
                println(deffer.getCompleted())
            }
        }
    
        // 等待协程完成
        while (!job.isCompleted || !job.isCancelled) {
            Thread.sleep(100)
        }
    }
    等待第1个异步完成
    庙里有个老和尚在讲故事
    等待第2个异步完成
    1.从前有座山
    等待第3个异步完成

六、Channel

  1. 概述

    Channel是一个可在主程序和协程中使用的容器。它不仅仅是一个容器,还可以让接收数据的线程等待,并让将数据放入Channel的线程等待发送,因为Channel中包含了数据。我们需要对数据进行流量控制,例如让线程等待。这就是Channel被称为热流的原因,它的特点是无论是否有接收者(取出值的处理)都会执行发送过程(放入数据)。

    因此,需要采取以下预防措施。如果发送的次数没有收到数据,就会发生内存泄漏。换句话说,发送方的处理会继续进行,直到收到为止!

    以下是发送和接收的方法:

    方法 解释
    Channel#send 向 Channel 发送数据。
    Channel#receive 从 Channel 接收数据。
  2. 流程图

    graph LR
        A[开始] --> B[创建Channel]
        B --> C[发送数据至Channel]
        C --> D[接收Channel中的数据]
        D --> E[处理接收到的数据]
        E --> F[循环接收数据]
        F --> D[继续接收数据]
        E --> G[结束]
    
    Loading
  3. 示例代码

    import kotlinx.coroutines.channels.Channel
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    val stories = arrayOf(
        "庙里有个老和尚在讲故事",
        "1.从前有座山",
        "2.山里有座庙",
        "3.庙里有个盆",
        "4.盆里有个钵"
    )
    
    fun main() = runBlocking {
        // 初始化 Channel(只能发送和接收 String)
        val channel = Channel<String>()
    
        // 调用异步处理
        launch {
            stories.forEach {
                // 发送字符串到 Channel
                channel.send(it)
                println("发送:$it")
                // 1秒等待
                delay(1000)
            }
        }
    
        // 重复5次,就像发送到 Channel 5次一样
        repeat(5) {
            // 从 Channel 接收
            val story = channel.receive()
            println("接收:$story")
        }
    
        println("结束")
    }
    发送:庙里有个老和尚在讲故事
    接收:庙里有个老和尚在讲故事
    发送:1.从前有座山
    接收:1.从前有座山
    发送:2.山里有座庙
    接收:2.山里有座庙
    发送:3.庙里有个盆
    接收:3.庙里有个盆
    发送:4.盆里有个钵
    接收:4.盆里有个钵
    结束

七、Channel Buffer

  1. 概述

    Channel 是数据的容器,但到目前为止它只能存储单条数据。但是,可以通过设置缓冲区来存储多个数据。

  2. 流程图

    graph LR
        A[开始] --> B[创建Channel]
        B --> C[设置Channel缓冲区大小]
        C --> D[发送数据至Channel]
        D --> E[将数据放入缓冲区]
        E --> F[接收Channel中的数据]
        F --> G[从缓冲区获取数据]
        G --> H[处理接收到的数据]
        H --> I[循环接收数据]
        I --> F[继续接收数据]
        H --> J[结束]
    
    Loading
  3. 示例代码

    实例化 Channel 时,可以通过将整数传递给构造函数来设置缓冲区。

    import kotlinx.coroutines.channels.Channel
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    val stories = arrayOf(
        "庙里有个老和尚在讲故事",
        "1.从前有座山",
        "2.山里有座庙",
        "3.庙里有个盆",
        "4.盆里有个钵"
    )
    
    fun main() = runBlocking {
        // 初始化 Channel(只能发送和接收 String)
        val channel = Channel<String>(5)
    
        // 调用异步处理
        launch {
            stories.forEach {
                // 发送字符串到 Channel
                channel.send(it)
                println("发送:$it")
            }
        }
    
        // 重复5次,就像发送到 Channel 5次一样
        repeat(5) {
            Thread.sleep(1000)
            // 从 Channel 接收
            val story = channel.receive()
            println("接收:$story")
        }
        println("结束")
    }
    发送:庙里有个老和尚在讲故事
    发送:1.从前有座山
    发送:2.山里有座庙
    发送:3.庙里有个盆
    发送:4.盆里有个钵
    接收:庙里有个老和尚在讲故事
    接收:1.从前有座山
    接收:2.山里有座庙
    接收:3.庙里有个盆
    接收:4.盆里有个钵
    结束

八、Channel Cancel&Close

  1. 概述

    前面提到过 Channel 可能会导致内存泄漏,但是可以通过使用 cancel 和 close 来避免这种情况。

    Cancel 和 Close 的方法说明如下:

    方法 解释
    Channel#cancel 取消接收元素。 关闭 Channel 并删除所有缓冲的发送元素。 如果取消,将会发生异常(java.util.concurrent.CancellationException)。
    Channel#close 关闭 Channel。从现在开始,即使你调用它,它也会返回 false。
  2. 流程图

    Cancel:

    graph LR
        A[开始] --> B[创建Channel]
        B --> C[发送数据至Channel]
        C --> D[接收Channel中的数据]
        D --> E[处理接收到的数据]
        E --> F[检查取消状态]
        F --> G[取消Channel接收操作]
        G --> H[结束]
    
    Loading

    Close:

    graph LR
        A[开始] --> B[创建Channel]
        B --> C[发送数据至Channel]
        C --> D[接收Channel中的数据]
        D --> E[处理接收到的数据]
        E --> F[检查Channel是否关闭]
        F --> G[关闭Channel]
        G --> H[结束]
    
    Loading
  3. 示例代码

    Cancel:

    import kotlinx.coroutines.channels.Channel
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    val stories = arrayOf(
        "庙里有个老和尚在讲故事",
        "1.从前有座山",
        "2.山里有座庙",
        "3.庙里有个盆",
        "4.盆里有个钵"
    )
    
    fun main() = runBlocking {
        // 初始化 Channel(只能发送和接收 String)
        val channel = Channel<String>()
    
        // 调用异步处理
        launch {
            stories.forEach {
    
                // 发送字符串到 Channel
                channel.send(it)
                println("发送:$it")
                // 0.5秒等待
                delay(500)
    
                // 中途被打断
                if (stories.indexOf(it) == 2) {
                    channel.cancel()
                }
            }
        }
    
        // 重复5次,就像发送到 Channel 5次一样
        repeat(5) {
            // 从 Channel 接收
            val story = channel.receive()
            println("接收:$story")
        }
    
        println("结束")
    }
    发送:庙里有个老和尚在讲故事
    接收:庙里有个老和尚在讲故事
    发送:1.从前有座山
    接收:1.从前有座山
    发送:2.山里有座庙
    接收:2.山里有座庙
    Exception in thread "main" java.util.concurrent.CancellationException: Channel was cancelled

    Close:

    import kotlinx.coroutines.channels.Channel
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    val stories = arrayOf(
        "庙里有个老和尚在讲故事",
        "1.从前有座山",
        "2.山里有座庙",
        "3.庙里有个盆",
        "4.盆里有个钵"
    )
    
    fun main() = runBlocking {
        // 初始化 Channel(只能发送和接收 String)
        val channel = Channel<String>()
    
        // 调用异步处理
        launch {
            stories.forEach {
    
                // 发送字符串到 Channel
                channel.send(it)
                println("发送:$it")
                // 0.5秒等待
                delay(500)
    
                // 中途被打断
                if (stories.indexOf(it) == 2) {
                    channel.close()
                }
            }
        }
    
        // 重复5次,就像发送到 Channel 5次一样
        repeat(5) {
            // 从 Channel 接收
            val story = channel.receive()
            println("接收:$story")
        }
    
        println("结束")
    }
    发送:庙里有个老和尚在讲故事
    接收:庙里有个老和尚在讲故事
    发送:1.从前有座山
    接收:1.从前有座山
    发送:2.山里有座庙
    接收:2.山里有座庙
    Exception in thread "main" kotlinx.coroutines.channels.ClosedSendChannelException: Channel was closed

九、Flow

  1. 概述

    Flow 称为冷流,其行为与热流 Channel 有很大不同。与 Channel 不同的是,除非确定了接收处理​​(collect方法),否则 Flow 不会执行发送处理(emit方法)。结果,Flow 将不会运行。因此,不会发生内存泄漏。此外,取消操作实现了协作式取消。

  2. 流程图

    graph LR
        A[开始] --> B[创建Flow]
        B --> C[定义Flow的数据流]
        C --> D[收集Flow的数据]
        D --> E[处理收集到的数据]
        E --> F[循环收集数据]
        F --> D[继续收集数据]
        E --> G[结束]
    
    Loading
  3. 示例代码

    Flow 的基本使用:

    import kotlinx.coroutines.delay
    import kotlinx.coroutines.flow.flow
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    val stories = arrayOf(
        "庙里有个老和尚在讲故事",
        "1.从前有座山",
        "2.山里有座庙",
        "3.庙里有个盆",
        "4.盆里有个钵"
    )
    
    fun teller() = flow {
        repeat(stories.count()) {
            Thread.sleep(1000)
            emit(stories[it])
            println("${it + 1}次调用了emit")
        }
    }
    
    fun main() = runBlocking {
        launch {
            for (i in 1..3) {
                println("${i}次在main方法中进行延迟处理")
                delay(100)
            }
        }
    
        val collector = teller()
    
        collector.collect { value ->
            println(value)
            Thread.sleep(100)
        }
    
        println("结束")
    }
    庙里有个老和尚在讲故事
    第1次调用了emit
    1.从前有座山
    第2次调用了emit
    2.山里有座庙
    第3次调用了emit
    3.庙里有个盆
    第4次调用了emit
    4.盆里有个钵
    第5次调用了emit
    结束
    第1次在main方法中进行延迟处理
    第2次在main方法中进行延迟处理
    第3次在main方法中进行延迟处理

    协作式取消:

    import kotlinx.coroutines.flow.flow
    import kotlinx.coroutines.runBlocking
    import kotlinx.coroutines.withTimeoutOrNull
    
    val stories = arrayOf(
        "庙里有个老和尚在讲故事",
        "1.从前有座山",
        "2.山里有座庙",
        "3.庙里有个盆",
        "4.盆里有个钵"
    )
    
    fun teller() = flow {
    
        repeat(stories.count()) {
            Thread.sleep(1000)
            emit(stories[it])
            println("${it + 1}次调用了emit")
        }
    }
    
    fun main() = runBlocking {
    
        val collector = teller()
    
        // 如果时间超过2.5秒,则取消
        withTimeoutOrNull(2500) {
            collector.collect { value ->
                println(value)
                Thread.sleep(100)
            }
        }
    
        println("结束")
    }
    庙里有个老和尚在讲故事
    第1次调用了emit
    1.从前有座山
    第2次调用了emit
    结束

十、总结

总结以上所述内容并考虑使用场景时,可以如下所示:

类型 使用场景
launch 当你想要启动一个协程而不需要返回值时
async 当需要获得任意返回值并且希望并行执行多个任务并等待它们完成时
Channel 当需要实时处理具有相同类型的多个返回值时
Flow 当需要确保以相同类型的多个返回值必须完全处理时

ArkUI - 向左/向右滑动删除

核心知识点:List容器 -> ListItem -> swipeAction

先看效果图:

代码实现:

// 任务类
class Task {
  static id: number = 1
  // 任务名称
  name: string = `任务${Task.id++}`
  // 任务状态
  finished: boolean = false
}
// 统一的卡片样式
@Styles function card() {
  .width('95%')
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(15)
  .shadow({ radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4 })
}
@Entry
@Component
struct PropPage {
  // 总任务数量
  @State totalTask: number = 0
  // 已完成任务数量
  @State finishTask: number = 0
  // 任务数组
  @State tasks: Task[] = []

  build() {
    Column({ space: 10 }) {
      // 新增任务按钮
      Button('新增任务')
        .width(200)
        .margin({ top: 10 })
        .onClick(() => {
          // 新增任务数据
          this.tasks.push(new Task())
          // 更新任务总数量
          this.totalTask = this.tasks.length
        })

      // 任务列表
      List({ space: 10 }) {
        ForEach(
          this.tasks,
          (item: Task, index) => {
            ListItem() {
              Row() {
                Text(item.name)
                  .fontSize(20)
                Checkbox()
                  .select(item.finished)
                  .onChange(val => {
                    // 更新当前任务状态
                    item.finished = val
                    // 更新已完成任务数量
                    this.finishTask = this.tasks.filter(item => item.finished).length
                  })
              }
              .card()
              .justifyContent(FlexAlign.SpaceBetween)
            }
            .swipeAction({ end: this.DeleteButton(index) })
          }
        )
      }
      .width('100%')
      .layoutWeight(1)
      .alignListItem(ListItemAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F2F3')
  }

  @Builder DeleteButton(index: number) {
    Button() {
      Image($r('app.media.delete'))
        .fillColor(Color.White)
        .width(20)
    }
    .width(40)
    .height(40)
    .type(ButtonType.Circle)
    .backgroundColor(Color.Red)
    .margin(5)
    .onClick(() => {
      this.tasks.splice(index, 1)
      this.totalTask = this.tasks.length
      this.finishTask = this.tasks.filter(item => item.finished).length
    })
  }
}
  • .swipeAction({ end: ... })

    向左滑动

  • .swipeAction({ start: ... })

    向右滑动

ArkUI - 页面路由

一、概念

页面路由是指在应用程序中实现不同页面之间的跳转和数据传递。

案例:第一次使用某个购物应用,打开时肯定会是一个登录页,在登录成功以后,会跳转到首页,然后可能会去搜索,就会进入到搜索列表页,接着呢如果搜索到某一个感兴趣的商品A,点击,就会进入到商品A的详情页,到现在为止已经访问了好多不同的页面,并且在它们之间完成了跳转,那我之前访问过的页面都去哪里了?是不是全部被销毁了?其实并没有,我们在页面跳转之间访问过的所有页面,都会被 HarmonyOS 保存到页面栈的空间当中。页面栈顾名思义就是保存页面的栈结构空间,栈结构是先进后出,所以呢,我们最早访问的登录页就被压到了栈的最底层,而当前正在访问的商品A的详情页就在栈顶。也就是说,谁在栈顶,当前显示的就是谁的页面。那么 HarmonyOS 为啥要把这些访问过的页面保存起来,而不是直接销毁掉呢?放在这里不占用内存吗?这其实是我们的页面功能需要去用到这些历史页面。一般商品详情页都会有返回按钮,当点击返回按钮,应该返回到之前访问过的搜索列表页,有了页面栈,想要实现这个功能就非常简单了。只需要在点击返回时,把栈顶的这个页面移除,这样一来,紧挨着栈顶的搜索列表页就成为了新的栈顶页面,现在显示的就是搜索列表页,从而也就实现了返回的效果。如果现在想做页面跳转,过程就相反,比如在搜索列表页点击商品B,只需要把商品B的详情页创建出来,然后压入栈里,现在栈顶就是商品B的详情页,从而实现了跳转效果。简单来讲,如果想实现创建页面,就压入栈;如果想实现返回,就把栈顶页面弹出栈,即可。

  1. 页面栈的最大容量:

    页面栈的最大容量上限为 32 个页面。就是说如果我们不断的去访问新的页面,往栈里压入页面,可能就会达到上限,这时候再想访问这个页面,再想往里面去压栈,就会报错,这时候就不得不去调用 router.clear() 方法去清空页面栈,就会把历史页面干掉,释放内存。但是,一旦把历史页面干掉,再想返回前一个页面就访问不了了。所以 router.clear() 要慎重使用。我们在开发的过程中一定要想办法控制页面栈里的页面数量,不要让它达到上限,而不是说等达到上限去清空。

  2. 怎么去控制页面栈里的页面数量呢?就要使用页面栈不同的跳转行为模式。Router 有两种页面跳转模式:

    • router.pushUrl():目标页不会替换当前页,而是压入页面栈。比如当前在商品A的详情页,如果点击商品B的图片,就需要压入栈,就需要创建一个商品B的页面,把商品B的详情页压入栈顶,这时候,原有的商品A的详情页不会被移除,而是压到栈的内部,成为一个历史页面,因此点返回按钮,用 router.back() 就会返回到历史页面商品A的详情页。但是,这种实现方式会导致栈里的页面会越来越多。

    • router.replaceUrl():目标页替换当前页,当前页会被销毁并释放资源。也就是说从商品A的详情页跳转到商品B的详情页,这时候商品A的详情页就变成了历史页,会直接被销毁,而不是在栈内保存。这样一来,内存就节省出来,但是如果想从商品B的详情页返回到商品A的详情页,就返回不了了。

  3. 何时使用 router.pushUrl(),何时又使用 router.replaceUrl() 呢?

    举个例子,比如说,我们的登录页,只有在第一次打开的时候才需要,只要不退出,就不用再登录。所以登录页基本上就访问一次,而且也不需要返回,登录页保存在历史页面栈里没有任何意义,所以在登录成功以后,跳转到首页时,就可以使用 router.replaceUrl() 把登录页销毁;如果我们从首页跳转到搜索列表页,如果这时候点返回,返回到首页,所以我们的首页应该在页面栈里保存,作为一个历史页面,就要用 router.pushUrl()

  4. 如果商品A的详情页和商品B的详情页来回切换,如果用到 router.pushUrl(),会导致页面栈的容量一会儿就满了,就要用到页面实例模式,Router 有两种页面实例模式:

    • Standard:标准实例模式,每次跳转都会新建一个目标页并压入栈顶。默认就是这种模式。

    • Single:单实例模式,顾名思义,每一个页面只会存在一份,如果目标页已经在栈中,则离栈顶最近的同Url页面会被移动到栈顶并重新加载。

结合合适的跳转模式(router.pushUrl()router.replaceUrl())和实例模式(StandardSingle),就能够控制页面栈里的页面数量,避免达到上限。

二、Router API 用法

  1. 首先要导入 HarmonyOS 提供的 Router 模块:

    import router from '@ohos.router';
  2. 然后利用 router 实现跳转、返回等操作:

    router.pushUrl(
      {
        url: 'pages/PageA',
        params: { id: 1 }
      },
      router.RouterMode.Single,
      err => {
        if (err) {
          console.log('路由失败。')
        }
      }
    )
    • RouterOptions

      • url:目标页面路径
      • params:传递的参数(可选)
    • RouterMode

      • Standard:标准实例模式
      • Single:单实例模式
    • 异常响应回调函数

      • 错误码 100001:内部错误,可能是渲染失败
      • 错误码 100002:路由地址错误
      • 错误码 100003:路由栈中页面超过32
  3. 目标页获取传递过来的参数

    params: any = router.getParams()
  4. 目标页返回上一页

    router.back()
  5. 目标页返回指定页,并携带参数

    router.back({
      url: 'pages/Index',
      params: { id: 10 }
    })

三、示例

  1. 代码目录结构

    |____src
    | |____main
    | | |____resources
    | | | | |____profile
    | | | | | |____main_pages.json
    | | | | |____media
    | | | | | |____back.png
    | | |____ets
    | | | |____components
    | | | | |____CommonComponents.ets
    | | | |____pages
    | | | | |____PageD.ets
    | | | | |____PageC.ets
    | | | | |____PageB.ets
    | | | | |____PageA.ets
    | | | | |____Index.ets
    
  2. main_pages.json

    {
      "src": [
        "pages/Index",
        "pages/PageA",
        "pages/PageB",
        "pages/PageC",
        "pages/PageD"
      ]
    }
  3. CommonComponents.ets

    import router from '@ohos.router'
    
    @Component
    export struct Header {
      @State params: any = router.getParams()
    
      build() {
        Row({ space: 5 }) {
          Image($r('app.media.back'))
            .width(30)
            .onClick(() => {
              // 返回前的警告
              router.showAlertBeforeBackPage({
                message: 'Show Alert Before Back Page'
              })
              // 返回上一页
              router.back()
            })
          if (this.params) {
            Text(`Params id: ${this.params.id}`)
              .fontSize(28)
              .fontWeight(FontWeight.Bold)
          }
        }
        .width('98%')
        .height(30)
      }
    }
  4. Index.ets

    import router from '@ohos.router'
    
    class RouterInfo {
      // 页面路径
      url: string
      // 页面标题
      title: string
    
      constructor(url: string, title: string) {
        this.url = url
        this.title = title
      }
    }
    
    @Entry
    @Component
    struct Index {
      @State message: string = '页面列表'
      private routers: RouterInfo[] = [
        new RouterInfo('pages/PageA', 'A页面'),
        new RouterInfo('pages/PageB', 'B页面'),
        new RouterInfo('pages/PageC', 'C页面'),
        new RouterInfo('pages/PageD', 'D页面')
      ]
    
      build() {
        Column() {
          Text(this.message)
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
            .height(80)
    
          List({ space: 15 }) {
            ForEach(
              this.routers,
              (router, index) => {
                ListItem() {
                  this.RouterItem(router, index + 1)
                }
              }
            )
          }
          .layoutWeight(1)
          .alignListItem(ListItemAlign.Center)
          .width('100%')
        }
        .width('100%')
        .height('100%')
      }
    
      @Builder
      RouterItem(r: RouterInfo, i: number) {
        Row() {
          Text(i + '. ')
            .fontSize(20)
            .fontColor(Color.White)
          Text(r.title)
            .fontSize(20)
            .fontColor(Color.White)
        }
        .width('90%')
        .padding(12)
        .backgroundColor('#38F')
        .borderRadius(20)
        .shadow({ radius: 6, color: '#4F000000', offsetX: 2, offsetY: 2 })
        .onClick(() => {
          // router 跳转
          router.pushUrl(
            {
              url: r.url,
              params: { id: i }
            },
            router.RouterMode.Single,
            err => {
              if (err) {
                console.log(`路由失败,errCode: ${err.code} errMsg: ${err.message}`)
              }
            }
          )
        })
      }
    }
  5. PageA.ets

    import { Header } from '../components/CommonComponents'
    
    @Entry
    @Component
    struct PageA {
      @State message: string = 'Page A'
    
      build() {
        Column() {
          Header()
          Row() {
            Column() {
              Text(this.message)
                .fontSize(50)
                .fontWeight(FontWeight.Bold)
            }
            .width('100%')
          }
          .height('100%')
        }.width('100%')
      }
    }
  6. 运行效果

四、总结

  1. 页面栈的最大容量上限为 32 个页面,使用 router.clear() 方法可以清空页面栈,释放内存。

  2. Router 有两种页面跳转模式,分别是:

    • router.pushUrl():目标页不会替换当前页,而是压入页面栈,因此可以用 router.back() 返回当前页。
    • router.replaceUrl():目标页替换当前页,当前页会被销毁并释放资源,无法返回当前页。
  3. Router 有两种页面实例模式,分别是:

    • Standard:标准实例模式,每次调整都会新建一个目标并压入栈顶。默认就是这种模式。
    • Single:单实例模式,如果目标页已经在栈中,则离栈顶最近的同 url 页面会被移动到栈顶并重新加载。
  4. router 的使用步骤:

    1. 导入 router 模块
    2. 使用 router 的 API

源码中的建造者模式

本文是基于 Android 14 的源码解析

在 Android 源码中,最常用到的建造者模式就是 AlertDialog.Builder,使用该建造者来构建复杂的 AlertDialog 对象。在开发过程中,我们经常用到 AlertDialog,具体示例如下:

private fun showDialog(context: Context) {
    val builder = AlertDialog.Builder(context)
    with(builder) {
        setIcon(R.mipmap.ic_launcher)
        setTitle("Title")
        setMessage("Message")
        setPositiveButton("Positive") { dialog, which ->
            ...
        }
        setNeutralButton("Neutral") { dialog, which ->
            ...
        }
        setNegativeButton("Negative") { dialog, which ->
            ...
        }
        create()
    }.show()
}

显示结果如图1所示:

图1

从类名就可以看出这就是一个建造者模式,通过建造者对象来组装 Dialog 的各个部分,如 title、button、message 等,将 Dialog 的构造和表示进行分离。下面看看 AlertDialog 的相关源码:

public class AlertDialog extends AppCompatDialog implements DialogInterface {

    // AlertController 接收建造者成员变量 P 中的各个参数
    final AlertController mAlert;

    ...

    // 构造函数
    protected AlertDialog(@NonNull Context context) {
        this(context, 0);
    }

    // 构造 AlertDialog
    protected AlertDialog(@NonNull Context context, @StyleRes int themeResId) {
        super(context, resolveDialogTheme(context, themeResId));
        // 构造AlertController
        mAlert = new AlertController(getContext(), this, getWindow());
    }

    ...

    // 实际上调用的是 mAlert 的 setTitle 方法
    @Override
    public void setTitle(CharSequence title) {
        super.setTitle(title);
        mAlert.setTitle(title);
    }

    // 实际上调用的是 mAlert 的 setCustomTitle 方法
    public void setCustomTitle(View customTitleView) {
        mAlert.setCustomTitle(customTitleView);
    }

    ...

    public static class Builder {
        // 存储 AlertDialog 的各个参数,如 title、message、icon 等
        private final AlertController.AlertParams P;
        
        ...

        public Builder(@NonNull Context context) {
            this(context, resolveDialogTheme(context, 0));
        }

        public Builder(@NonNull Context context, @StyleRes int themeResId) {
            P = new AlertController.AlertParams(new ContextThemeWrapper(
                    context, resolveDialogTheme(context, themeResId)));
            mTheme = themeResId;
        }

        ...

        // 设置各种参数
        public Builder setTitle(@Nullable CharSequence title) {
            P.mTitle = title;
            return this;
        }

        public Builder setMessage(@Nullable CharSequence message) {
            P.mMessage = message;
            return this;
        }

        public Builder setView(View view) {
            P.mView = view;
            P.mViewLayoutResId = 0;
            P.mViewSpacingSpecified = false;
            return this;
        }

        ...

        // 构建 AlertDialog, 传递参数
        @NonNull
        public AlertDialog create() {
            // 调用 new AlertDialog 构造对象,并且将参数传递给个体 AlertDialog
            final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
            // 将 P 中的参数应用到 dialog 中的 mAlert 对象中
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }

        public AlertDialog show() {
            final AlertDialog dialog = create();
            dialog.show();
            return dialog;
        }
    }
}

上述代码中,Builder 类可以设置 AlertDialog 中的 title、message、button 等参数,这些参数都存储在类型为 AlertController.AlertParams 的成员变量 P 中,AlertController.AlertParams 中包含了与 AlertDialog 视图中对应的成员变量。在调用 Builder 类的 create 函数时会创建 AlertDialog,并且将 Builder 成员变量 P 中保存的参数应用到 AlertDialog 的 mAlert 对象中,即 P.apply(dialog.mAlert) 代码段。我们再看看 apply 函数的实现:

public void apply(AlertController dialog) {
    if (mCustomTitleView != null) {
        dialog.setCustomTitle(mCustomTitleView);
    } else {
        if (mTitle != null) {
            dialog.setTitle(mTitle);
        }
        if (mIcon != null) {
            dialog.setIcon(mIcon);
        }
        if (mIconId != 0) {
            dialog.setIcon(mIconId);
        }
        if (mIconAttrId != 0) {
            dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
        }
    }
    if (mMessage != null) {
        dialog.setMessage(mMessage);
    }
    if (mPositiveButtonText != null || mPositiveButtonIcon != null) {
        dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
                mPositiveButtonListener, null, mPositiveButtonIcon);
    }
    if (mNegativeButtonText != null || mNegativeButtonIcon != null) {
        dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
                mNegativeButtonListener, null, mNegativeButtonIcon);
    }
    if (mNeutralButtonText != null || mNeutralButtonIcon != null) {
        dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
                mNeutralButtonListener, null, mNeutralButtonIcon);
    }

    // 如果设置了 mItems,则表示是单选或者多选列表,此时创建—个 ListView
    if ((mItems != null) || (mCursor != null) || (mAdapter != null)) {
        createListView(dialog);
    }

    // 将 mView 设置给 Dialog
    if (mView != null) {
        if (mViewSpacingSpecified) {
            dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
                    mViewSpacingBottom);
        } else {
            dialog.setView(mView);
        }
    } else if (mViewLayoutResId != 0) {
        dialog.setView(mViewLayoutResId);
    }
}

在 apply 函数中,只是将 AlertParams 参数设置到 AlertController 中,例如,将标题设置到 Dialog 对应的标题视图中,将 Message 设置到内容视图中等。当我们获取到 AlertDialog 对象后,通过 show 函数就可以显示这个对话框。我们看看 Dialog 的 show 函数:

public void show() {
    ...

    if (!mCreated) {
        dispatchOnCreate(null);
    } else {
        final Configuration config = mContext.getResources().getConfiguration();
        mWindow.getDecorView().dispatchConfigurationChanged(config);
    }

    onStart();
    mDecor = mWindow.getDecorView();

    ...

    WindowManager.LayoutParams l = mWindow.getAttributes();

    ...

    mWindowManager.addView(mDecor, l);
    
    ...
}

在 show 函数中主要做了如下几个事情:

  1. 通过 dispatchOnCreate 函数来调用 AlertDialog 的 onCreate 函数。
  2. 然后调用 AlertDialog 的 onStart 函数。
  3. 最后将 Dialog 的 DecorView 添加到 WindowManager 中。

很明显,这就是一系列典型的生命周期函数。那么按照惯例,AlertDialog 的内容视图构建按理应该在 onCreate 函数中,我们来看看是不是:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mAlert.installContent();
}

在 onCreate 函数中主要调用 了AlertController 的 installContent 方法,Dialog 中的 onCreate 函数只是一个空实现而己,可以忽略它。那么 AlertDialog 的内容视图必然就在 installContent 函数中:

public void installContent() {
    final int contentView = selectContentView();
    mDialog.setContentView(contentView);
    setupView();
}

installContent 函数的代码很少,但极为重要,它调用了 Window 对象的 setContentView,这个 setContentView 就与 Activity 中的一模一样,实际上 Activity 最终也是调用 Window 对象的 setContentView 函数。因此,这里就是设置 AlertDialog 的内容布局,这个布局就是 mAlertDialogLayout 字段的值,这个值在 AlertController 的构造函数中进行了初始化,具体代码如下:

public AlertController(Context context, AppCompatDialog di, Window window) {
    ...

    final TypedArray a = context.obtainStyledAttributes(null, R.styleable.AlertDialog,
            R.attr.alertDialogStyle, 0);

    mAlertDialogLayout = a.getResourceId(R.styleable.AlertDialog_android_layout, 0);
    
    ...

    a.recycle();

    ...
}

用图2来大致描述一下 AlertDialog 的布局结构:

图2

当通过 Builder 对象的 setTitle、setMessage 等方法设置具体内容时,就是将这些内容填充到对应的视图中。而 AlertDialog 也允许你通过 setView 传入内容视图,这个内容视图就是替换掉图2的内容区域,AlertDialog 预留了一个 customPanel 区域用来显示用户自定义的内容视图。我们来看看 setupView 方法:

private void setupView() {
    final View parentPanel = mWindow.findViewById(R.id.parentPanel);
    final View defaultTopPanel = parentPanel.findViewById(R.id.topPanel);
    final View defaultContentPanel = parentPanel.findViewById(R.id.contentPanel);
    final View defaultButtonPanel = parentPanel.findViewById(R.id.buttonPanel);

    final ViewGroup customPanel = (ViewGroup) parentPanel.findViewById(R.id.customPanel);
    setupCustomContent(customPanel);

    final View customTopPanel = customPanel.findViewById(R.id.topPanel);
    final View customContentPanel = customPanel.findViewById(R.id.contentPanel);
    final View customButtonPanel = customPanel.findViewById(R.id.buttonPanel);

    final ViewGroup topPanel = resolvePanel(customTopPanel, defaultTopPanel);
    final ViewGroup contentPanel = resolvePanel(customContentPanel, defaultContentPanel);
    final ViewGroup buttonPanel = resolvePanel(customButtonPanel, defaultButtonPanel);

    setupContent(contentPanel);
    setupButtons(buttonPanel);
    setupTitle(topPanel);

    final boolean hasCustomPanel = customPanel != null
            && customPanel.getVisibility() != View.GONE;
    final boolean hasTopPanel = topPanel != null
            && topPanel.getVisibility() != View.GONE;
    final boolean hasButtonPanel = buttonPanel != null
            && buttonPanel.getVisibility() != View.GONE;

    if (!hasButtonPanel) {
        if (contentPanel != null) {
            final View spacer = contentPanel.findViewById(R.id.textSpacerNoButtons);
            if (spacer != null) {
                spacer.setVisibility(View.VISIBLE);
            }
        }
    }

    if (hasTopPanel) {
        if (mScrollView != null) {
            mScrollView.setClipToPadding(true);
        }

        View divider = null;
        if (mMessage != null || mListView != null) {
            divider = topPanel.findViewById(R.id.titleDividerNoCustom);
        }

        if (divider != null) {
            divider.setVisibility(View.VISIBLE);
        }
    } else {
        if (contentPanel != null) {
            final View spacer = contentPanel.findViewById(R.id.textSpacerNoTitle);
            if (spacer != null) {
                spacer.setVisibility(View.VISIBLE);
            }
        }
    }

    if (mListView instanceof RecycleListView) {
        ((RecycleListView) mListView).setHasDecor(hasTopPanel, hasButtonPanel);
    }

    if (!hasCustomPanel) {
        final View content = mListView != null ? mListView : mScrollView;
        if (content != null) {
            final int indicators = (hasTopPanel ? ViewCompat.SCROLL_INDICATOR_TOP : 0)
                    | (hasButtonPanel ? ViewCompat.SCROLL_INDICATOR_BOTTOM : 0);
            setScrollIndicators(contentPanel, content, indicators,
                    ViewCompat.SCROLL_INDICATOR_TOP | ViewCompat.SCROLL_INDICATOR_BOTTOM);
        }
    }

    final ListView listView = mListView;
    if (listView != null && mAdapter != null) {
        listView.setAdapter(mAdapter);
        final int checkedItem = mCheckedItem;
        if (checkedItem > -1) {
            listView.setItemChecked(checkedItem, true);
            listView.setSelection(checkedItem);
        }
    }
}

这个 setupView 方法的名字已经很直观了:它用于初始化 AlertDialog 布局中的各个部分,例如标题区域、按钮区域和内容区域。在调用此函数之后,整个对话框的视图内容都会被设置完毕。这些不同区域的视图都是 mAlertDialogLayout 布局的子元素。Window 对象与整个 mAlertDialogLayout 的布局树相关联。当 setupView 调用完成后,整个视图树的数据都被填充完毕。当用户调用 show 函数时,WindowManager 会将 Window 对象的 DecorView(也就是对应于 mAlertDialogLayout 的视图)添加到用户的窗口上,并显示出来。

总之,AlertDialog 的建造者模式使得创建和配置对话框变得更加灵活和易于维护。通过链式调用,我们可以按需设置对话框的各个属性,而不必关心参数的顺序或数量。

ArkUI - 自定义卡片样式

HarmonyOS API 9 没有提供原生的卡片样式,我定义了一个卡片样式,可以方便大家在日常开发中使用。

效果图:

卡片样式代码如下:

@Styles function card() {
  .width('95%')
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(15)
  .shadow({ radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4 })
}

使用卡片样式:

@Entry
@Component
struct Test {
  build() {
    Column({ space: 10 }) {
      Row() {
        Text('卡片样式')
      }
      .card()
      .margin({ top: 20, bottom: 10 })
      .justifyContent(FlexAlign.SpaceEvenly)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F2F3')
  }
}

在 Windows 和 Ubuntu 之间传输文件

  1. 在 Ubuntu 上安装 Samba:

    sudo apt-get update
    sudo apt-get install samba
    
  2. 在 Ubuntu 上创建一个共享文件夹并设置权限:

    mkdir /home/your_username/shared
    sudo chown nobody:nogroup /home/your_username/shared
    sudo chmod 0777 /home/your_username/shared
    
  3. 在 Ubuntu 上配置 Samba,编辑 /etc/samba/smb.conf 文件,添加以下内容:

    [shared]
       path = /home/your_username/shared
       read only = no
       guest ok = yes
    
  4. 在 Ubuntu 上重启 Samba 服务:

    sudo service smbd restart
    
  5. 在 Windows 上,打开文件资源管理器,输入:

    \\your_ubuntu_ip_address\shared
    

    就可以看到共享的文件夹并进行文件传输。

注意:请将 your_usernameyour_ubuntu_ip_address 替换为实际的用户名和 Ubuntu 的 IP 地址。

ArkUI - 列表布局(List)

List

列表(List)是一种复杂容器,具备下列特点:

  • 列表项(ListItem)数量过多超出屏幕后,会自动提供滚动功能。
  • 列表项(ListItem)既可以纵向排列,也可以横向排列。

语法

List({ space: 10 }) {
  ForEach([1, 2, 3, 4], item => {
    ListItem() {
      Text('ListItem')
    }
  })
}
.width('100%')
.listDirection(Axis.Vertical)
  • space:列表项间距。

  • ListItem:列表项。它本身不是一个容器,代表 List 内部的一个项,需要把各种布局组件(Text、Button 等)写在 ListItem 里。因为 ListItem 内部只能包含一个根组件,所以如果要写多个组件,需要将组件包到 Row 或 Colum 容器里。

    ListItem() {
      Row() {
        ...
      }
    }
    
  • listDirection:列表方向,默认纵向。

    Axis.Vertical   // 纵向
    Axis.Horizontal // 横向
    

示例代码

class Item {
  name: string
  image: ResourceStr
  box_office: string

  constructor(name: string, image: ResourceStr, box_office: string) {
    this.name = name
    this.image = image
    this.box_office = box_office
  }
}
@Entry
@Component
struct Index {
  private items: Array<Item> = [
    { name: '热辣滚烫', image: $r('app.media.1'), box_office: '23.41亿' },
    { name: '飞驰人生2', image: $r('app.media.2'), box_office: '20.46亿' },
    { name: '熊出没·逆转时空', image: $r('app.media.3'), box_office: '11.82亿' },
    { name: '第二十条', image: $r('app.media.4'), box_office: '10.41亿' },
    { name: '我们一起摇太阳', image: $r('app.media.5'), box_office: '9618.7万' },
    { name: '红毯先生', image: $r('app.media.6'), box_office: '7884.5万' }
  ]

  build() {
    Column({ space: 8 }) {
      Row() {
        Text('2024春节档新片票房榜')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
      }

      List({ space: 8 }) {
        ForEach(
          this.items,
          (item: Item) => {
            ListItem() {
              Row({ space: 8 }) {
                Image(item.image)
                  .width(157)
                  .height(220)
                Column() {
                  Text(item.name)
                    .fontSize(20)
                    .fontWeight(FontWeight.Bold)
                  Text(item.box_office)
                    .fontSize(18)
                }
                .height('100%')
                .alignItems(HorizontalAlign.Start)
              }
              .width('100%')
              .height(220)
            }
          }
        )
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .padding(8)
  }
}

运行效果:
列表布局

ArkTS 声明式 UI 基础概念

如下代码是在 DevEco Studio 新建 Empty Ability 工程时自动创建的默认代码:

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

下面进行分层次逐一解析每一层的含义:

  1. 自定义组件

    struct Index {
    }
    

    可复用的UI单元

    所有代码都被封装到一个 struct 结构体当中,这个结构体在 ArkTS 中被称为自定义组件,它是一个可复用的UI单元。在这个组件的内部,可以去定义页面的元素、样式、功能等。

  2. 装饰器

    @Entry
    @Component
    struct Index {
      @State
    }
    

    用来装饰类结构、方法、变量。

    • @component:标记自定义组件
    • @entry:标记当前组件是入口组件
    • @State:标记该变量是状态变量,值变化时会触发UI刷新

    在这段代码中,在结构体的上面就有一个装饰器 @component,这个装饰器代表的含义就是组件的意思,也就是说用来标记这个结构体是一个组件的,所以它们结合起来才是一个自定义组件。

    @component 上面还有一个装饰器 @entry,用来标记当前组件是入口组件,也就是说这个组件是可以被独立去访问的,也就是说,它自己就是一个页面,在 App 内部可以直接访问。比如我们的首页肯定是一个入口型的组件,要加上 @entry;首页点击按钮跳到另一个页面,那肯定也是一个入口型的组件,所以这些都要加上 @entry。如果组件不加 @entry,它就是一个普通组件,不能直接显示,必须被其它的入口型组件引用,也就是说只是用来作页面元素的封装,起到一个可复用的效果。

    @State 的作用是用来标记一个变量是状态型变量,这样的变量就会被 ArkTS 去监控,一旦这个变量的值发生了变更,会去触发这个组件内部跟这个变量有关的页面元素重新渲染,这样就很容易的实现页面的动态变化。

  3. UI描述

    build() {
    }
    

    其内部以声明式方式描述UI结构

    用来描述组件内部的UI结构,描述的方式是声明式UI,在鸿蒙开发套件中,有一个叫做 ArkUI,它里面定义好了大量的UI范式和UI组件,因此,只需要利用它描述一下页面长什么样子,就会自动渲染,非常方便。

  4. 内置组件

    Row() {
      Column() {
        Text(this.message)
      }
    }
    

    ArkUI 内置的组件

    • 容器组件:用来完成页面布局,例如 Row(行式布局)、Column(列式布局)
    • 基础组件:自带样式和功能的页面元素,例如 Text(文本组件)
  5. 属性方法

     Text(this.message)
       .fontSize(50)
       .fontWeight(FontWeight.Bold)
    
    Row() {
      Column() {
      }
      .width('100%')
    }
    .height('100%')
    

    设置组件的UI样式

    比如 Text 组件被声明出来以后,去调用它的 fontSize 方法,就是去修改字体大小;调用 fontWeight 方法,就是去修改字体的粗细。

    行和列指定了宽度100%、高度100%,这样呢,元素就把整个页面给撑满了。

    所以这些都是用来控制UI的属性方法。

  6. 事件方法

    Text(this.message)
      .onClick(() => {
        // ... 处理点击事件
      })
    

    设置组件的事件回调

    Text 调用了一个 onClick 方法,然后传了一个回调函数,在里面编写业务逻辑,每当 Text 被点击时就会去触发并执行业务逻辑。

    因此任何一个组件都会有属性方法和事件方法,分别用来去控制组件的样式和添加事件回调。

系统启动流程分析 —— init 进程启动过程

本文基于 Android 14.0.0_r2 的系统启动流程分析。

一、概述

init 进程属于一个守护进程,准确的说,它是 Linux 系统中用户控制的第一个进程,它的进程号为 1,它的生命周期贯穿整个 Linux 内核运行的始终。Android 中所有其它的进程共同的鼻祖均为 init 进程。

可以通过 adb shell ps | grep init 命令来查看 init 的进程号:

wutianhao@wutianhao-Ubuntu:~$ adb shell ps | grep init
root             1     0 10858080   932 0                   0 S init

二、init 进程入口

init 入口函数是 main.cpp,它把各个阶段的操作分离开来,使代码更加简洁:

/system/core/init/main.cpp

int main(int argc, char** argv) {
    ...

    // 设置进程最高优先级 -20最高,20最低
    setpriority(PRIO_PROCESS, 0, -20);

    // 当 argv[0] 的内容为 ueventd 时,strcmp的值为 0,!strcmp 为 1;
    // 1 表示 true,也就执行 ueventd_main;
    // ueventd 主要是负责设备节点的创建、权限设定等一些列工作。
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }

    if (argc > 1) {
        // 参数为 subcontext,初始化日志系统。
        if (!strcmp(argv[1], "subcontext")) {
            android::base::InitLogging(argv, &android::base::KernelLogger);
            const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();

            return SubcontextMain(argc, argv, &function_map);
        }

        // 参数为 selinux_setup,启动 SELinux 安全策略
        if (!strcmp(argv[1], "selinux_setup")) {
            return SetupSelinux(argv);
        }

        // 参数为 second_stage,启动 init 进程第二阶段
        if (!strcmp(argv[1], "second_stage")) {
            return SecondStageMain(argc, argv);
        }
    }

    // 默认启动 init 进程第一阶段
    return FirstStageMain(argc, argv);
}

main 函数有四个参数入口:

  • 参数中有 ueventd,进入 ueventd_main。
  • 参数中有 subcontext,进入 InitLogging 和 SubcontextMain。
  • 参数中有 selinux_setup,进入 SetupSelinux。
  • 参数中有 second_stage,进入 SecondStageMain。

main 函数的执行顺序:

  1. ueventd_main:init 进程创建子进程 ueventd,并将创建设备节点文件的工作托付给 ueventd,ueventd 通过两种方式创建设备节点文件。
  2. FirstStageMain:启动第一阶段。
  3. SetupSelinux:加载 selinux 规则,并设置 selinux 日志,完成 SELinux 相关工作。
  4. SecondStageMain:启动第二阶段。

三、ueventd_main

ueventd_main 函数是 Android 系统中 ueventd 服务的主入口函数,负责处理和响应来自 Linux 内核的 uevents(设备事件)。

源码路径:/system/core/init/ueventd.cpp

  1. 初始化

    int ueventd_main(int argc, char** argv) {
        umask(000);
    
        android::base::InitLogging(argv, &android::base::KernelLogger);
    
        ...
    }
    • 首先调用 umask(000) 来设置进程创建文件时不受 umask 影响,确保新创建的文件具有指定的精确权限。
    • 接着初始化日志系统以便记录相关信息。
  2. SELinux 设置

    int ueventd_main(int argc, char** argv) {
        ...
    
        SelinuxSetupKernelLogging();
        SelabelInitialize();
    
        ...
    }
    • 调用 SelinuxSetupKernelLogging()SelabelInitialize() 函数来配置 SELinux 相关的日志以及标签库初始化。
  3. 创建 UeventHandler 对象

    int ueventd_main(int argc, char** argv) {
        ...
    
        std::vector<std::unique_ptr<UeventHandler>> uevent_handlers;
    
        auto ueventd_configuration = GetConfiguration();
    
        uevent_handlers.emplace_back(std::make_unique<DeviceHandler>(
                std::move(ueventd_configuration.dev_permissions),
                std::move(ueventd_configuration.sysfs_permissions),
                std::move(ueventd_configuration.subsystems), android::fs_mgr::GetBootDevices(), true));
        uevent_handlers.emplace_back(std::make_unique<FirmwareHandler>(
                std::move(ueventd_configuration.firmware_directories),
                std::move(ueventd_configuration.external_firmware_handlers)));
    
        if (ueventd_configuration.enable_modalias_handling) {
            std::vector<std::string> base_paths = {"/odm/lib/modules", "/vendor/lib/modules"};
            uevent_handlers.emplace_back(std::make_unique<ModaliasHandler>(base_paths));
        }
    
        ...
    }
    • 根据获取到的配置信息,创建并存储多个不同类型的 UeventHandler 子类实例,如 DeviceHandler、FirmwareHandler 和可能的 ModaliasHandler。这些处理器分别负责特定类型的设备事件处理,如挂载设备节点、管理固件目录等。
  4. 初始化 UeventListener

    int ueventd_main(int argc, char** argv) {
        ...
    
        UeventListener uevent_listener(ueventd_configuration.uevent_socket_rcvbuf_size);
    
        ...
    }
    • 创建一个 UeventListener 对象,用于监听内核通过 uevent socket 发送的 uevents,并配置接收缓冲区大小。
  5. 冷启动处理

    int ueventd_main(int argc, char** argv) {
        ...
    
        if (!android::base::GetBoolProperty(kColdBootDoneProp, false)) {
            ColdBoot cold_boot(uevent_listener, uevent_handlers,
                            ueventd_configuration.enable_parallel_restorecon,
                            ueventd_configuration.parallel_restorecon_dirs);
            cold_boot.Run();
        }
    
        ...
    }
    • 检查系统是否完成冷启动(android::base::GetBoolProperty(kColdBootDoneProp, false)),如果尚未完成,则执行 ColdBoot 类的 Run() 方法进行冷启动相关的设备事件处理和权限恢复。
  6. 冷启动完成通知

    int ueventd_main(int argc, char** argv) {
        ...
    
        for (auto& uevent_handler : uevent_handlers) {
            uevent_handler->ColdbootDone();
        }
    
        ...
    }
    • 所有 UeventHandler 对象调用 ColdbootDone() 方法以表明冷启动阶段已完成。
  7. 信号处理

    int ueventd_main(int argc, char** argv) {
        ...
    
        signal(SIGCHLD, SIG_IGN);
        while (waitpid(-1, nullptr, WNOHANG) > 0) {
        }
    
        ...
    }
    • 忽略子进程结束信号 SIGCHLD,并清理任何已退出但未被收集的子进程。
  8. 恢复优先级

    int ueventd_main(int argc, char** argv) {
        ...
    
        setpriority(PRIO_PROCESS, 0, 0);
    
        ...
    }
    • 调用 setpriority(PRIO_PROCESS, 0, 0) 来恢复进程的默认优先级。
  9. 主循环

    int ueventd_main(int argc, char** argv) {
        ...
    
        uevent_listener.Poll([&uevent_handlers](const Uevent& uevent) {
            for (auto& uevent_handler : uevent_handlers) {
                uevent_handler->HandleUevent(uevent);
            }
            return ListenerAction::kContinue;
        });
    
        ...
    }
    • 进入主循环,使用 UeventListener 的 Poll() 方法监听 uevents。当接收到 uevent 时,遍历所有 UeventHandler 对象并调用它们的 HandleUevent() 方法来处理相应的事件。

总结:ueventd_main 函数在 Android 启动过程中扮演着核心角色,它负责监听和处理与硬件设备状态变化相关的事件,确保系统能够正确识别并响应设备添加、移除或属性更改等操作,从而使得设备驱动和用户空间能够有效地交互。

四、FirstStageMain

FirstStageMain 是 Android 系统启动流程中第一阶段初始化的主入口函数,它负责在系统启动早期进行一系列关键的系统设置和挂载操作。

源码路径:/system/core/init/first_stage_init.cpp

  1. 信号处理

    int FirstStageMain(int argc, char** argv) {
        
        if (REBOOT_BOOTLOADER_ON_PANIC) {
            InstallRebootSignalHandlers();
        }
    
        ...
    }
    • 如果定义了 REBOOT_BOOTLOADER_ON_PANIC,则安装重启到引导加载器的信号处理程序,在系统出现 panic 时执行。
  2. 时间戳记录与错误检查宏

    int FirstStageMain(int argc, char** argv) {
        ...
    
        boot_clock::time_point start_time = boot_clock::now();
    
        std::vector<std::pair<std::string, int>> errors;
    #define CHECKCALL(x) \
        if ((x) != 0) errors.emplace_back(#x " failed", errno);
    
        ...
    }
    • 记录启动时间点。
    • 定义一个宏 CHECKCALL(x),用于调用函数 x 并检查其返回值是否为0(表示成功),若非0,则将错误信息添加至错误列表中。
  3. 设置文件或目录的默认权限

    int FirstStageMain(int argc, char** argv) {
        ...
    
        umask(0);
    
        ...
    }
    • 当 umask 值为 0 时,意味着新建的文件或目录将具有最大权限,即对于文件来说是 666(rw-rw-rw-),对于目录来说是 777(rwxrwxrwx)。
  4. 环境清理与基本文件系统准备

    int FirstStageMain(int argc, char** argv) {
        ...
        
        CHECKCALL(clearenv());
        CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
        CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
        CHECKCALL(mkdir("/dev/pts", 0755));
        CHECKCALL(mkdir("/dev/socket", 0755));
        CHECKCALL(mkdir("/dev/dm-user", 0755));
        CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
    #define MAKE_STR(x) __STRING(x)
        CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
    #undef MAKE_STR
        CHECKCALL(chmod("/proc/cmdline", 0440));
        std::string cmdline;
        android::base::ReadFileToString("/proc/cmdline", &cmdline);
        chmod("/proc/bootconfig", 0440);
        std::string bootconfig;
        android::base::ReadFileToString("/proc/bootconfig", &bootconfig);
        gid_t groups[] = {AID_READPROC};
        CHECKCALL(setgroups(arraysize(groups), groups));
        CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
        CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));
    
        ...
    }
    • 清除当前进程的环境变量。
    • 设置 PATH 环境变量为默认值。
    • 挂载临时文件系统 tmpfs 到 /dev 目录下,并创建必要的子目录如 /dev/pts/dev/socket/dev/dm-user
    • 按需挂载 proc、sysfs、selinuxfs 文件系统并调整相关权限。
  5. 特殊设备节点创建

    int FirstStageMain(int argc, char** argv) {
        ...
        
        CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));
    
        if constexpr (WORLD_WRITABLE_KMSG) {
            CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
        }
    
        CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
        CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));
    
        CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
        CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));
    
        CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                        "mode=0755,uid=0,gid=1000"));
        CHECKCALL(mkdir("/mnt/vendor", 0755));
        CHECKCALL(mkdir("/mnt/product", 0755));
    
        CHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                        "mode=0755,uid=0,gid=0"));
    
        CHECKCALL(mount("tmpfs", kSecondStageRes, "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                        "mode=0755,uid=0,gid=0"))
    #undef CHECKCALL
    
        ...
    }
    • 创建 kmsg、random、urandom、ptmx 和 null 等特殊设备节点。
  6. 日志初始化与权限控制

    int FirstStageMain(int argc, char** argv) {
        ...
    
        SetStdioToDevNull(argv);
        InitKernelLogging(argv);
    
        ...
    }
    • 将标准输入输出重定向至 /dev/null,以避免不必要的输出干扰。
    • 初始化内核日志功能。
  7. 挂载特定临时文件系统

    int FirstStageMain(int argc, char** argv) {
        ...
    
        LOG(INFO) << "init first stage started!";
    
        auto old_root_dir = std::unique_ptr<DIR, decltype(&closedir)>{opendir("/"), closedir};
        if (!old_root_dir) {
            PLOG(ERROR) << "Could not opendir(\"/\"), not freeing ramdisk";
        }
    
        struct stat old_root_info;
        if (stat("/", &old_root_info) != 0) {
            PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
            old_root_dir.reset();
        }
    
        ...
    }
    • /mnt/mnt/vendor/mnt/product 下挂载临时文件系统,为后续挂载分区做准备。
    • 创建 /debug_ramdisk 和第二阶段资源存储目录,并挂载临时文件系统。
  8. 模块加载及计时

    int FirstStageMain(int argc, char** argv) {
        ...
    
        auto want_console = ALLOW_FIRST_STAGE_CONSOLE ? FirstStageConsole(cmdline, bootconfig) : 0;
        auto want_parallel =
                bootconfig.find("androidboot.load_modules_parallel = \"true\"") != std::string::npos;
    
        boot_clock::time_point module_start_time = boot_clock::now();
        int module_count = 0;
        if (!LoadKernelModules(IsRecoveryMode() && !ForceNormalBoot(cmdline, bootconfig), want_console,
                            want_parallel, module_count)) {
            if (want_console != FirstStageConsoleParam::DISABLED) {
                LOG(ERROR) << "Failed to load kernel modules, starting console";
            } else {
                LOG(FATAL) << "Failed to load kernel modules";
            }
        }
        if (module_count > 0) {
            auto module_elapse_time = std::chrono::duration_cast<std::chrono::milliseconds>(
                    boot_clock::now() - module_start_time);
            setenv(kEnvInitModuleDurationMs, std::to_string(module_elapse_time.count()).c_str(), 1);
            LOG(INFO) << "Loaded " << module_count << " kernel modules took "
                    << module_elapse_time.count() << " ms";
        }
    
        ...
    }
    • 加载内核模块,可以按照配置选择是否并行加载,并统计加载耗时。
  9. 创建设备节点与控制台启动

    int FirstStageMain(int argc, char** argv) {
        ...
    
        bool created_devices = false;
        if (want_console == FirstStageConsoleParam::CONSOLE_ON_FAILURE) {
            if (!IsRecoveryMode()) {
                created_devices = DoCreateDevices();
                if (!created_devices) {
                    LOG(ERROR) << "Failed to create device nodes early";
                }
            }
            StartConsole(cmdline);
        }
    
        ...
    }
    • 根据需要创建设备节点。
    • 根据配置决定是否启动控制台。
  10. ramdisk 属性复制

    int FirstStageMain(int argc, char** argv) {
        ...
    
        if (access(kBootImageRamdiskProp, F_OK) == 0) {
            std::string dest = GetRamdiskPropForSecondStage();
            std::string dir = android::base::Dirname(dest);
            std::error_code ec;
            if (!fs::create_directories(dir, ec) && !!ec) {
                LOG(FATAL) << "Can't mkdir " << dir << ": " << ec.message();
            }
            if (!fs::copy_file(kBootImageRamdiskProp, dest, ec)) {
                LOG(FATAL) << "Can't copy " << kBootImageRamdiskProp << " to " << dest << ": "
                        << ec.message();
            }
            LOG(INFO) << "Copied ramdisk prop to " << dest;
        }
    
        ...
    }
    • 将 bootimage 中 ramdisk 的属性复制到指定位置以便于第二阶段使用。
  11. 调试模式支持

    int FirstStageMain(int argc, char** argv) {
        ...
    
        if (access("/force_debuggable", F_OK) == 0) {
            constexpr const char adb_debug_prop_src[] = "/adb_debug.prop";
            constexpr const char userdebug_plat_sepolicy_cil_src[] = "/userdebug_plat_sepolicy.cil";
            std::error_code ec;
            if (access(adb_debug_prop_src, F_OK) == 0 &&
                !fs::copy_file(adb_debug_prop_src, kDebugRamdiskProp, ec)) {
                LOG(WARNING) << "Can't copy " << adb_debug_prop_src << " to " << kDebugRamdiskProp
                            << ": " << ec.message();
            }
            if (access(userdebug_plat_sepolicy_cil_src, F_OK) == 0 &&
                !fs::copy_file(userdebug_plat_sepolicy_cil_src, kDebugRamdiskSEPolicy, ec)) {
                LOG(WARNING) << "Can't copy " << userdebug_plat_sepolicy_cil_src << " to "
                            << kDebugRamdiskSEPolicy << ": " << ec.message();
            }
            setenv("INIT_FORCE_DEBUGGABLE", "true", 1);
        }
    
        ...
    }
    • 当检测到 "/force_debuggable" 文件存在时,会启用用户debug模式相关的设置,例如允许adb root访问等。
  12. 切换根文件系统

    int FirstStageMain(int argc, char** argv) {
        ...
    
        if (ForceNormalBoot(cmdline, bootconfig)) {
            mkdir("/first_stage_ramdisk", 0755);
            PrepareSwitchRoot();
            if (mount("/first_stage_ramdisk", "/first_stage_ramdisk", nullptr, MS_BIND, nullptr) != 0) {
                PLOG(FATAL) << "Could not bind mount /first_stage_ramdisk to itself";
            }
            SwitchRoot("/first_stage_ramdisk");
        }
    
        ...
    }
    • 如果满足条件(例如非恢复模式且不强制正常启动),则执行切换根文件系统的操作,包括创建新目录、绑定挂载以及调用 SwitchRoot() 函数。
  13. 完成第一阶段挂载

    int FirstStageMain(int argc, char** argv) {
        ...
    
        if (!DoFirstStageMount(!created_devices)) {
            LOG(FATAL) << "Failed to mount required partitions early ...";
        }
    
        ...
    }
    • 执行 DoFirstStageMount() 函数来挂载启动过程中所需的必要分区。
  14. 释放旧 ramdisk 资源

    int FirstStageMain(int argc, char** argv) {
        ...
    
        struct stat new_root_info;
        if (stat("/", &new_root_info) != 0) {
            PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
            old_root_dir.reset();
        }
    
        if (old_root_dir && old_root_info.st_dev != new_root_info.st_dev) {
            FreeRamdisk(old_root_dir.get(), old_root_info.st_dev);
        }
    
        ...
    }
    • 验证旧根文件系统是否已成功切换,如果已切换,则释放旧的 ramdisk 相关资源。
  15. 其他系统设置

    int FirstStageMain(int argc, char** argv) {
        ...
    
        SetInitAvbVersionInRecovery();
    
        setenv(kEnvFirstStageStartedAt, std::to_string(start_time.time_since_epoch().count()).c_str(),
            1);
    • 设置 AVB 版本信息等额外操作。
  16. 进入 SetupSelinux

    int FirstStageMain(int argc, char** argv) {
        ...
    
        const char* path = "/system/bin/init";
        const char* args[] = {path, "selinux_setup", nullptr};
        auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        close(fd);
        execv(path, const_cast<char**>(args));
    
        PLOG(FATAL) << "execv(\"" << path << "\") failed";
    
        return 1;
    }
    • 通过 execv() 函数执行 /system/bin/init 进程,传入 "selinux_setup" 参数作为子进程的启动参数,开始进入 SetupSelinux。

总结:整个 FirstStageMain 函数确保了 Android 系统在初始启动阶段能够正确地设置文件系统结构、加载必需的内核模块、建立基础设备节点以及安全地切换到下一阶段的初始化流程。

五、SetupSelinux

SetupSelinux 是 Android 系统启动流程中用于设置和初始化 SELinux 环境的关键函数。

源码路径:/system/core/init/selinux.cpp

  1. 标准输入输出重定向与内核日志初始化

    int SetupSelinux(char** argv) {
        SetStdioToDevNull(argv);
        InitKernelLogging(argv);
    
        ...
    }
    • SetStdioToDevNull(argv) 将标准输入、输出和错误重定向至 /dev/null,防止无用的日志输出。
    • InitKernelLogging(argv) 初始化内核日志功能。
  2. 信号处理

    int SetupSelinux(char** argv) {
        ...
    
        if (REBOOT_BOOTLOADER_ON_PANIC) {
            InstallRebootSignalHandlers();
        }
    
        ...
    }
    • 如果定义了 REBOOT_BOOTLOADER_ON_PANIC,则安装重启到引导加载器的信号处理程序。
  3. 计时并挂载分区

    int SetupSelinux(char** argv) {
        ...
    
        boot_clock::time_point start_time = boot_clock::now();
    
        MountMissingSystemPartitions();
    
        ...
    }
    • 记录开始时间点,并调用 MountMissingSystemPartitions() 挂载必要的系统分区。
  4. SELinux 内核日志设置

    int SetupSelinux(char** argv) {
        ...
    
        SelinuxSetupKernelLogging();
    
        ...
    }
    • 调用 SelinuxSetupKernelLogging() 来设置SELinux相关的内核日志参数。
  5. 准备和读取SELinux策略

    int SetupSelinux(char** argv) {
        ...
    
        PrepareApexSepolicy();
    
        std::string policy;
        ReadPolicy(&policy);
        CleanupApexSepolicy();
    
        ...
    }
    • 准备Apex SELinux策略文件 (PrepareApexSepolicy)。
    • 读取SELinux策略文件的内容并将它存储在字符串变量 policy 中。
    • 清理 Apex SELinux 策略文件相关资源。
  6. 管理 snapuserd 守护进程

    int SetupSelinux(char** argv) {
        ...
    
        auto snapuserd_helper = SnapuserdSelinuxHelper::CreateIfNeeded();
        if (snapuserd_helper) {
            snapuserd_helper->StartTransition();
        }
    
        ...
    }
    • 创建或获取一个 SnapuserdSelinuxHelper 对象来管理 snapuserd 守护进程的 SELinux 上下文转换。
    • 如果需要,杀死旧的 snapuserd 进程以避免产生审计消息,并开始转换过程。
  7. 加载 SELinux 策略

    int SetupSelinux(char** argv) {
        ...
    
        LoadSelinuxPolicy(policy);
    
        ...
    }
    • 使用之前读取的 policy 字符串内容加载 SELinux 策略 (LoadSelinuxPolicy(policy))。
  8. 完成 snapuserd 的 SELinux 上下文转换

    int SetupSelinux(char** argv) {
        ...
    
        if (snapuserd_helper) {
            snapuserd_helper->FinishTransition();
            snapuserd_helper = nullptr;
        }
    
        ...
    }
    • 如果存在 snapuserd_helper,完成其 SELinux 上下文转换过程,并释放该对象。
  9. 恢复上下文与设置强制执行模式

    int SetupSelinux(char** argv) {
        ...
    
        if (selinux_android_restorecon("/dev/selinux/", SELINUX_ANDROID_RESTORECON_RECURSE) == -1) {
            PLOG(FATAL) << "restorecon failed of /dev/selinux failed";
        }
    
        SelinuxSetEnforcement();
    
        ...
    }
    • 恢复 /dev/selinux/ 目录及其子目录下的文件到正确的SELinux上下文。
    • 设置 SELinux 进入强制执行模式 (SelinuxSetEnforcement)。
  10. 针对 init 进程进行额外的上下文恢复

    int SetupSelinux(char** argv) {
        ...
    
        if (selinux_android_restorecon("/system/bin/init", 0) == -1) {
            PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
        }
    
        ...
    }
    • 特别对 /system/bin/init 进行 SELinux 上下文恢复,确保其拥有正确权限以便进行后续启动操作。
  11. 设置环境变量

    int SetupSelinux(char** argv) {
        ...
    
        setenv(kEnvSelinuxStartedAt, std::to_string(start_time.time_since_epoch().count()).c_str(), 1);
    
        ...
    }
    • 设置环境变量 kEnvSelinuxStartedAt 记录 SELinux 启动的时间点。
  12. 执行第二阶段 init 进程

    int SetupSelinux(char** argv) {
        ...
    
        const char* path = "/system/bin/init";
        const char* args[] = {path, "second_stage", nullptr};
        execv(path, const_cast<char**>(args));
    
        PLOG(FATAL) << "execv(\"" << path << "\") failed";
    
        return 1;
    }
    • 准备参数数组,包含命令路径 "/system/bin/init" 和参数 "second_stage"。
    • 使用 execv 系统调用执行新的 init 进程,进入系统的第二阶段初始化。
    • 如果 execv 函数返回(通常表示出错),会记录致命错误并退出程序。由于在成功执行 execv 后不会返回,所以这里的 PLOG(FATAL) ... 是一种异常情况处理。

总结:通过 SetupSelinux 函数的执行,Android 系统能够在启动时正确地建立和启用 SELinux 安全机制,为后续系统的运行提供安全保障。

六、SecondStageMain

SecondStageMain 函数是 Android 系统启动过程中的第二个阶段,主要负责更深层次的系统初始化工作。

源码路径:/system/core/init/init.cpp

  1. 信号处理

    int SecondStageMain(int argc, char** argv) {
        if (REBOOT_BOOTLOADER_ON_PANIC) {
            InstallRebootSignalHandlers();
        }
    
        boot_clock::time_point start_time = boot_clock::now();
    
        trigger_shutdown = [](const std::string& command) { shutdown_state.TriggerShutdown(command); };
    
        SetStdioToDevNull(argv);
        InitKernelLogging(argv);
        LOG(INFO) << "init second stage started!";
    
        SelinuxSetupKernelLogging();
    
        ...
    }
    • 设置重启至引导加载器的信号处理程序,并忽略 SIGPIPE 信号,防止因管道破裂导致的进程崩溃。
  2. 资源准备与清理

    int SecondStageMain(int argc, char** argv) {
        ...
    
        if (setenv("PATH", _PATH_DEFPATH, 1) != 0) {
            PLOG(FATAL) << "Could not set $PATH to '" << _PATH_DEFPATH << "' in second stage";
        }
    
        {
            struct sigaction action = {.sa_flags = SA_RESTART};
            action.sa_handler = [](int) {};
            sigaction(SIGPIPE, &action, nullptr);
        }
    
        if (auto result =
                    WriteFile("/proc/1/oom_score_adj", StringPrintf("%d", DEFAULT_OOM_SCORE_ADJUST));
            !result.ok()) {
            LOG(ERROR) << "Unable to write " << DEFAULT_OOM_SCORE_ADJUST
                    << " to /proc/1/oom_score_adj: " << result.error();
        }
    
        keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1);
    
        close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
    
        const char* force_debuggable_env = getenv("INIT_FORCE_DEBUGGABLE");
        bool load_debug_prop = false;
        if (force_debuggable_env && AvbHandle::IsDeviceUnlocked()) {
            load_debug_prop = "true"s == force_debuggable_env;
        }
        unsetenv("INIT_FORCE_DEBUGGABLE");
    
        if (!load_debug_prop) {
            UmountDebugRamdisk();
        }
    
        ...
    }
    • 设置 PATH 变量值。
    • 调整 init 进程及其子进程的内存管理器 OOM 分数调整值。
    • 设置会话密钥环,保证进程间共享加密密钥的安全性。
    • 标记正在启动状态,打开 /dev/.booting 文件。
    • 根据解锁状态和环境变量决定是否加载调试属性,并卸载调试 ramdisk。
  3. 系统服务和属性初始化

    int SecondStageMain(int argc, char** argv) {
        ...
    
        PropertyInit();
    
        UmountSecondStageRes();
    
        if (load_debug_prop) {
            UmountDebugRamdisk();
        }
    
        MountExtraFilesystems();
    
        SelabelInitialize();
        SelinuxRestoreContext();
    
        ...
    }
    • 初始化属性服务,加载系统属性。
    • 卸载第二阶段资源。
    • 挂载额外的文件系统。
    • 初始化 SELinux 并恢复上下文。
  4. 事件循环与回调设置

    int SecondStageMain(int argc, char** argv) {
        ...
    
        Epoll epoll;
        if (auto result = epoll.Open(); !result.ok()) {
            PLOG(FATAL) << result.error();
        }
    
        epoll.SetFirstCallback(ReapAnyOutstandingChildren);
    
        InstallSignalFdHandler(&epoll);
        InstallInitNotifier(&epoll);
        StartPropertyService(&property_fd);
    
        ...
    }
    • 使用 Epoll 实现 I/O 多路复用,设置信号处理回调,处理子进程退出事件。
    • 安装 Init 通知器,启动属性服务。
  5. 关键数据记录与设置

    int SecondStageMain(int argc, char** argv) {
        ...
    
        RecordStageBoottimes(start_time);
    
        if (const char* avb_version = getenv("INIT_AVB_VERSION"); avb_version != nullptr) {
            SetProperty("ro.boot.avb_version", avb_version);
        }
        unsetenv("INIT_AVB_VERSION");
    
        ...
    }
    • 记录启动阶段的时间点供 bootstat 使用。
    • 设置 libavb 版本属性。
  6. 系统特定功能

    int SecondStageMain(int argc, char** argv) {
        ...
    
        fs_mgr_vendor_overlay_mount_all();
        export_oem_lock_status();
        MountHandler mount_handler(&epoll);
        SetUsbController();
        SetKernelVersion();
    
        const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();
        Action::set_function_map(&function_map);
    
        if (!SetupMountNamespaces()) {
            PLOG(FATAL) << "SetupMountNamespaces failed";
        }
    
        InitializeSubcontext();
    
        ActionManager& am = ActionManager::GetInstance();
        ServiceList& sm = ServiceList::GetInstance();
    
        LoadBootScripts(am, sm);
    
        ...
    }
    • 负责厂商层叠挂载、导出 OEM 锁定状态、挂载处理器、设置 USB 控制器、设置内核版本等。
    • 设置内置函数映射表,加载启动脚本。
  7. 初始化命名空间与控制组

    int SecondStageMain(int argc, char** argv) {
        ...
    
        if (false) DumpState();
    
        auto is_running = android::gsi::IsGsiRunning() ? "1" : "0";
        SetProperty(gsi::kGsiBootedProp, is_running);
        auto is_installed = android::gsi::IsGsiInstalled() ? "1" : "0";
        SetProperty(gsi::kGsiInstalledProp, is_installed);
    
        ...
    }
    • 设置挂载命名空间。
    • 初始化子上下文。
  8. 调度内置动作

    int SecondStageMain(int argc, char** argv) {
        ...
    
        am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");
        am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");
        am.QueueBuiltinAction(TestPerfEventSelinuxAction, "TestPerfEventSelinux");
        am.QueueBuiltinAction(ConnectEarlyStageSnapuserdAction, "ConnectEarlyStageSnapuserd");
        am.QueueEventTrigger("early-init");
    
        am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
        am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
        Keychords keychords;
        am.QueueBuiltinAction(
                [&epoll, &keychords](const BuiltinArguments& args) -> Result<void> {
                    for (const auto& svc : ServiceList::GetInstance()) {
                        keychords.Register(svc->keycodes());
                    }
                    keychords.Start(&epoll, HandleKeychord);
                    return {};
                },
                "KeychordInit");
    
        am.QueueEventTrigger("init");
    
        ...
    }
    • 队列化一系列内置动作,如设置 cgroups、设置 kptr_restrict 等安全选项。
    • 注册并启动按键组合(Keychord)处理。
  9. 启动与触发事件

    int SecondStageMain(int argc, char** argv) {
        ...
    
        std::string bootmode = GetProperty("ro.bootmode", "");
        if (bootmode == "charger") {
            am.QueueEventTrigger("charger");
        } else {
            am.QueueEventTrigger("late-init");
        }
    
        am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");
    
        ...
    }
    • 根据不同的启动模式触发相应事件(正常启动、充电模式等)。
    • 基于当前属性状态触发属性关联的事件。
  10. 主循环

    int SecondStageMain(int argc, char** argv) {
        ...
    
        while (true) {
            const boot_clock::time_point far_future = boot_clock::time_point::max();
            boot_clock::time_point next_action_time = far_future;
    
            auto shutdown_command = shutdown_state.CheckShutdown();
            if (shutdown_command) {
                LOG(INFO) << "Got shutdown_command '" << *shutdown_command
                        << "' Calling HandlePowerctlMessage()";
                HandlePowerctlMessage(*shutdown_command);
            }
    
            if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
                am.ExecuteOneCommand();
                if (am.HasMoreCommands()) {
                    next_action_time = boot_clock::now();
                }
            }
            if (!IsShuttingDown()) {
                auto next_process_action_time = HandleProcessActions();
    
                if (next_process_action_time) {
                    next_action_time = std::min(next_action_time, *next_process_action_time);
                }
            }
    
            std::optional<std::chrono::milliseconds> epoll_timeout;
            if (next_action_time != far_future) {
                epoll_timeout = std::chrono::ceil<std::chrono::milliseconds>(
                        std::max(next_action_time - boot_clock::now(), 0ns));
            }
            auto epoll_result = epoll.Wait(epoll_timeout);
            if (!epoll_result.ok()) {
                LOG(ERROR) << epoll_result.error();
            }
            if (!IsShuttingDown()) {
                HandleControlMessages();
                SetUsbController();
            }
        }
    
        ...
    }
    • 在无限循环中,根据队列中的动作计划执行相关命令。
    • 监听并处理控制消息,如电源管理命令(如关机、重启等)。
    • 处理进程管理和重启操作。
    • 检查并更新 USB 控制器状态。

总结:SecondStageMain 函数对 Android 系统进行全面深入的初始化,包括设置系统资源、挂载文件系统、启动关键服务、初始化安全性组件以及执行启动脚本等。整个函数通过事件循环持续监控并响应系统内部及外部事件,直至系统完全启动就绪。

七、信号处理

init 是一个守护进程,为了防止 init 的子进程成为僵尸进程(zombie process),需要 init 在子进程在结束时获取子进程的结束码,通过结束码将程序表中的子进程移除,防止成为僵尸进程的子进程占用程序表的空间(程序表的空间达到上限时,系统就不能再启动新的进程了,会引起严重的系统问题)。

子进程重启流程如下图所示:

信号处理主要工作:

  • 初始化信号 signal 句柄
  • 循环处理子进程
  • 注册 epoll 句柄
  • 处理子进程终止

注意:EPOLL 类似于 POLL,是 Linux 中用来做事件触发的,跟 EventBus 功能差不多。Linux 很长的时间都在使用 select 来做事件触发,它是通过轮询来处理的,轮询的 fd 数目越多,自然耗时越多,对于大量的描述符处理,EPOLL 更有优势。

  1. InstallSignalFdHandler

    InstallSignalFdHandler 用于在 Linux 系统中设置信号处理。

    源码路径:/system/core/init/init.cpp

    static void InstallSignalFdHandler(Epoll* epoll) {
        // 设置一个默认的 SIGCHLD 信号处理器,并使用 sigaction 函数应用了 SA_NOCLDSTOP 标志。这可以防止当子进程停止或继续时,signalfd 接收到 SIGCHLD 信号。
        const struct sigaction act { .sa_handler = SIG_DFL, .sa_flags = SA_NOCLDSTOP };
        sigaction(SIGCHLD, &act, nullptr);
    
        // 创建一个信号集,将 SIGCHLD 信号添加到该集中。
        sigset_t mask;
        sigemptyset(&mask);
        sigaddset(&mask, SIGCHLD);
    
        // 如果当前进程没有重启能力(可能是在容器中运行),那么它还会将 SIGTERM 信号添加到信号集中。这是因为在容器环境中,接收到 SIGTERM 信号通常会导致系统关闭。
        if (!IsRebootCapable()) {
            sigaddset(&mask, SIGTERM);
        }
    
        // 使用 sigprocmask 函数阻塞这些信号。如果阻塞失败,它将记录一条致命错误日志。
        if (sigprocmask(SIG_BLOCK, &mask, nullptr) == -1) {
            PLOG(FATAL) << "failed to block signals";
        }
    
        // 使用 pthread_atfork 函数注册一个 fork 处理器,该处理器在子进程中解除信号阻塞。如果注册失败,它将记录一条致命错误日志。
        const int result = pthread_atfork(nullptr, nullptr, &UnblockSignals);
        if (result != 0) {
            LOG(FATAL) << "Failed to register a fork handler: " << strerror(result);
        }
    
        // 使用 signalfd 函数创建一个信号文件描述符,用于接收信号。如果创建失败,它将记录一条致命错误日志。
        signal_fd = signalfd(-1, &mask, SFD_CLOEXEC);
        if (signal_fd == -1) {
            PLOG(FATAL) << "failed to create signalfd";
        }
    
        // 使用 epoll->RegisterHandler 函数注册一个处理器,用于处理从信号文件描述符接收到的信号。如果注册失败,它将记录一条致命错误日志。
        constexpr int flags = EPOLLIN | EPOLLPRI;
        if (auto result = epoll->RegisterHandler(signal_fd, HandleSignalFd, flags); !result.ok()) {
            LOG(FATAL) << result.error();
        }
    }

    InstallSignalFdHandler 函数的主要目的是设置一个系统,使得当特定的信号发生时,可以通过文件描述符来接收和处理这些信号。

  2. RegisterHandler

    RegisterHandler 在 Epoll 类中定义。这个函数的目的是注册一个处理器(Handler)来处理特定文件描述符(fd)的事件。这是通过 Linux 的 epoll 机制实现的,epoll 是一种 I/O 多路复用技术。

    源码路径:/system/core/init/epoll.cpp

    Result<void> Epoll::RegisterHandler(int fd, Handler handler, uint32_t events) {
        // 检查是否指定了事件(events)。如果没有指定任何事件,函数将返回一个错误。
        if (!events) {
            return Error() << "Must specify events";
        }
    
        // 尝试将文件描述符(fd)和一个包含事件和处理器的 Info 对象插入到 epoll_handlers_ 映射中。
        auto [it, inserted] = epoll_handlers_.emplace(
                fd, Info{
                            .events = events,
                            .handler = std::move(handler),
                    });
        // 如果插入失败(也就是说,给定的文件描述符已经有一个处理器),函数将返回一个错误。
        if (!inserted) {
            return Error() << "Cannot specify two epoll handlers for a given FD";
        }
        // 创建一个 epoll_event 结构体,该结构体包含了事件和文件描述符。
        epoll_event ev = {
                .events = events,
                .data.fd = fd,
        };
        // 使用 epoll_ctl 函数将文件描述符添加到 epoll 实例中。如果这个操作失败,它将从 epoll_handlers_ 映射中删除文件描述符,并返回一个错误。
        if (epoll_ctl(epoll_fd_.get(), EPOLL_CTL_ADD, fd, &ev) == -1) {
            Result<void> result = ErrnoError() << "epoll_ctl failed to add fd";
            epoll_handlers_.erase(fd);
            return result;
        }
    
        // 如果所有操作都成功,函数将返回一个空的 Result 对象,表示操作成功。
        return {};
    }

    RegisterHandler 函数的主要用途是设置 epoll,使得当文件描述符上发生指定的事件时,可以调用相应的处理器来处理这些事件。

  3. HandleSignalFd

    HandleSignalFd 用于处理通过 signal_fd 接收到的信号。这个函数使用了 Linux 的 signalfd 机制,该机制允许通过文件描述符接收信号。

    源码路径:/system/core/init/init.cpp

    static void HandleSignalFd() {
        // 定义了一个 signalfd_siginfo 结构体,用于存储从 signal_fd 读取的信号信息。
        signalfd_siginfo siginfo;
        // 尝试从 signal_fd 读取信号信息。如果读取失败,将记录一条错误日志并返回。
        ssize_t bytes_read = TEMP_FAILURE_RETRY(read(signal_fd, &siginfo, sizeof(siginfo)));
        // 如果成功读取到信号信息,将检查读取的字节数是否等于 signalfd_siginfo 的大小。如果不等,将记录一条错误日志并返回。
        if (bytes_read != sizeof(siginfo)) {
            PLOG(ERROR) << "Failed to read siginfo from signal_fd";
            return;
        }
    
        switch (siginfo.ssi_signo) {
            case SIGCHLD:
                ReapAnyOutstandingChildren();
                break;
            case SIGTERM:
                HandleSigtermSignal(siginfo);
                break;
            default:
                LOG(ERROR) << "signal_fd: received unexpected signal " << siginfo.ssi_signo;
                break;
        }
    }

    HandleSignalFd 函数的主要用途是处理通过 signal_fd 接收到的信号,以便在接收到特定的信号时执行相应的操作。

  4. ReapOneProcess

    ReapOneProcess 用于处理子进程的结束。它使用了 Linux 的 waitid 和 waitpid 系统调用来收集子进程的退出状态,防止子进程变成僵尸进程。

    源码路径:/system/core/init/sigchld_handler.cpp

    void ReapAnyOutstandingChildren() {
        while (ReapOneProcess() != 0) {
        }
    }
    
    static pid_t ReapOneProcess() {
        // 定义一个 siginfo_t 结构体,用于存储从 waitid 系统调用中获取的信息。
        siginfo_t siginfo = {};
        // 调用 waitid 系统调用来获取一个僵尸进程的 PID,或者得知没有更多的僵尸进程需要处理。注意,这个调用并不实际收集僵尸进程的退出状态,这个操作在后面进行。
        if (TEMP_FAILURE_RETRY(waitid(P_ALL, 0, &siginfo, WEXITED | WNOHANG | WNOWAIT)) != 0) {
            PLOG(ERROR) << "waitid failed";
            return 0;
        }
    
        // 如果 waitid 调用失败,它将记录一条错误日志并返回 0。
        const pid_t pid = siginfo.si_pid;
        if (pid == 0) {
            DCHECK_EQ(siginfo.si_signo, 0);
            return 0;
        }
    
        // 如果 waitid 调用成功,它将检查返回的 PID 是否为 0。如果是,它将确保信号编号为 0,然后返回0。如果 PID 不为 0,它将确保信号编号为SIGCHLD。
        DCHECK_EQ(siginfo.si_signo, SIGCHLD);
    
        // 创建一个作用域保护器(scope guard)。这个保护器在函数返回时会自动调用 waitpid 系统调用来收集僵尸进程的退出状态。
        auto reaper = make_scope_guard([pid] { TEMP_FAILURE_RETRY(waitpid(pid, nullptr, WNOHANG)); });
    
        std::string name;
        std::string wait_string;
        Service* service = nullptr;
    
        // 尝试找到与 PID 对应的服务。如果找到,将记录服务的名称和 PID,以及服务的执行时间。如果没有找到,将记录 PID。
        if (SubcontextChildReap(pid)) {
            name = "Subcontext";
        } else {
            service = ServiceList::GetInstance().FindService(pid, &Service::pid);
    
            if (service) {
                name = StringPrintf("Service '%s' (pid %d)", service->name().c_str(), pid);
                if (service->flags() & SVC_EXEC) {
                    auto exec_duration = boot_clock::now() - service->time_started();
                    auto exec_duration_ms =
                        std::chrono::duration_cast<std::chrono::milliseconds>(exec_duration).count();
                    wait_string = StringPrintf(" waiting took %f seconds", exec_duration_ms / 1000.0f);
                } else if (service->flags() & SVC_ONESHOT) {
                    auto exec_duration = boot_clock::now() - service->time_started();
                    auto exec_duration_ms =
                            std::chrono::duration_cast<std::chrono::milliseconds>(exec_duration)
                                    .count();
                    wait_string = StringPrintf(" oneshot service took %f seconds in background",
                                            exec_duration_ms / 1000.0f);
                }
            } else {
                name = StringPrintf("Untracked pid %d", pid);
            }
        }
    
        // 检查子进程是正常退出还是因为接收到信号而退出,并记录相应的日志。
        if (siginfo.si_code == CLD_EXITED) {
            LOG(INFO) << name << " exited with status " << siginfo.si_status << wait_string;
        } else {
            LOG(INFO) << name << " received signal " << siginfo.si_status << wait_string;
        }
    
        // 如果没有找到与 PID 对应的服务,将记录一条日志,并返回 PID。
        if (!service) {
            LOG(INFO) << name << " did not have an associated service entry and will not be reaped";
            return pid;
        }
    
        // 如果找到了服务,将调用服务的 Reap 方法来处理服务的结束。
        service->Reap(siginfo);
    
        // 如果服务是临时的,将从服务列表中移除服务。
        if (service->flags() & SVC_TEMPORARY) {
            ServiceList::GetInstance().RemoveService(*service);
        }
    
        // 返回 PID
        return pid;
    }

    ReapOneProcess 函数的主要用途是处理子进程的结束,收集子进程的退出状态,防止子进程变成僵尸进程,并处理与子进程相关的服务。

八、属性服务

我们在开发和调试过程中看到通过 property_set 可以轻松设置系统属性,那干嘛这里还要启动一个属性服务呢?这里其实涉及到一些权限的问题,不是所有进程都可以随意修改任何的系统属性,Android 将属性的设置统一交由 init 进程管理,其他进程不能直接修改属性,而只能通知 init 进程来修改,而在这过程中,init 进程可以进行权限控制,我们来看看具体的流程是什么:

  1. PropertyInit

    PropertyInit 用于初始化 Android 系统的属性服务。这个服务用于管理系统的属性,这些属性是一些键值对,可以被系统的各个部分用来配置和通信。

    源码路径:/system/core/init/property_service.cpp

    void PropertyInit() {
        // 设置一个 SELinux 回调函数 PropertyAuditCallback,用于处理 SELinux 的审计事件。
        selinux_callback cb;
        cb.func_audit = PropertyAuditCallback;
        selinux_set_callback(SELINUX_CB_AUDIT, cb);
    
        // 创建一个名为 /dev/__properties__ 的目录,这个目录用于存储系统属性的信息。
        mkdir("/dev/__properties__", S_IRWXU | S_IXGRP | S_IXOTH);
        // 调用 CreateSerializedPropertyInfo 函数来创建序列化的属性信息。
        CreateSerializedPropertyInfo();
        // 调用 __system_property_area_init 函数来初始化属性区域。如果初始化失败,它将记录一条致命错误日志并退出。
        if (__system_property_area_init()) {
            LOG(FATAL) << "Failed to initialize property area";
        }
        // 尝试从默认路径加载序列化的属性信息文件。如果加载失败,它将记录一条致命错误日志并退出。
        if (!property_info_area.LoadDefaultPath()) {
            LOG(FATAL) << "Failed to load serialized property info file";
        }
    
        // 处理内核设备树(DT)和内核命令行中的参数。如果这两种方式都提供了参数,设备树中的属性将优先于命令行中的属性。
        ProcessKernelDt();
        ProcessKernelCmdline();
        // 处理启动配置。
        ProcessBootconfig();
    
        // 将内核变量传播到 init 使用的内部变量以及当前需要的属性。
        ExportKernelBootProps();
    
        // 加载启动默认属性。
        PropertyLoadBootDefaults();
    }

    PropertyInit 函数的主要用途是初始化系统属性服务,以便系统的各个部分可以使用系统属性进行配置和通信。

  2. StartPropertyService

    StartPropertyService 函数用于启动属性服务。

    源码路径:/system/core/init/property_service.cpp

    void StartPropertyService(int* epoll_socket) {
        // 调用 InitPropertySet 函数来初始化一个名为 ro.property_service.version 的系统属性,其值为"2"。
        InitPropertySet("ro.property_service.version", "2");
    
        // 创建一个 UNIX 域套接字对,用于 property_service 和 init 之间的通信。如果套接字对的创建失败,它将记录一条致命错误日志并退出。
        int sockets[2];
        if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sockets) != 0) {
            PLOG(FATAL) << "Failed to socketpair() between property_service and init";
        }
        // 将套接字对的一个端点赋值给 epoll_socket 和 from_init_socket,另一个端点赋值给 init_socket。
        *epoll_socket = from_init_socket = sockets[0];
        init_socket = sockets[1];
        // 调用 StartSendingMessages 函数来开始发送消息。
        StartSendingMessages();
    
        // 创建一个名为 PROP_SERVICE_NAME 的套接字,用于处理属性设置请求。如果套接字的创建失败,它将记录一条致命错误日志并退出。
        if (auto result = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
                                    false, false, 0666, 0,
                                    0, {});
            result.ok()) {
            property_set_fd = *result;
        } else {
            LOG(FATAL) << "start_property_service socket creation failed: " << result.error();
        }
    
        // 调用 listen 函数来开始监听属性设置请求。
        listen(property_set_fd, 8);
    
        // 创建一个新的线程来运行 PropertyServiceThread 函数。这个函数用于处理属性设置请求。
        auto new_thread = std::thread{PropertyServiceThread};
        property_service_thread.swap(new_thread);
    
        // 检查 ro.property_service.async_persist_writes 系统属性的值。
        auto async_persist_writes =
                android::base::GetBoolProperty("ro.property_service.async_persist_writes", false);
        
        // 如果 async_persist_writes 为 true,将创建一个 PersistWriteThread 对象来异步写入持久化的属性。
        if (async_persist_writes) {
            persist_write_thread = std::make_unique<PersistWriteThread>();
        }
    }

    StartPropertyService 函数的主要用途是启动属性服务,以便系统的各个部分可以使用系统属性进行配置和通信。

  3. handle_property_set_fd

    handle_property_set_fd 函数用于处理来自客户端的属性设置请求。这个函数通过接收和处理来自 UNIX 域套接字的消息来完成这个任务。

    源码路径:/system/core/init/property_service.cpp

    static void handle_property_set_fd() {
        static constexpr uint32_t kDefaultSocketTimeout = 2000;
    
        // 调用 accept4 函数来接收新的连接请求。如果接收失败,函数会直接返回。
        int s = accept4(property_set_fd, nullptr, nullptr, SOCK_CLOEXEC);
        if (s == -1) {
            return;
        }
    
        // 获取连接的对端的凭据(包括用户ID,组ID和进程ID)。
        ucred cr;
        socklen_t cr_size = sizeof(cr);
        if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) {
            close(s);
            PLOG(ERROR) << "sys_prop: unable to get SO_PEERCRED";
            return;
        }
    
        // 创建一个 SocketConnection 对象,用于处理和这个连接相关的操作。
        SocketConnection socket(s, cr);
        uint32_t timeout_ms = kDefaultSocketTimeout;
    
        // 从套接字中读取一个 32 位的命令。如果读取失败,函数会发送一个错误码并返回。
        uint32_t cmd = 0;
        if (!socket.RecvUint32(&cmd, &timeout_ms)) {
            PLOG(ERROR) << "sys_prop: error while reading command from the socket";
            socket.SendUint32(PROP_ERROR_READ_CMD);
            return;
        }
    
        switch (cmd) {
        // 如果命令是 PROP_MSG_SETPROP,函数会从套接字中读取属性的名字和值,然后调用 HandlePropertySetNoSocket 函数来设置属性。
        case PROP_MSG_SETPROP: {
            char prop_name[PROP_NAME_MAX];
            char prop_value[PROP_VALUE_MAX];
    
            if (!socket.RecvChars(prop_name, PROP_NAME_MAX, &timeout_ms) ||
                !socket.RecvChars(prop_value, PROP_VALUE_MAX, &timeout_ms)) {
            PLOG(ERROR) << "sys_prop(PROP_MSG_SETPROP): error while reading name/value from the socket";
            return;
            }
    
            prop_name[PROP_NAME_MAX-1] = 0;
            prop_value[PROP_VALUE_MAX-1] = 0;
    
            std::string source_context;
            if (!socket.GetSourceContext(&source_context)) {
                PLOG(ERROR) << "Unable to set property '" << prop_name << "': getpeercon() failed";
                return;
            }
    
            const auto& cr = socket.cred();
            std::string error;
            auto result = HandlePropertySetNoSocket(prop_name, prop_value, source_context, cr, &error);
            if (result != PROP_SUCCESS) {
                LOG(ERROR) << "Unable to set property '" << prop_name << "' from uid:" << cr.uid
                        << " gid:" << cr.gid << " pid:" << cr.pid << ": " << error;
            }
    
            break;
        }
    
        // 如果命令是 PROP_MSG_SETPROP2,函数会从套接字中读取属性的名字和值,然后调用 HandlePropertySet 函数来设置属性。这个函数会处理异步属性设置的情况。
        case PROP_MSG_SETPROP2: {
            std::string name;
            std::string value;
            if (!socket.RecvString(&name, &timeout_ms) ||
                !socket.RecvString(&value, &timeout_ms)) {
            PLOG(ERROR) << "sys_prop(PROP_MSG_SETPROP2): error while reading name/value from the socket";
            socket.SendUint32(PROP_ERROR_READ_DATA);
            return;
            }
    
            std::string source_context;
            if (!socket.GetSourceContext(&source_context)) {
                PLOG(ERROR) << "Unable to set property '" << name << "': getpeercon() failed";
                socket.SendUint32(PROP_ERROR_PERMISSION_DENIED);
                return;
            }
    
            const auto& cr = socket.cred();
            std::string error;
            auto result = HandlePropertySet(name, value, source_context, cr, &socket, &error);
            if (!result) {
                return;
            }
            if (*result != PROP_SUCCESS) {
                LOG(ERROR) << "Unable to set property '" << name << "' from uid:" << cr.uid
                        << " gid:" << cr.gid << " pid:" << cr.pid << ": " << error;
            }
            socket.SendUint32(*result);
            break;
        }
    
        // 如果命令是其他值,函数会记录一个错误日志并发送一个错误码。
        default:
            LOG(ERROR) << "sys_prop: invalid command " << cmd;
            socket.SendUint32(PROP_ERROR_INVALID_CMD);
            break;
        }
    }

    handle_property_set_fd 函数的主要用途是处理属性设置请求,以便系统的各个部分可以使用系统属性进行配置和通信。

九、init.rc

当属性服务建立完成后,init 的自身功能基本就告一段落,接下来需要来启动其他的进程。但是 init 进程如何启动其他进程呢?其他进程都是一个二进制文件,我们可以直接通过 exec 的命令方式来启动,例如 ./system/bin/init second_stage,来启动 init 进程的第二阶段。但是 Android 系统有那么多的 Native 进程,如果都通过传 exec 在代码中一个个的来执行进程,那无疑是一个灾难性的设计。在这个基础上 Android 推出了一个 init.rc 的机制,即类似通过读取配置文件的方式,来启动不同的进程。接下来我们就来看看 init.rc 是如何工作的。

init.rc 是一个配置文件,内部由 Android 初始化语言编写(Android Init Language)编写的脚本。init.rc 在 Android 设备的目录:./init.rc。init.rc 主要包含五种类型语句:ActionCommandServiceOptionImport

  1. Action

    Action 表示了一组命令(commands)组成.动作包括一个触发器,决定了何时运行这个 Action。Action 通过触发器(trigger),即以 on 开头的语句来决定执行相应的 service 的时机,具体有如下时机:

    • on early-init:在初始化早期阶段触发
    • on init:在初始化阶段触发
    • on late-init:在初始化晚期阶段触发
    • on boot/charger:当系统启动/充电时触发
    • on property:<key>=<value>:当属性值满足条件时触发
  2. Command

    Command 是 Action 的命令列表中的命令,或者是 Service 中的选项 onrestart 的参数命令,命令将在所属事件发生时被一个个地执行。

    下面列举常用的命令:

    • class_start <service_class_name>:启动属于同一个 class 的所有服务
    • class_stop <service_class_name>:停止指定类的服务
    • start <service_name>:启动指定的服务,若已启动则跳过
    • stop <service_name>:停止正在运行的服务
    • setprop <name> <value>:设置属性值
    • mkdir <path>:创建指定目录
    • symlink <target> <sym_link>:创建连接到 <target><sym_link> 符号链接
    • write <path> <string>:向文件 path 中写入字符串
    • exec:fork 并执行,会阻塞 init 进程直到程序完毕
    • exprot <name> <name>:设定环境变量
    • loglevel <level>:设置 log 级别
    • hostname <name>:设置主机名
    • import <filename>:导入一个额外的 init 配置文件
  3. Service

    服务 Service,以 service 开头,由 init 进程启动,一般运行在 init 的一个子进程,所以启动 service 前需要判断对应的可执行文件是否存在。

    命令:service <name><pathname> [ <argument> ]* <option> <option>

    参数 含义
    <name> 表示此服务的名称
    <pathname> 此服务所在路径,因为是可执行文件,所以一定有存储路径。
    <argument> 启动服务所带的参数
    <option> 对此服务的约束选项

    init 生成的子进程,定义在 rc 文件,其中每一个 service 在启动时会通过 fork 方式生成子进程。

    例如:service servicemanager /system/bin/servicemanager 代表的是服务名为 servicemanager,服务执行的路径为 /system/bin/servicemanager

  4. Options

    Options 是 Service 的可选项,与 service 配合使用:

    • disabled:不随 class 自动启动,只有根据 service 名才启动。
    • oneshot:service 退出后不再重启。
    • user/group:设置执行服务的用户/用户组,默认都是 root。
    • class:设置所属的类名,当所属类启动/退出时,服务也启动/停止,默认为 default。
    • onrestart:当服务重启时执行相应命令。
    • socket:创建名为 /dev/socket/<name> 的 socket。
    • critical:在规定时间内该 service 不断重启,则系统会重启并进入恢复模式。

    default:意味着 disabled=false,oneshot=false,critical=false。

  5. Import

    用来导入其他的 rc 文件。

    命令:import <filename>

  6. init.rc 解析过程 - LoadBootScripts

    LoadBootScripts 的主要任务是加载启动脚本。

    源码路径:/system/core/init/init.cpp

    static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
        // 创建一个Parser对象,该对象用于解析启动脚本。
        Parser parser = CreateParser(action_manager, service_list);
    
        // 尝试获取名为"ro.boot.init_rc"的属性,该属性的值应该是一个启动脚本的路径。
        std::string bootscript = GetProperty("ro.boot.init_rc", "");
        // 如果该属性不存在或者为空,那么它会尝试解析一系列默认的启动脚本。
        if (bootscript.empty()) {
            parser.ParseConfig("/system/etc/init/hw/init.rc");
            if (!parser.ParseConfig("/system/etc/init")) {
                // 如果在尝试解析时失败,那么它会将该路径添加到 late_import_paths 列表中,这意味着这些路径将在稍后再次尝试解析。
                late_import_paths.emplace_back("/system/etc/init");
            }
            parser.ParseConfig("/system_ext/etc/init");
            if (!parser.ParseConfig("/vendor/etc/init")) {
                // 如果在尝试解析时失败,那么它会将该路径添加到 late_import_paths 列表中,这意味着这些路径将在稍后再次尝试解析。
                late_import_paths.emplace_back("/vendor/etc/init");
            }
            if (!parser.ParseConfig("/odm/etc/init")) {
                // 如果在尝试解析时失败,那么它会将该路径添加到 late_import_paths 列表中,这意味着这些路径将在稍后再次尝试解析。
                late_import_paths.emplace_back("/odm/etc/init");
            }
            if (!parser.ParseConfig("/product/etc/init")) {
                // 如果在尝试解析时失败,那么它会将该路径添加到 late_import_paths 列表中,这意味着这些路径将在稍后再次尝试解析。
                late_import_paths.emplace_back("/product/etc/init");
            }
        } else {
            // 如果"ro.boot.init_rc"属性存在并且不为空,那么它会尝试解析该属性指定的启动脚本。
            parser.ParseConfig(bootscript);
        }
    }

    总的来说,LoadBootScripts 函数的目的是尽可能地加载和解析所有可用的启动脚本。

  7. init.rc 解析过程 - CreateParser

    CreateParser 的主要任务是创建并配置一个Parser对象。它接受两个参数,一个是ActionManager类型的action_manager,另一个是ServiceList类型的service_list。

    源码路径:/system/core/init/init.cpp

    Parser CreateParser(ActionManager& action_manager, ServiceList& service_list) {
        // 创建一个 Parser 对象。
        Parser parser;
    
        // 解析启动脚本中的"service"部分。
        parser.AddSectionParser("service", std::make_unique<ServiceParser>(
                                                &service_list, GetSubcontext(), std::nullopt));
        // 解析启动脚本中的"on"部分。
        parser.AddSectionParser("on", std::make_unique<ActionParser>(&action_manager, GetSubcontext()));
        // 解析启动脚本中的"import"部分。
        parser.AddSectionParser("import", std::make_unique<ImportParser>(&parser));
    
        // 返回配置好的 Parser 对象。
        return parser;
    }

    总的来说,这个函数的目的是创建一个能够解析启动脚本中的"service"、"on"和"import"部分的Parser对象。

  8. init.rc 解析过程 - 执行 Action 动作

    按顺序把相关 Action 加入触发器队列,按顺序为 early-init -> init -> late-init。然后在循环中,执行所有触发器队列中 Action 带 Command 的执行函数。

    源码路径:/system/core/init/init.cpp

    am.QueueEventTrigger("early-init");
    am.QueueEventTrigger("init");
    am.QueueEventTrigger("late-init");
    
    ...
    
    while (true) {
            ...
    
            if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
                am.ExecuteOneCommand();
                
                ...
            }
    
            ...
    }
    
    ...
  9. init.rc 解析过程 - Zygote 启动

    Android 支持 64 位的编译,因此 zygote 本身也支持 32 位和 64 位。通过属性 ro.zygote 来控制不同版本的 zygote 进程启动。在 init.rc 的 import 段我们看到如下代码:

    import /system/etc/init/hw/init.${ro.zygote}.rc
    

    init.rc 位于 /system/core/rootdir/ 下。在这个路径下还包括三个关于 zygote 的 rc 文件。分别是 init.zygote32.rcinit.zygote64.rcinit.zygote64_32.rc,由硬件决定调用哪个文件。

    这里拿 64 位处理器为例,init.zygote64.rc 的代码如下所示:

    // 定义了一个名为"zygote"的服务,它使用/system/bin/app_process64程序,并传递了一些参数来启动zygote进程。
    service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
        // 将此服务归类为主服务。
        class main
        // 设置此服务的优先级为-20,这是最高优先级,意味着这个服务将优先于其他服务运行。
        priority -20
        // 设置此服务的用户和组为root,同时给予了读取进程信息和访问保留磁盘的权限。
        user root
        group root readproc reserved_disk
        // 创建了两个名为"zygote"和"usap_pool_primary"的socket,权限为660,所有者和组都是root和system。
        socket zygote stream 660 root system
        socket usap_pool_primary stream 660 root system
        // 定义了当服务重启时要执行的命令,包括执行一些命令、写入一些系统文件、重启一些服务等。
        onrestart exec_background - system system -- /system/bin/vdc volume abort_fuse
        onrestart write /sys/power/state on
        # NOTE: If the wakelock name here is changed, then also
        # update it in SystemSuspend.cpp
        onrestart write /sys/power/wake_lock zygote_kwl
        onrestart restart audioserver
        onrestart restart cameraserver
        onrestart restart media
        onrestart restart media.tuner
        onrestart restart netd
        onrestart restart wificond
        // 设置了任务的性能配置文件。
        task_profiles ProcessCapacityHigh MaxPerformance
        // 设置了一个名为"zygote-fatal"的目标,当zygote进程在定义的窗口时间内崩溃时,将会触发这个目标。
        critical window=${zygote.critical_window.minute:-off} target=zygote-fatal
    

    总的来说,这个脚本定义了 zygote 服务的行为和属性,包括它如何启动,它的权限,它的优先级,以及当它重启时应该执行的操作。

十、总结

init 进程启动过程分为三个阶段:

  • 第一阶段:主要工作是挂载分区,创建设备节点和一些关键目录,初始化日志输出系统,并启用 SELinux 安全策略。

  • 第二阶段:主要工作是初始化属性系统,解析 SELinux 的策略文件,处理子进程终止信号,启动系统属性服务。每一项都是关键的。如果说第一阶段是为属性系统和 SELinux 做准备,那么第二阶段就是真正去实现这些功能。

  • 第三阶段:主要是解析 init.rc 文件来启动其他进程,并进入一个无限循环,进行子进程的实时监控。

参考

ArkUI - 循环控制(ForEach)

一、语法

ForEach(
  arr: Array,
  (item: any, index?: number) => {

  }, 
  keyGenerator?: (item: any, index?: number): string => {

  }
)
  1. arr: Array

    要遍历的数据数组

    比如我们有一组2024春节档新片票房的数据:

    private items = [
      { name: '热辣滚烫', image: '1.', box_office: '13.41亿' },
      { name: '飞驰人生2', image: '', box_office: '11.90亿' },
      { name: '熊出没·逆转时空', image: '', box_office: '7.26亿' },
      { name: '第二十条', image: '', box_office: '5.50亿' },
      { name: '我们一起摇太阳', image: '', box_office: '5835.6万' }
    ]
    

    这个 item 就可以作为第一个参数 arr 传进来,ForEach 就会去遍历这个数组,拿到里面的每一个电影数据。

  2. (item: any, index?: number) => { }

    页面组件生成的函数

    item 是数组中的元素,由于数组中元素的类型是不确定的,所以类型是 Any;
    index 是数组的角标,为可选参数。

  3. keyGenerator?: (item: any, index?: number): string => { }

    生成函数,为数组每一项生成一个唯一标示,组件是否重新渲染的判断标准

    假如我们以电影名称作为唯一标示,现在向这个数组中插入一条新的数据,在遍历的过程中,发现前面的N条数据的标示没有发生变化,因为名字没变,那这时候就不需要去重复渲染,只有最后插入的这条新数据,它的名字跟之前比是不存在的,是新的,这时候只需把这条新的渲染出来就可以了。这样就减少了不必要的渲染,提高了整个页面渲染效率。

二、示例代码

  1. 首先自定义一个类 Item

    class Item {
      name: string
      image: ResourceStr
      box_office: string
    
      constructor(name: string, image: ResourceStr, box_office: string) {
        this.name = name
        this.image = image
        this.box_office = box_office
      }
    }
    
  2. 在结构体 Index 里定义 items 成员变量,也就是电影的数组

    private items: Array<Item> = [
      { name: '热辣滚烫', image: $r('app.media.1'), box_office: '13.41亿' },
      { name: '飞驰人生2', image: $r('app.media.2'), box_office: '11.90亿' },
      { name: '熊出没·逆转时空', image: $r('app.media.3'), box_office: '7.26亿' },
      { name: '第二十条', image: $r('app.media.4'), box_office: '5.50亿' },
      { name: '我们一起摇太阳', image: $r('app.media.5'), box_office: '5835.6万' }
    ]
    
  3. 使用 ForEach 循环遍历数组

    build() {
      Column({ space: 8 }) {
        ForEach(
          this.items,
          (item: Item) => {
            Row({ space: 8 }) {
              Image(item.image)
                .width(157)
                .height(220)
              Column() {
                Text(item.name)
                  .fontSize(20)
                  .fontWeight(FontWeight.Bold)
                Text(item.box_office)
                  .fontSize(18)
              }
              .height('100%')
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .height(220)
          }
        )
      }
      .width('100%')
      .height('100%')
      .padding(8)
    }
    
  4. 效果展示
    循环控制

Stage 模型 - 应用配置文件

如图所示:

Stage 模型应用配置文件主要有两类:

  • 全局配置文件。放在 AppScope 目录下,app.json5。用来配置应用全局的信息。
  • 模块配置文件,放在每个模块里,module.json5。用来配置模块的信息。

一、全局配置文件

示例:

{
  "app": {
    "bundleName": "com.tyhoo.ohos.myapplication",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:app_icon",
    "label": "$string:app_name"
  }
}
  1. bundleName

    • 应用的唯一标识(也叫包名)。
    • 命名格式:在 HarmonyOS 当中,要求使用域名倒置的方式去定义。
  2. versionCode、versionName

    • 版本。
    • versionCode 是数字格式的版本,versionName 是字符串格式的版本。
  3. icon

    • 应用图标。
    • 没有直接指定图片路径,而是使用 $ 符号的方式。读取的就是 /AppScope/resources/base/media 目录下的图片。
  4. label

    • 应用描述字符(也叫应用名称)。
    • 没有直接指定字符值,而是使用 $ 符号的方式。读取的就是 /AppScope/resources/base/element 目录下的 string.json。

二、模块配置文件

示例:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ],
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ts",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ]
  }
}
  1. requestPermissions

    • 权限申请。
  2. name、type

    • 当前模块的名称和类型。
    • 模块分成两大类:Ability 和 Library。Ability 又分为 entry 和 feature。Library 又分为 shared。
  3. description

    • 当前模块的描述。
    • 使用 $string 去读取,读取的是 /当前模块/src/main/resources/base/element 目录下的 string.json。
  4. mainElement

    • 当前模块的入口。
    • 每个模块将来编译之后都是一个 HAP 文件,都是可以独立运行的。在独立运行时,先创建 AbilityStage(应用组件的舞台),在这个舞台上面创建一个 Ability。事实上,在一个模块的内部,可以创建多个 Ability。如果这个应用内部有多个 Ability,其实默认只能启动一个,默认启动的这个叫EntryAbility(入口 Ability),存放在 /当前模块/src/main/ets/entryability 目录下的 EntryAbility.ts。
  5. deviceTypes

    • 设备的类型。
    • 在一个项目下,一个应用内部,有多个模块,每个模块将来都可以打包成一个 HAP 文件,可以给不同的模块设置不同的设备类型。
  6. deliveryWithInstall

    • 是否支持安装。
    • 如果为 true,是要跟随整个 APP 一起安装的。
    • 如果为 false,可安装可不安装。
  7. pages

    • 当前这个模块包含的所有页面。
    • 使用 $profile 去读取,读取的是 /当前模块/src/main/resources/base/profile 目录下的 main_pages.json。
  8. abilities

    • 当前模块包含的所有 Ability。
    • 一个模块下可以创建多个 Ability,都需要在 abilities 中配置。

注:更详细的配置信息可以到 官方文档 - 应用配置文件概述(Stage模型)查看。

Kotlin 使用 Channel 实现发布/订阅模式

一、角色设定

  1. 发布者:MainActivity
  2. 订阅者:FirstActivity、SecondActivity、ThirdActivity

二、代码实现

  1. 定义全局 Channel

     object MessageChannel {
         val channel = Channel<Int>()
     }

    注:MessageChannel.channel 是一个 Channel 对象,用于在不同页面之间传递整数类型的计数值。它充当了一个中介,允许一个页面发送计数值,而其他页面则可以接收这些计数值。

  2. 发布者 MainActivity

     class MainActivity : AppCompatActivity() {
    
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             setContentView(R.layout.activity_main)
    
             startTimer()
             startReceiver()
    
             // 点击跳转到 FirstActivity
             findViewById<Button>(R.id.btn_first).setOnClickListener {
                 val intent = Intent(this, FirstActivity::class.java)
                 startActivity(intent)
             }
    
             // 点击跳转到 SecondActivity
             findViewById<Button>(R.id.btn_second).setOnClickListener {
                 val intent = Intent(this, SecondActivity::class.java)
                 startActivity(intent)
             }
    
             // 点击跳转到 ThirdActivity
             findViewById<Button>(R.id.btn_third).setOnClickListener {
                 val intent = Intent(this, ThirdActivity::class.java)
                 startActivity(intent)
             }
         }
    
         private fun startTimer() {
             lifecycleScope.launch {
                 var count = 0
                 while (true) {
                     MessageChannel.channel.send(count)
                     Log.d("Tyhoo", "主页面 send: $count")
                     count++
                     delay(1000)
                 }
             }
         }
    
         private fun startReceiver() {
             lifecycleScope.launch {
                 while (true) {
                     val count = MessageChannel.channel.receive()
                     Log.d("Tyhoo", "主页面 received: $count")
                 }
             }
         }
    
         override fun onDestroy() {
             super.onDestroy()
             MessageChannel.channel.cancel()
         }
     }

    注:在 MainActivity 的 startTimer 函数中,使用协程和循环不断地发送计数值到 MessageChannel.channel。这样做的效果是,每隔一秒钟,会将一个递增的计数值发送到通道中。

    在 MainActivity 的 startReceiver 函数中,使用协程和循环不断地接收 MessageChannel.channel 中的计数值。这样做的效果是,一旦有计数值被发送到通道中,该函数就会立即接收并打印该计数值。

  3. 订阅者 FirstActivity(SecondActivity、ThirdActivity 同理)

     class FirstActivity : AppCompatActivity() {
    
         private var job: Job? = null
    
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             job = startReceivingCount()
         }
    
         private fun startReceivingCount(): Job {
             return lifecycleScope.launch {
                 for (count in MessageChannel.channel) {
                     Log.d("Tyhoo", "First 页面 received: $count")
                 }
             }
         }
    
         override fun onDestroy() {
             super.onDestroy()
             job?.cancel()
         }
     }

    注:在 FirstActivity 的 startReceivingCount 函数中,使用协程和循环不断地接收 MessageChannel.channel 中的计数值。这样做的效果是,在该页面中可以实时接收来自 MainActivity 发送的计数值,并打印在日志中。

三、总结

通过使用 Channel,实现了一种简单的发布/订阅模式,其中 MainActivity 充当了发布者,而 FirstActivity(SecondActivity、ThirdActivity ) 充当了订阅者。计数值通过 Channel 在两个页面之间进行传递,并且能够实时更新响应。这种方式提供了一种灵活的通信机制,可以在不同的组件或模块之间进行数据传递事件通知

属性动画

在属性动画出现之前,Android 系统提供的动画只有帧动画和 View 动画。View 动画我们都了解,它提供了 AlphaAnimation、RotateAnimation、TranslateAnimation、ScaleAnimation 这4种动画方式,并提供了 AnimationSet 动画集合来混合使用多种动画。随着属性动画的推出,View 动画不再风光。

相比属性动画,View 动画一个非常大的缺陷突显,其不具有交互性。当某个元素发生 View 动画后,其响应事件的位置依然在动画进行前的地方,所以 View 动画只能做普通的动画效果,要避免涉及交互操作。但是它的优点也非常明显:效率比较高,使用也方便。由于之前已有的动画框架 Animation 存在一些局限性,也就是动画改变的只是显示,但 View 的位置没有发生变化,View 移动后并不能响应事件,所以谷歌推出了新的动画框架,帮助开发者实现更加丰富的动画效果。在 Animator 框架中使用最多的就是 AnimatorSet 和 ObjectAnimator,配合使用 ObjectAnimator 进行更精细化的控制,控制一个对象和一个属性值,而使用多个 ObjectAnimator 组合到 AnimatorSet 形成一个动画。属性动画通过调用属性 get、set 方法来真实地控制一个 View 的属性值,因此,强大的属性动画框架基本可以实现所有的动画效果。

一、ObjectAnimator

ObjectAnimator 是属性动画最重要的类,创建一个 ObjectAnimator 只需通过其静态工厂类直接返还一个 ObjectAnimator 对象。参数包括一个对象和对象的属性名字,但这个属性必须有 get 和 set 方法,其内部会通过 Java 反射机制来调用 set 方法修改对象的属性值。下面看看平移动画是如何实现的,代码如下所示:

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <View
        android:id="@+id/test_view"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val testView = findViewById<View>(R.id.test_view)
        val objectAnimator = ObjectAnimator.ofFloat(testView, "translationX", 200F)
        objectAnimator.setDuration(3000)
        objectAnimator.start()
    }
}

运行程序,效果如图1所示:

图1

通过 ObjectAnimator 的静态方法,创建一个 ObjectAnimator 对象,查看 ObjectAnimator 的静态方法 ofFloat(),源码如下所示:

public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
    ObjectAnimator anim = new ObjectAnimator(target, propertyName);
    anim.setFloatValues(values);
    return anim;
}

从源码可以看出第一个参数是要操作的 Object;第二个参数是要操作的属性;最后一个参数是一个可变的 float 类型数组,需要传进去该属性变化的取值过程,这里设置了一个参数,变化到200。与 View 动画一样,也可以给属性动画设置显示时长、插值器等属性。下面就是一些常用的可以直接使用的属性动画的属性值。

  • translationX 和 translationY:用来沿着 X 轴或者 Y 轴进行平移。
  • rotation、rotationX、rotationY:用来围绕 View 的支点进行旋转。
  • PrivotX 和 PrivotY:控制 View 对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认该支点位置就是 View 对象的中心点。
  • alpha:透明度,默认是1(不透明),0代表完全透明。
  • x 和 y:描述 View 对象在其容器中的最终位置。

需要注意的是,在使用 ObjectAnimator 的时候,要操作的属性必须要有 get 和 set 方法,不然 ObjectAnimator 就无法生效。如果一个属性没有 get、set 方法,也可以通过自定义一个属性类或包装类来间接地给这个属性增加 get 和 set 方法。现在来看看如何通过包装类的方法给一个属性增加 get 和 set 方法,代码如下所示:

class MyView(private val view: View) {

    fun getWidth(): Int {
        return view.layoutParams.width
    }

    fun setWidth(width: Int) {
        view.layoutParams.width = width
        view.requestLayout()
    }
}

使用时只需要操作包类就可以调用 get、set 方法了:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val testView = findViewById<View>(R.id.test_view)
        val myView = MyView(testView)
        ObjectAnimator.ofInt(myView, "width", 500).setDuration(3000).start()
    }
}

运行程序,效果如图2所示:

图2

二、ValueAnimator

ValueAnimator 不提供任何动画效果,它更像一个数值发生器,用来产生有一定规律的数字,从而让调用者控制动画的实现过程。通常情况下,在 ValueAnimator 的 AnimatorUpdateListener 中监听数值的变化,从而完成动画的变换,代码如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val testView = findViewById<View>(R.id.test_view)
        val valueAnimator = ValueAnimator.ofFloat(0F, 100F)
        valueAnimator.setTarget(testView)
        valueAnimator.setDuration(3000).start()
        valueAnimator.addUpdateListener { animation ->
            val animatedValue = animation.animatedValue
        }
    }
}

三、动画的监听

完整的动画具有 start、Repeat、End、Cancel 这4个过程,代码如下所示:

val animator = ObjectAnimator.ofFloat(testView, "alpha", 1.5F)
animator.addListener(object : AnimatorListener {
    override fun onAnimationStart(animation: Animator) {
    }

    override fun onAnimationEnd(animation: Animator) {
    }

    override fun onAnimationCancel(animation: Animator) {
    }

    override fun onAnimationRepeat(animation: Animator) {
    }
})

大部分时候我们只关心 onAnimationEnd 事件,Android 也提供了 AnimatorListenterAdaper 来让我们选择必要的事件进行监听。

val animator = ObjectAnimator.ofFloat(testView, "alpha", 1.5F)
animator.addListener(object : AnimatorListenerAdapter() {
    override fun onAnimationEnd(animation: Animator) {
        super.onAnimationEnd(animation)
    }
})

四、组合动画(AnimatorSet)

AnimatorSet 类提供了一个 play() 方法,如果我们向这个方法中传入一个 Animator 对象(ValueAnimator 或 ObjectAnimator),将会返回一个 AnimatorSet.Builder 的实例。AnimatorSet 的 play() 方法源码如下所示:

public Builder play(Animator anim) {
    if (anim != null) {
        return new Builder(anim);
    }
    return null;
}

很明显,在 play() 方法中创建了一个 AnimatorSet.Builder 类,这个 Builder 类是 AnimatorSet 的内部类。我们来看看这个 Builder 类中有什么,代码如下所示:

public class Builder {

    private Node mCurrentNode;

    Builder(Animator anim) {
        mDependencyDirty = true;
        mCurrentNode = getNodeForAnimation(anim);
    }

    public Builder with(Animator anim) {
        Node node = getNodeForAnimation(anim);
        mCurrentNode.addSibling(node);
        return this;
    }

    public Builder before(Animator anim) {
        Node node = getNodeForAnimation(anim);
        mCurrentNode.addChild(node);
        return this;
    }

    public Builder after(Animator anim) {
        Node node = getNodeForAnimation(anim);
        mCurrentNode.addParent(node);
        return this;
    }

    public Builder after(long delay) {
        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
        anim.setDuration(delay);
        after(anim);
        return this;
    }
}

从源码中可以看出,Builder 类采用了建造者模式,每次调用方法时都返回 Builder 自身用于继续构建。AnimatorSet.Builder 中包括以下4个方法:

  • with(Animator anim):将现有动画和传入的动画同时执行。
  • before(Animator anim):将现有动画插入到传入的动画之前执行。
  • after(Animator anim):将现有动画插入到传入的动画之后执行。
  • after(long delay):将现有动画延迟指定毫秒后执行。

AnimatorSet 正是通过这几种方法来控制动画播放顺序的。这里再举一个例子,代码如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val testView = findViewById<View>(R.id.test_view)
        val animator1 = ObjectAnimator.ofFloat(testView, "translationX", 0.0F, 200.0F, 0F)
        val animator2 = ObjectAnimator.ofFloat(testView, "scaleX", 1.0F, 2.0F)
        val animator3 = ObjectAnimator.ofFloat(testView, "rotationX", 0.0F, 90.0F, 0.0F)
        val set = AnimatorSet()
        set.setDuration(3000)
        set.play(animator1).with(animator2).after(animator3)
        set.start()
    }
}

首先我们创建3个 ObjectAnimator,分别是 animator1、animator2 和 animator3,然后创建 AnimatorSet。在这里先执行 animator3,然后同时执行 animator1 和 animator2(也可以调用 set.playTogether(animator1,animator2) 来使这两种动画同时执行)。

运行程序,效果如图3所示:

图3

五、组合动画(PropertyValuesHolder)

除了上面的 AnimatorSet 类,还可以使用 PropertyValuesHolder 类来实现组合动画。不过这个组合动画就没有上面的丰富了,使用 PropertyValuesHolder 类只能是多个动画一起执行。当然我们得结合 ObjectAnimator.ofPropertyValuesHolder(Object target,PropertyValuesHolder…values) 方法来使用。其第一个参数是动画的目标对象;之后的参数是 PropertyValuesHolder 类的实例,可以有多个这样的实例。具体代码如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val testView = findViewById<View>(R.id.test_view)
        val valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1.0F, 1.5F)
        val valuesHolder2 = PropertyValuesHolder.ofFloat("rotationX", 0.0F, 90.0F, 0.0F)
        val valuesHolder3 = PropertyValuesHolder.ofFloat("alpha", 1.0F, 0.3F, 1.0F)
        val objectAnimator = ObjectAnimator.ofPropertyValuesHolder(
            testView,
            valuesHolder1,
            valuesHolder2,
            valuesHolder3
        )
        objectAnimator.setDuration(3000).start()
    }
}

运行程序,效果如图4所示:

图4

六、在 xml 中使用属性动画

和 View 动画一样,属性动画也可以直接写在 xml 中。在 res 文件中新建 animator 文件夹,在里面新建一个 scale.xml,其内容如下所示:

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:duration="3000"
    android:propertyName="scaleX"
    android:valueFrom="1.0"
    android:valueTo="2.0"
    android:valueType="floatType" />

在程序中引用 xml 定义的属性动画也很简单,代码如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val testView = findViewById<View>(R.id.test_view)
        val animator = AnimatorInflater.loadAnimator(this, R.animator.scale)
        animator.setTarget(testView)
        animator.start()
    }
}

运行程序,效果如图5所示:

图5

系统启动流程分析 —— Zygote 进程启动过程

本文基于 Android 14.0.0_r2 的系统启动流程分析。

一、概述

Zygote 是 Android 系统中的一个核心进程,它在系统启动时被初始化。Zygote 的主要任务是加载系统的核心类库(如 Java 核心库和 Android 核心库),然后进入一个循环,等待请求来创建新的 Android 应用程序进程。

当一个新的 Android 应用程序需要启动时,Zygote 会 fork 出一个新的进程,这个新的进程继承了 Zygote 的内存空间,包括已经预加载的类库。这种方式可以大大提高新进程的启动速度,因为不需要再次加载这些类库。

Zygote 进程是所有 Android 应用程序进程的父进程,它的启动和初始化对于 Android 系统的运行至关重要。

二、源码分析

init.rc 文件中会执行 class_start main 来启动 Zygote,源代码如下:

路径:/system/core/rootdir/init.rc

on nonencrypted
    class_start main
    class_start late_start

这个 main 就是 Zygote,可以通过 init.zygote64.rc 来查看,源代码如下:

路径:/system/core/rootdir/init.zygote64.rc

// 定义了一个名为 zygote 的服务,它运行的是 /system/bin/app_process64 可执行文件。
// “-Xzygote” 参数指定了这是一个 Zygote 进程,用于启动 Android 应用进程和系统服务。
// “--zygote” 标志表明这是一个 Zygote 服务实例。
// “--start-system-server” 意味着在 Zygote 启动时同时启动系统服务。
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    // 指定服务所属的主要控制类,即在 init 进程中优先启动。
    class main

    // 设置服务启动的优先级为-20(数值越低,优先级越高)。
    priority -20

    // 指定服务运行时的用户和组,这里是 root 用户,以及包含 root 组和其他两个附加权限组 readproc 和 reserved_disk。
    user root
    group root readproc reserved_disk

    // 创建两个命名 socket。
    // zygote socket 用于与系统进行通信,以便请求创建新的应用程序进程。
    // usap_pool_primary 用途可能与进程间通信或资源池管理有关。
    socket zygote stream 660 root system
    socket usap_pool_primary stream 660 root system

    // 定义了一系列在 zygote 服务重启时执行的操作。
    onrestart exec_background - system system -- /system/bin/vdc volume abort_fuse
    onrestart write /sys/power/state on
    onrestart write /sys/power/wake_lock zygote_kwl
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart media.tuner
    onrestart restart netd
    onrestart restart wificond

    // 指定 zygote 任务应具有高处理能力和最大性能配置文件。
    task_profiles ProcessCapacityHigh MaxPerformance

    // 设置 zygote 服务的关键窗口时间,在此期间如果服务未成功启动,则视为致命错误。
    critical window=${zygote.critical_window.minute:-off} target=zygote-fatal

可以看到 audioserver、cameraserver、media、netd、wificond 这些进程都隶属于 Zygote 进程中,那就代表着:

  • 如果 Zygote died,这些进程将一起 died。
  • 如果这些进程 died,并不会影响 Zygote died。

如果 Zygote died 将会捕获到进程异常信号,将 Zygote 进程进行重启,Zygote main 入口位置:/frameworks/base/cmds/app_process/app_main.cpp

  1. app_main.cpp 源码分析

    路径:/frameworks/base/cmds/app_process/app_main.cpp
    
    int main(int argc, char* const argv[])
    {
        ...
    
        AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
        
        ...
    
        // 如果 zygote 为 true 则代表即将创建该进程。
        bool zygote = false;
        // 如果 startSystemServer 为 true 则代表创建 zygote 时也会创建 SystemServer。
        bool startSystemServer = false;
        // 系统正常启动都会将这两个 bool 默认给到 true,因为 rc 启动 main 后携带了--zygote 和 --start-system-server 两个参数。
        bool application = false;
        String8 niceName;
        String8 className;
    
        ++i;
        while (i < argc) {
            const char* arg = argv[i++];
            if (strcmp(arg, "--zygote") == 0) { // zygote 将为 true,名称就叫 zygote。
                zygote = true;
                niceName = ZYGOTE_NICE_NAME;
            } else if (strcmp(arg, "--start-system-server") == 0) { // startSystemServer 将为 true。
                startSystemServer = true;
            } else if (strcmp(arg, "--application") == 0) {
                application = true;
            } else if (strncmp(arg, "--nice-name=", 12) == 0) {
                niceName.setTo(arg + 12);
            } else if (strncmp(arg, "--", 2) != 0) {
                className.setTo(arg);
                break;
            } else {
                --i;
                break;
            }
        }
    
        Vector<String8> args;
        if (!className.isEmpty()) {
            ...
        } else {
            // 进入创建 zygote 模式。
            // 创建 /data/dalvik-cache,为后续会创建 Dalvik 虚拟机做准备。
            maybeCreateDalvikCache();
    
            if (startSystemServer) {
                args.add(String8("start-system-server"));
            }
    
            char prop[PROP_VALUE_MAX];
            
            ...
    
            // 将所有剩余参数传递给 args,例如 application 或 tool 或 start-system-server 或 abi。
            // 这些启动参数将会传递到其他进程中,后续取出参数决定是否启动 systemServer 等操作。
            for (; i < argc; ++i) {
                args.add(String8(argv[i]));
            }
        }
    
        if (!niceName.isEmpty()) {
            runtime.setArgv0(niceName.string(), true);
        }
    
        // zygote 为真,将创建 zygote,该 args 启动参数会包含 start-system-server。
        // 调用 runtime(AppRuntime) 的 start 来启动 zygote,将 args 传入,因为 args 包含了启动 SystemServer 的标志。
        if (zygote) {
            runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
        } else if (!className.isEmpty()) {
            runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
        } else {
            fprintf(stderr, "Error: no class name or --zygote supplied.\n");
            app_usage();
            LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
        }
    }

    以上代码就是启动 Zygote 和将 start-system-server 放入启动参数,后续会读取参数启动 SystemServer,继续分析一下 runtime.startcom.android.internal.os.ZygoteInit 进程,位于 /frameworks/base/core/jni/AndroidRuntime.cpp

  2. AndroidRuntime.cpp 源码分析

    路径:/frameworks/base/core/jni/AndroidRuntime.cpp
    
    // Vector<String8>& options 就是包含了 start-system-server 的启动参数,通过 app_main.cpp 传递过来的。
    void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
    {
        ALOGD(">>>>>> START %s uid %d <<<<<<\n",
                className != NULL ? className : "(unknown)", getuid());
    
        // 默认会启动 SystemServer。
        static const String8 startSystemServer("start-system-server");
        // 是否私有,如果 SystemServer 会被创建时,将会设置为私有。
        bool primary_zygote = false;
    
        for (size_t i = 0; i < options.size(); ++i) {
            // options 就是传递过来的 args,默认是包含了 start-system-server。
            if (options[i] == startSystemServer) {
                primary_zygote = true;
            
            ...
        }
    
        // 获取环境变量,这里第一次执行时默认为空,所以 rootDir 不存在。
        const char* rootDir = getenv("ANDROID_ROOT");
        if (rootDir == NULL) {
            // 将直接拿到 /system 作为 rootDir 并设置环境变量。
            rootDir = "/system";
            if (!hasDir("/system")) {
                LOG_FATAL("No root directory specified, and /system does not exist.");
                return;
            }
            setenv("ANDROID_ROOT", rootDir, 1);
        }
    
        ...
    
        // 这里就开始启动虚拟机了。
        // JNI 功能初始化。
        JniInvocation jni_invocation;
        jni_invocation.Init(NULL);
        JNIEnv* env;
        // 创建 Dalvik 虚拟机(这里 --> DVM == JavaVM)。
        if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
            return;
        }
        onVmCreated(env);
    
        // 调用 startReg 函数用来为 DVM 注册 JNI。
        if (startReg(env) < 0) {
            ALOGE("Unable to register all android natives\n");
            return;
        }
    
        jclass stringClass;
        jobjectArray strArray;
        jstring classNameStr;
    
        // 通过反射拿到 String 类型。
        stringClass = env->FindClass("java/lang/String");
        assert(stringClass != NULL);
        // options 就是 app_main.cpp 传递过来的 args,包含了 start-system-server。
        // 将 options 转换为 array list 对象。
        strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
        assert(strArray != NULL);
        // 从 app_main.cpp 的 main 函数得知 className 为 com.android.internal.os.ZygoteInit。
        classNameStr = env->NewStringUTF(className);
        assert(classNameStr != NULL);
        // 将数据转换给 java 类型的 array 数组。
        env->SetObjectArrayElement(strArray, 0, classNameStr);
    
        for (size_t i = 0; i < options.size(); ++i) {
            jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
            assert(optionsStr != NULL);
            env->SetObjectArrayElement(strArray, i + 1, optionsStr);
        }
    
        // 启动 com.android.internal.os.ZygoteInit,该线程成为 JVM 的主进程,在 VM 退出之前不会返回。
        char* slashClassName = toSlashClassName(className != NULL ? className : "");
        jclass startClass = env->FindClass(slashClassName);
        if (startClass == NULL) {
            ...
        } else {
            // 通过反射的方式,找到 ZygoteInit 的 main 函数。
            // 若获取到内容则执行 else。
            jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
                "([Ljava/lang/String;)V");
            if (startMeth == NULL) {
                ALOGE("JavaVM unable to find main() in '%s'\n", className);
            } else {
                // 通过 JNI 调用 ZygoteInit 的 main 函数,将 args(strArray) 传递到 java 层。
                // 因为 ZygoteInit 的 main 函数是 Java 编写的,因此需要通过 JNI 调用。
                // 所以这里继续跟到 java 层面:ZygoteInit.java。
                env->CallStaticVoidMethod(startClass, startMeth, strArray);
    
                ...
            }
        }
        // 若执行到这里,则会结束 Zygote 创建,关闭 JVM。
        free(slashClassName);
    
        ALOGD("Shutting down VM\n");
        if (mJavaVM->DetachCurrentThread() != JNI_OK)
            ALOGW("Warning: unable to detach main thread\n");
        if (mJavaVM->DestroyJavaVM() != 0)
            ALOGW("Warning: VM did not shut down cleanly\n");
    }

    可以看到以上的代码主要就是初始化了 JNI(C++ 与 Java 交互)功能并创建并启动了 JVM 虚拟机,通过反射的方式去启动 ZygoteInit.java 的 main 方法,并将 args 参数(包含了是否启动 SystemServer 的参数)传递过去。而 JVM 虚拟机进程就是 com.android.internal.os.ZygoteInit,而 ZygoteInit 进程位于 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 。

  3. ZygoteInit.java 源码分析

    路径:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    
    public static void main(String[] argv) {
        ZygoteServer zygoteServer = null;
    
        // 标记 zygote 开始了。
        ZygoteHooks.startZygoteNoThreadCreation();
    
        // 设置 zygote 自己的用户组 pid。
        try {
            Os.setpgid(0, 0);
        } catch (ErrnoException ex) {
            throw new RuntimeException("Failed to setpgid(0,0)", ex);
        }
    
        Runnable caller;
        try {
            // 读取系统是否已经启动完成。
            final long startTime = SystemClock.elapsedRealtime();
            final boolean isRuntimeRestarted = "1".equals(
                    SystemProperties.get("sys.boot_completed"));
    
            // 将行为写入 trace log 标记目前正处于 ZygoteInit 阶段。
            String bootTimeTag = Process.is64Bit() ? "Zygote64Timing" : "Zygote32Timing";
            TimingsTraceLog bootTimingsTraceLog = new TimingsTraceLog(bootTimeTag,
                    Trace.TRACE_TAG_DALVIK);
            bootTimingsTraceLog.traceBegin("ZygoteInit");
            RuntimeInit.preForkInit();
    
            boolean startSystemServer = false;
            // zygote 进程就是一个 socket,名称就叫 zygote。
            String zygoteSocketName = "zygote";
            String abiList = null;
            boolean enableLazyPreload = false;
            for (int i = 1; i < argv.length; i++) {
                // 从 AndroidRuntime.cpp 中传递上来,已经包含了 start-system-server。
                // 所以 startSystemServer = true。
                if ("start-system-server".equals(argv[i])) {
                    startSystemServer = true;
                }
    
                ...
            }
    
            // 为 true,是私有 zygote。
            final boolean isPrimaryZygote = zygoteSocketName.equals(Zygote.PRIMARY_SOCKET_NAME);
            
            ...
    
            // 记录的 trace log,只记录到这个地方。
            bootTimingsTraceLog.traceEnd();
    
            // 初始化 socket,从环境中获取套接字 FD(ANDROID_SOCKET_zygote)。
            // 若获取不到则创建一个用于和 systemServer 通信的 socket,当 systemServer fork 出来后 socket 进程将关闭。
            Zygote.initNativeState(isPrimaryZygote);
    
            ...
    
            // 根据环境变量(LocalServerSocket)获取 zygote 文件描述符并重新创建一个 socket,可以从这里看到 zygote 其实就是一个 socket。
            // 这个 name 为 zygote 的 Socket 用来等待 ActivityManagerService 来请求 Zygote 来 fork 出新的应用程序进程。
            // 所以 ActivityManagerService 里启动应用程序(APP),都是由该 zygote socket 进行处理并 fork 出的子进程。
            zygoteServer = new ZygoteServer(isPrimaryZygote);
    
            // 默认为 true,将启动 systemServer。
            if (startSystemServer) {
                // zygote 就是一个孵化器,所以这里直接 fork 出来 SystemServer。
                Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);
    
                // 让 SystemServer 子进程运行起来。
                if (r != null) {
                    r.run();
                    return;
                }
            }
    
            Log.i(TAG, "Accepting command socket connections");
    
            // 让 zygote socket(注意不是 systemServer zygote)循环运行。
            // 等待 client 进程来请求调用,请求创建子进程(fork 出子进程(例如等待 AMS 的请求))。
            caller = zygoteServer.runSelectLoop(abiList);
        } catch (Throwable ex) {
            ...
        } finally {
            if (zygoteServer != null) {
                // 停止关于 systemServer 的 socket,保留和 AMS 通信的 socket。
                // 在 initNativeState 阶段创建了一个和 systemServer 通信的 socket。
                // 接着拿到 systemServer socket 文件描述符重新创建了一个可以和 AMS 通信的 socket(/dev/socket/zygote)。
                zygoteServer.closeServerSocket();
            }
        }
    
        ...
    }

    以上代码讲述了 SystemServer Socket 的创建,将行为写入到 trace log 日志系统中,并通过 JNI 调用到底层的 fork 函数,孵化出 SystemServer 进程,如果 SystemServer 创建成功并已经运行了就会将当前 Socket 进行 close。期间会创建一个 Zygote Socket,用于等待其他子进程来连接,例如等待 AMS(Activity Manager Service)来连接该 Socket,然后继续 fork 出子进程(也就是应用程序,所以应用程序就是通过 Zygote 来 fork 出来的)。创建了 2 个 Socket,一个是 SystemServer Socket(Zygote.initNativeState(isPrimaryZygote) 来创建),一个是 Zygote Socket(new ZygoteServer(isPrimaryZygote) 来创建),注意区分。

    继续来看一下 zygoteServer = new ZygoteServer(isPrimaryZygote);

  4. ZygoteServer.java 源码分析

    路径:/frameworks/base/core/java/com/android/internal/os/ZygoteServer.java
    
    ZygoteServer(boolean isPrimaryZygote) {
        mUsapPoolEventFD = Zygote.getUsapPoolEventFD();
    
        // 创建 socket,名称为 zygote,路径:/dev/sockets/zygote 。
        if (isPrimaryZygote) {
            mZygoteSocket = Zygote.createManagedSocketFromInitSocket(Zygote.PRIMARY_SOCKET_NAME);
            
            ...
        }
        
        ...
    }
    路径:/frameworks/base/core/java/com/android/internal/os/Zygote.java
    
    static LocalServerSocket createManagedSocketFromInitSocket(String socketName) {
        // 文件描述符通过 ANDROID_socket_<socketName> 环境变量共享。
        int fileDesc;
        final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;
    
        try {
            String env = System.getenv(fullSocketName);
            // 拿到文件描述符内容。
            fileDesc = Integer.parseInt(env);
        } catch (RuntimeException ex) {
            throw new RuntimeException("Socket unset or invalid: " + fullSocketName, ex);
        }
    
        try {
            // 生成文件描述符。
            FileDescriptor fd = new FileDescriptor();
            fd.setInt$(fileDesc);
            return new LocalServerSocket(fd);
        } catch (IOException ex) {
            throw new RuntimeException(
                "Error building socket from file descriptor: " + fileDesc, ex);
        }
    }
    路径:/frameworks/base/core/java/android/net/LocalServerSocket.java
    
    public LocalServerSocket(FileDescriptor fd) throws IOException
    {
        // 创建 socket 并持续监听(等待 client 来调用)。
        impl = new LocalSocketImpl(fd);
        impl.listen(LISTEN_BACKLOG);
        localAddress = impl.getSockAddress();
    }

    简单点来说就是创建了一个 Zygoye Socket,位于 /dev/sockets/zygote,并调用了 runSelectLoop 让其循环运行,等待新进程发来的请求并进行连接 zygoteServer.runSelectLoop(abiList) 然后 fork 出子应用程序进程。

    路径:/frameworks/base/core/java/com/android/internal/os/ZygoteServer.java
    
    Runnable runSelectLoop(String abiList) {
        ArrayList<FileDescriptor> socketFDs = new ArrayList<>();
        ArrayList<ZygoteConnection> peers = new ArrayList<>();
    
        // 拿到 socket 的文件描述符。
        socketFDs.add(mZygoteSocket.getFileDescriptor());
        peers.add(null);
    
        ...
    
        while (true) {
            ...
    
            if (pollReturnValue == 0) {
                ...
            } else {
                boolean usapPoolFDRead = false;
    
                while (--pollIndex >= 0) {
                    if ((pollFDs[pollIndex].revents & POLLIN) == 0) {
                        continue;
                    }
    
                    if (pollIndex == 0) {
                        // acceptCommandPeer 函数得到 ZygoteConnection 类并添加到 Socket 连接列表 peers 中。
                        // 接着将该 ZygoteConnection 的文件描述符添加到 fd 列表 fds 中,以便可以接收到 ActivityManagerService 发送过来的请求。
                        ZygoteConnection newPeer = acceptCommandPeer(abiList);
                        peers.add(newPeer);
                        socketFDs.add(newPeer.getFileDescriptor());
                    }
                    
                    ...
                }
    
                ...
            }
    
            ...
        }
    }

    zygoteServer.runSelectLoop(abiList) 持续等待进程来请求连接并 fork 出应用。

    至此 Zygote Socket 已经启动完毕了,该 Socket 会等待 AMS 进程发来的应用程序进程 fork。

    继续看看 SystemServer 是怎么被 fork 出来的 forkSystemServer(abiList, zygoteSocketName, zygoteServer);

  5. forkSystemServer 源码分析

    路径:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    
    private static Runnable forkSystemServer(String abiList, String socketName,
            ZygoteServer zygoteServer) {
        ...
    
        // 创建 args 数组,这个数组用来保存启动 SystemServer 的启动参数,其中可以看出 SystemServer 进程的用户 id 和用户组 id 被设置为 1000。
        // 并且拥有用户组 1001 ~ 1010,1018,1021,1023,1024,1032,1065,3001 ~ 3003,3005 ~ 3007,3009 ~ 3012 的权限,进程名为 system_server。
        // 启动的类名为 com.android.server.SystemServer
        String[] args = {
                "--setuid=1000",
                "--setgid=1000",
                "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,"
                        + "1024,1032,1065,3001,3002,3003,3005,3006,3007,3009,3010,3011,3012",
                "--capabilities=" + capabilities + "," + capabilities,
                "--nice-name=system_server",
                "--runtime-args",
                "--target-sdk-version=" + VMRuntime.SDK_VERSION_CUR_DEVELOPMENT,
                "com.android.server.SystemServer",
        };
        ZygoteArguments parsedArgs;
    
        int pid;
    
        try {
            ...
    
            // 通过 JNI 形式去调用 init 进程下的 fork 函数,派生出 systemServer 进程。
            pid = Zygote.forkSystemServer(
                    parsedArgs.mUid, parsedArgs.mGid,
                    parsedArgs.mGids,
                    parsedArgs.mRuntimeFlags,
                    null,
                    parsedArgs.mPermittedCapabilities,
                    parsedArgs.mEffectiveCapabilities);
        } catch (IllegalArgumentException ex) {
            throw new RuntimeException(ex);
        }
    
        // pid == 0 代表已经运行在子进程(SystemServer)上了。
        // 代表 SystemServer 创建成功,创建成功后会关闭该 socket。
        if (pid == 0) {
            ...
    
            // 销毁 zygoteServer,保留和 AMS 通信的 socket(runSelectLoop)。
            // 当 SystemServer 创建过后,zygoteServerSocket 就没有用处了,进行关闭。
            zygoteServer.closeServerSocket();
            // 处理 system server 进程初始化工作并启动 SystemServer 进程。
            // 并启动了一个 binder 线程池供 system server 进程和其他进程通信使用。
            // 最后调用 RuntimeInit.applicationInit() 执行进程启动自身初始化工作。
            // applicationInit() 最后是通过反射调用了 SystemServer.java 中的 main() 方法。
            return handleSystemServerProcess(parsedArgs);
        }
    
        return null;
    }

    Zygote.forkSystemServer 就是调用了底层的 fork 函数。以上代码已知 SystemServer 子进程已经创建成功,将调用 handleSystemServerProcess 来启动 SystemServer.java 的入口。handleSystemServerProcess 会一直调用到 RuntimeInit.java 的 findStaticMain 方法中:

    路径:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    
    private static Runnable handleSystemServerProcess(ZygoteArguments parsedArgs) {
        ...
    
        if (parsedArgs.mInvokeWith != null) {
            ...
        } else {
            ...
    
            return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
                    parsedArgs.mDisabledCompatChanges,
                    parsedArgs.mRemainingArgs, cl);
        }
    }
    路径:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    
    public static Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
            String[] argv, ClassLoader classLoader) {
        ...
    
        return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
                classLoader);
    }
    路径:/frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
    
    protected static Runnable applicationInit(int targetSdkVersion, long[] disabledCompatChanges,
            String[] argv, ClassLoader classLoader) {
        ...
    
        return findStaticMain(args.startClass, args.startArgs, classLoader);
    }
    路径:/frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
    
    protected static Runnable findStaticMain(String className, String[] argv,
            ClassLoader classLoader) {
        Class<?> cl;
    
        try {
            // 反射拿到 SystemServer 类
            cl = Class.forName(className, true, classLoader);
        } catch (ClassNotFoundException ex) {
            ...
        }
    
        Method m;
        try {
            // 反射拿到 SystemServer.java 的 main 函数,并启动。
            m = cl.getMethod("main", new Class[] { String[].class });
        } catch (NoSuchMethodException ex) {
            ...
        } catch (SecurityException ex) {
            ...
        }
    
        ...
    
        return new MethodAndArgsCaller(m, argv);
    }

    可以看到 handleSystemServerProcess 下面的子方法去调用了 com.android.server.SystemServer 的 main 方法,至此 SystemServer 就创建和启动完毕了,那么 SystemServer Socket 就会销毁并关闭。

三、总结

Zygote 进程是从 init.rc 脚本中启动的。Zygote 进程本质上是一个服务端 Socket,它会一直运行,等待新的进程请求。在创建 Zygote 进程时,会携带 StartSystemServer 参数,这个参数会触发 Zygote 进程创建 SystemServer 子进程。SystemServer 进程是通过 Zygote 进程 fork 出来的,它的启动是由 ZygoteInit 通过反射的方式调用 SystemServermain 方法实现的。

Zygote 进程在启动时创建了一个服务端 Socket,这个 Socket 用于与 SystemServer 进程的通信。当 SystemServer 进程创建完成后,Zygote 进程会关闭与 SystemServer 进程的 Socket 连接。此时,Zygote 进程已经完成了它的主要任务,它会进入一个循环,等待 Activity Manager Service (AMS) 或其他进程来请求新的应用程序进程。这个循环是通过调用 runSelectLoop 方法实现的。

请注意,Zygote 进程本身并不会关闭或销毁,它会一直运行,等待新的进程请求。而与 SystemServer 进程的 Socket 连接在 SystemServer 进程创建完成后就会关闭,因为此时已经没有必要维持这个连接了。

参考

网络分层

网络分层就是将网络节点所要完成的数据的发送或转发、打包或拆包,以及控制信息的加载或拆出等工作,分别由不同的硬件和软件模块来完成。这样可以将通信和网络互联这一复杂的问题变得较为简单。网络分层有不同的模型,有的模型分7层,有的模型分5层。这里介绍分5层的,因为它更好理解。网络分层的每一层都是为了完成一种功能而设的。为了实现这些功能,就需要遵守共同的规则,这个规则叫作协议

网络分层如图所示:


网络分层从上到下分别是应用层、传输层、网络层、数据链路层和物理层。越靠下的层越接近硬件。接下来我们从下而上来分别了解这些分层。

  1. 物理层

    该层负责比特流在节点间的传输,即负责物理传输。该层的协议既与链路有关,也与传输介质有关。其通俗来讲就是把计算机连接起来的物理手段。

  2. 数据链路层

    该层控制网络层与物理层之间的通信,其主要功能是如何在不可靠的物理线路上进行数据的可靠传递。为了保证传输,从网络层接收到的数据被分割成特定的可被物理层传输的帧。帧是用来移动数据的结构包,它不仅包括原始数据,还包括发送方和接收方的物理地址以及纠错和控制信息。其中的地址确定了帧将发送到何处,而纠错和控制信息则确保帧无差错到达。如果在传送数据时,接收点检测到所传数据中有差错,就要通知发送方重发这一帧。

  3. 网络层

    该层决定如何将数据从发送方路由到接收方。网络层通过综合考虑发送优先权、网络拥塞程度、服务质量以及可选路由的花费来决定从一个网络中的节点 A 到另一个网络中节点 B 的最佳路径。

  4. 传输层

    该层为两台主机上的应用程序提供端到端的通信。相比之下,网络层的功能是建立主机到主机的通信。传输层有两个传输协议:TCP(传输控制协议)和UDP(用户数据报协议)。其中,TCP 是一个可靠的面向连接的协议,UDP 是不可靠的或者说无连接的协议。

  5. 应用层

    应用程序收到传输层的数据后,接下来就要进行解读。解读必须事先规定好格式,而应用层就是规定应用程序的数据格式的。它的主要协议有 HTTP、FTP、Telnet、SMTP、POP3 等。

ArkUI - 动画

利用属性动画显示动画组件转场动画实现组件动画效果。

一、属性动画

属性动画是通过设置组件的 animation 属性来给组件添加动画,当组件的 width、height、Opacity、backgroundColor、scale、rotate、translate 等属性变更时,可以实现渐变过渡效果。

以 Image 组件为例,给它添加动画,其实就是给它添加 animation 的属性:

Image($r('app.media.app_icon'))
  .position({
    x: 10, // x轴坐标
    y: 10  // y轴坐标
  })
  .rotate({
    angle: 0,       // 旋转角度
    centerX: '50%', // 旋转中心横坐标
    centerY: '50%'  // 旋转中心纵坐标
  })
  .animation({
    duration: 1000,
    curve: Curve.EaseInOut
  })

这时,ArkUI 就能帮我们监控组件的样式变化,我们只需要在与用户互动的事件当中去修改组件的样式,ArkUI 一旦发现组件的样式变化,就会自动填充起始样式和结束样式之间的每一帧画面,从而实现样式变化的渐变过渡效果。所以,一个动画就出来了。

注意:

  1. animation 属性一定要放在需要有动画属性的样式之后。就像上面的实例代码,animation 要放在 position 和 rotate 之后。如果把 animation 放在前面,然后再写 position 和 rotate,那么这俩就不会有任何的变化。

  2. animation 属性不是对所有样式都有效。

animation 可以传递的动画参数:

名称 参数类型 必填 描述
duration number 设置动画时长。
默认值:1000,单位:毫秒
tempo number 动画播放速度。数值越大,速度越快。
默认值:1
curve Curve 设置动画曲线。
默认值:Curve.EaseInOut,平滑开始和结束。
delay number 设置动画延迟执行的时长。
默认值:0,单位:毫秒
iterations number 设置播放次数。
默认值:1,取值范围:[-1, +∞)
说明:设置为 -1 时表示无限次播放。
playMode PlayMode 设置动画播放模式,默认播放完成后重头开始播放。
默认值:PlayMode.Normal
onFinish ()=> void 状态回调,动画播放完成时触发。

示例代码:

@Entry
@Component
struct Index {
  @State textX: number = 10
  @State textY: number = 10

  build() {
    Column() {
      Image($r('app.media.app_icon'))
        .position({
          x: this.textX,
          y: this.textY
        })
        .rotate({
          angle: 0,
          centerX: '50%',
          centerY: '50%'
        })
        .width(40)
        .height(40)
        .animation({
          duration: 500
        })

      Button('按钮')
        .position({
          x: 10,
          y: 100
        })
        .onClick(() => {
          this.textX += 20
        })
    }
  }
}

运行效果:

二、显示动画

显示动画是通过全局 animationTo 函数来修改组件属性,实现属性变化时的渐变过渡效果。

显示调用 animationTo 函数触发动画:

animateTo(
  { duration: 1000 }, // 动画参数
  () => {
    // 修改组件属性关联的状态变量
  })

示例代码:

@Entry
@Component
struct Index {
  @State textX: number = 10
  @State textY: number = 10

  build() {
    Column() {
      Image($r('app.media.app_icon'))
        .position({
          x: this.textX,
          y: this.textY
        })
        .rotate({
          angle: 0,
          centerX: '50%',
          centerY: '50%'
        })
        .width(40)
        .height(40)

      Button('按钮')
        .position({
          x: 10,
          y: 100
        })
        .onClick(() => {
          animateTo(
            { duration: 500 },
            () => {
              this.textX += 20
            }
          )
        })
    }
  }
}

三、组件转场动画

组件转场动画是在组件插入或移除时的过渡动画,通过组件的 transition 属性来配置。组件插入可以理解为组件从无到有,也就是一个入场的过程;组件移除可以理解为组件从有到无,也就是一个退场的过程。

语法:

Image($r('app.media.app_icon'))
  .transition({
    opacity: 0,
    rotate: { angle: -360 },
    scale: { x: 0, y: 0 }
  })

动画参数:

参数名称 参数类型 必填 参数描述
type TransistionType 类型,默认包含组件新增和删除。
默认是 ALL
opacity number 不透明度,为插入时起点和删除时终点的值。
默认值:1,取值范围:[0, 1]
translate {
  x?: number或string,
  y?: number或string,
  z?: number或string
}
平移效果,为插入时起点和删除时终点的值。
-x: 横向的平移距离。
-y: 纵向的平移距离。
-z: 竖向的平移距离。
scale {
  x?: number,
  y?: number,
   z?: number,
  centerX?: number或string,
  centerY?: number或string
}
缩放效果,为插入时起点和删除时终点的值。
-x: 横向放大倍数(或缩小比例)。
-y: 纵向放大倍数(或缩小比例)。
-z: 当前为二维显示,该参数无效。
-centerX、centerY 指缩放中心点,centerX和centerY默认值是"50%"。
-中心点为0时,默认的是组件的左上角。
rotate {
  x?: number,
  y?: number,
  z?: number,
  angle: number或string,
  centerX?: number或string,
  centerY?: number或string
}
旋转效果:
angle 是旋转角度,其它参数与 scale 类似。

注意:transition 要结合 animateTo 去使用。

示例代码:

@Entry
@Component
struct Index {
  @State isBegin: boolean = false

  build() {
    Column() {
      if (this.isBegin) {
        Image($r('app.media.app_icon'))
          .position({
            x: 10,
            y: 10
          })
          .width(100)
          .height(100)
          .transition({
            type: TransitionType.Insert,
            opacity: 0,
            translate: { x: -100 }
          })
      }

      Button('按钮')
        .position({
          x: 10,
          y: 200
        })
        .onClick(() => {
          animateTo(
            { duration: 1000 },
            () => {
              this.isBegin = true
            })
        })
    }
  }
}

运行效果:

Stage 模型 - UIAbility 生命周期

UIAbility 就是 UI 界面的组件,提供一个用来绘制界面的窗口,应用就展现出来。一个应用内部会包含一个或多个 UIAbility。是系统内部,应用调度的基本单元。在应用运行的过程,其实就是一个个 UIAbility 创建、切换、销毁的过程。

一、UIAbility 生命周期过程

我们在手机桌面点击APP的图标,当 Stage 创建好以后,去 Create 入口 Ability。

当舞台创建好,然后舞台上要表演的 Ability 也创建好,但是还在幕后,这就需要从幕后挪到台前变成 Foreground,也就是前台状态,这时我们才能够看到应用所对应的 UI 界面。

在界面我们会点击各种各样的功能,当我们从一级页面 A 切换到二级页面 B(是一个独立的功能,就会有一个独立的 Ability)时,也就创建了新的 Ability,系统发现这个功能不在当前这个 Ability 里,就会去找到底在哪里 Ability 里,然后创建出来挪到前台,也就是再走一遍 Create -> Foreground 的过程。也就是说 B 这个 Ability 跑到舞台前台。舞台只能展示一个 Ability,当 B 跑到前台时,A 就跑到后台 Background

当我们通过任务列表清除应用时,也就是销毁 Destroy 所有创建好的 Ability。

如图1所示:

图1

二、完整的 UIAbility、WindowStage 生命周期变化

UIAbility 里面持有 WindowStage,也就是 Window 的舞台。随着 UIAbility 生命周期的变化,WindowStage 状态也在发生变化。

随着 UIAbility 的创建 Create,不会立刻切换到前台 Foreground,而是先会去把对应的 WindowStage 给它创建出来,即 WindowStageCreate,因为有了 Window 舞台,才能有窗口,才能有页面。然后再把 UIAbility 切换到前台 Foreground。切换到前台以后,WindowStage 状态从原来不可见变成可见 Visible,并且获取焦点 Active

将来这个 UIAbility 展示出来以后,还可能把它切到后台,变成一个后台应用。这个时候 WindowStage 也会发生变化。它会从获取焦点变成失焦 InActive,从可见变成不可见 InVisible。接着再把整个 UIAbility 连带窗口一起挪到后台 Background。变成一个后台应用。

当 UIAbility 被销毁的时候,对应的 WindowStage 也要被销毁。在销毁 UIAbility 之前,先销毁 WindowStage,即 WindowStageDestroy。再去销毁 UIAbility,即 Destroy

如图2所示:

图2

三、代码验证生命周期

在 /entry/src/main/ets/entryability/ 中,会有入口 Ability,即 EntryAbility.ts,每个 Ability 都有自己的生命周期,生命周期就定义在 Ability 里:

import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';

// 继承 UIAbility
export default class EntryAbility extends UIAbility {
  // 创建
  onCreate(want, launchParam) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }

  // 销毁
  onDestroy() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  // WindowStage 创建
  onWindowStageCreate(windowStage: window.WindowStage) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    // 加载 /pages.Index.ets(也就解释了应用打卡默认展示首页的原因)
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
    });
  }

  // WindowStage 销毁
  onWindowStageDestroy() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  // 前台
  onForeground() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  // 后台
  onBackground() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

运行程序,查看 log:

Ability onCreate
Ability onWindowStageCreate
Ability onForeground
Succeeded in loading the content. Data:

当我们点击 Home 按钮,查看 log:

Ability onBackground

点击任务列表按钮,点击刚才的程序,查看 log:

Ability onForeground

在应用内部的主页面,点击返回按钮,查看 log:

Ability onBackground
Ability onWindowStageDestroy
Ability onDestroy

四、总结

UIAbility 会经历如下几个阶段:

  1. 应用启动时,Ability 被创建。

    Ability onCreate
    
  2. Ability 创建之后,Ability 所持有的 WindowStage 被创建。

    Ability onWindowStageCreate
    
  3. WindowStage 创建之后,应用就可以切换到前台,同时对应的窗口被展示出来。

    Ability onForeground
    
  4. 窗口展示出来后,就可以加载窗口里面的页面,就可以看到页面内的内容。

    Succeeded in loading the content. Data:
    
  5. 如果我们在访问页面的某个功能时,不属于当前 Ability,就会触发其它 Ability 的 Create

    Ability onCreate
    Ability onWindowStageCreate
    Ability onForeground
    Succeeded in loading the content. Data:
    
  6. 之前的 Ability 就会被切到后台。

    Ability onBackground
    
  7. 之前的 Ability 也可以再切换回前台。

    Ability onForeground
    
  8. 退出应用,也就是销毁。

    Ability onBackground
    Ability onWindowStageDestroy
    Ability onDestroy
    

源码解析 Scroller

本文是基于 Android 14 的源码解析。

要想使用 Scroller,必须先调用 new Scroller()。下面先来看看 Scroller 的构造方法,代码如下所示:

public Scroller(Context context) {
    this(context, null);
}

public Scroller(Context context, Interpolator interpolator) {
    this(context, interpolator,
            context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}

public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
    mFinished = true;
    if (interpolator == null) {
        mInterpolator = new ViscousFluidInterpolator();
    } else {
        mInterpolator = interpolator;
    }
    mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
    mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
    mFlywheel = flywheel;

    mPhysicalCoeff = computeDeceleration(0.84f);
}

从上面的代码我们得知,Scroller 有三个构造方法,通常情况下我们都用第一个;第二个需要传进去一个插值器 Interpolator,如果不传则采用默认的插值器 ViscousFluidInterpolator。接下来看看 Scroller 的 startScroll() 方法,代码如下所示:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0f / (float) mDuration;
}

在 startScroll() 方法中并没有调用类似开启滑动的方法,而是保存了传进来的各种参数:startX 和 startY 表示滑动开始的起点,dx 和 dy 表示滑动的距离,duration 则表示滑动持续的时间。所以 startScroll() 方法只是用来做前期准备的,并不能使 View 进行滑动。关键是我们在 startScroll() 方法后调用了 invalidate() 方法,这个方法会导致 View 的重绘,而 View 的重绘会调用 View 的 draw() 方法,draw() 方法又会调用 View 的 computeScroll() 方法。我们重写 computeScroll() 方法如下:

override fun computeScroll() {
    super.computeScroll()
    scroller?.let {
        if (it.computeScrollOffset()) {
            (parent as View).scrollTo(it.currX, it.currY)
            invalidate()
        }
    }
}

我们在 computeScroll() 方法中通过 Scroller 来获取当前的 ScrollX 和 ScrollY,然后调用 scrollTo() 方法进行 View 的滑动,接着调用 invalidate 方法来让 View 进行重绘,重绘就会调用 computeScroll() 方法来实现 View 的滑动。这样通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果。但是在 Scroller 中如何获取当前位置的 ScrollX 和 ScrollY 呢?我们忘了一点,那就是在调用 scrollTo() 方法前会调用 Scroller 的 computeScrollOffset() 方法。接下来看看 computeScrollOffset() 方法:

public boolean computeScrollOffset() {
    if (mFinished) {
        return false;
    }

    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

    if (timePassed < mDuration) {
        switch (mMode) {
        case SCROLL_MODE:
            final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
            mCurrX = mStartX + Math.round(x * mDeltaX);
            mCurrY = mStartY + Math.round(x * mDeltaY);
            break;
        case FLING_MODE:
            final float t = (float) timePassed / mDuration;
            final int index = (int) (NB_SAMPLES * t);
            float distanceCoef = 1.f;
            float velocityCoef = 0.f;
            if (index < NB_SAMPLES) {
                final float t_inf = (float) index / NB_SAMPLES;
                final float t_sup = (float) (index + 1) / NB_SAMPLES;
                final float d_inf = SPLINE_POSITION[index];
                final float d_sup = SPLINE_POSITION[index + 1];
                velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                distanceCoef = d_inf + (t - t_inf) * velocityCoef;
            }

            mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
            
            mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
            // Pin to mMinX <= mCurrX <= mMaxX
            mCurrX = Math.min(mCurrX, mMaxX);
            mCurrX = Math.max(mCurrX, mMinX);
            
            mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
            // Pin to mMinY <= mCurrY <= mMaxY
            mCurrY = Math.min(mCurrY, mMaxY);
            mCurrY = Math.max(mCurrY, mMinY);

            if (mCurrX == mFinalX && mCurrY == mFinalY) {
                mFinished = true;
            }

            break;
        }
    }
    else {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}

首先会计算动画持续的时间 timePassed。如果动画持续时间小于我们设置的滑动持续时间 mDuration,则执行 Switch 语句。因为在 startScroll() 方法中的 mMode 值为 SCROLL_MODE,所以执行分支语句 SCROLL_MODE,然后根据插值器 Interpolator 来计算出在该时间段内移动的距离,赋值给 mCurrX 和 mCurrY,这样我们就能通过 Scroller 来获取当前的 ScrollX 和 ScrollY 了。另外,computeScrollOffset() 的返回值如果为 true 则表示滑动未结束,为 false 则表示滑动结束。所以,如果滑动未结束,我们就得持续调用 scrollTo() 方法和 invalidate() 方法来进行 View 的滑动。

讲到这里总结一下 Scroller 的原理:Scroller 并不能直接实现 View 的滑动,它需要配合 View 的 computeScroll() 方法。在 computeScroll() 中不断让 View 进行重绘,每次重绘都会计算滑动持续的时间,根据这个持续时间就能算出这次 View 滑动的位置,我们根据每次滑动的位置调用 scrollTo() 方法进行滑动,这样不断地重复上述过程就形成了弹性滑动。

在基于 Ubuntu 或 Debian 的系统上创建启动图标(快捷方式)

在 Ubuntu 系统,有时我们安装的应用程序在桌面或应用程序菜单中看不到快捷方式。这篇文章将告诉你如何在基于 Ubuntu 或 Debian 的系统上创建启动图标(快捷方式)。本文以 IDEA 为例介绍创建桌面配置文件的方法。

  1. 打开终端,进入到 /usr/share/applications 目录

    cd /usr/share/applications
    
  2. 创建 .desktop 文件

    sudo touch intellij-idea.desktop
    
  3. 使用 Visual Studio Code文本编辑器 打开 intellij-idea.desktop,然后把下面的内容复制过去

    推荐使用 Visual Studio Code,原因是配置简单,不需要敲命令

    [Desktop Entry]
    Name=IntelliJ IDEA
    Comment=intellij idea
    Exec=/home/dataspace01/Ubuntu_Software/IntelliJ-IDEA/bin/idea.sh
    Icon=/home/dataspace01/Ubuntu_Software/IntelliJ-IDEA/bin/idea.png
    Terminal=false
    Type=Application
    • Name:应用程序名称
    • Comment:将鼠标悬停在图标上时出现的小对话框
    • Exec:应用程序的可执行文件的路径
    • Icon:应用程序图标的路径
    • Terminal:软件打开时是否启动终端
  4. 保存 intellij-idea.desktop,就可以在 显示应用程序 列表里看到刚刚配置的快捷方式

源码解析 Activity 的构成

本文是基于 Android 14 的源码解析。

当我们写 Activity 时会调用 setContentView() 方法来加载布局。现在来看看 setContentView() 方法是怎么实现的,源码如下所示:

路径:/frameworks/base/core/java/android/app/Activity.java

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

这里调用了 getWindow().setContentView(layoutResID),getWindow() 指的是什么呢?接着往下看,getWindow() 返回 mWindow,源码如下所示:

路径:/frameworks/base/core/java/android/app/Activity.java

public Window getWindow() {
    return mWindow;
}

那这个 mWindow 又是什么呢?我们继续查看代码,最终在 Activity 的 attach() 方法中发现了 mWindow,源码如下所示:

路径:/frameworks/base/core/java/android/app/Activity.java

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
        IBinder shareableActivityToken) {
    ...

    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    
    ...
}

而 getWindow() 又指的是 PhoneWindow。所以来看看 PhoneWindow 的 setContentView() 方法,源码如下所示:

路径:/frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

public class PhoneWindow extends Window implements MenuBuilder.Callback {

    ...

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            mContentParent.addView(view, params);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

    ...
}

原来 mWindow 指的就是 PhoneWindow,PhoneWindow 是继承抽象类 Window 的,这样就知道了getWindow() 得到的是一个 PhoneWindow,因为 Activity 中 setContentView() 方法调用的是 getWindow().setContentView(layoutResID)。

挑关键的接着看,看看上面代码 installDecor() 方法里面做了什么,源码如下所示:

路径:/frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);

        ...
    }
}

在前面的代码中没发现什么,紧接着查看上面代码 generateDecor() 方法里做了什么,源码如下所示:

路径:/frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

protected DecorView generateDecor(int featureId) {
    ...
    
    return new DecorView(context, featureId, this, getAttributes());
}

这里创建了一个 DecorView,这个 DecorView 就是 Activity 中的根 View。接着查看 DecorView 的源码,发现 DecorView 是 PhoneWindow 类的内部类,并且继承了 FrameLayout。我们再回到 installDecor() 方法中,查看 generateLayout(mDecor) 做了什么,源码如下所示:

路径:/frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

protected ViewGroup generateLayout(DecorView decor) {
    ...

    int layoutResource;
    int features = getLocalFeatures();
    if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleIconsDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
        removeFeature(FEATURE_ACTION_BAR);
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
            && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
        layoutResource = R.layout.screen_progress;
    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogCustomTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_custom_title;
        }
        removeFeature(FEATURE_ACTION_BAR);
    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
            layoutResource = a.getResourceId(
                    R.styleable.Window_windowActionBarFullscreenDecorLayout,
                    R.layout.screen_action_bar);
        } else {
            layoutResource = R.layout.screen_title;
        }
    } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
        layoutResource = R.layout.screen_simple_overlay_action_mode;
    } else {
        layoutResource = R.layout.screen_simple;
    }

    ...

    return contentParent;
}

PhoneWindow 的 generateLayout() 方法比较长,这里只截取了一小部分关键的代码,其主要内容就是根据不同的情况加载不同的布局给 layoutResource。现在查看上面代码 R.layout.screen_title,源码如下所示:

路径:/frameworks/base/core/res/res/layout/screen_title.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

上面的 ViewStub 是用来显示 Actionbar 的。下面的两个 FrameLayout:一个是 title,用来显示标题;另一个是 content,用来显示内容。看到上面的源码,大家就知道了一个 Activity 包含一个 Window 对象,这个对象是由 PhoneWindow 来实现的。PhoneWindow 将 DecorView 作为整个应用窗口的根 View,而这个 DecorView 又将屏幕划分为两个区域:一个是 TitleView,另一个是 ContentView,而我们平常做应用所写的布局正是展示在 ContentView 中的,如图所示:

ArkUI - 状态管理

在声明式 UI 中,是以状态驱动视图更新,如图1所示:

图1

其中核心的概念就是状态(State)和视图(View):

  • 状态(State):指驱动视图更新的数据(被装饰器标记的变量)

    @Entry
    @Component
    struct Index {
      @State message: string = 'Hello World'
    
      build() {
        Column() {
          Text(this.message)
            .fontSize(50)
            .onClick(() => {
              this.message = 'Hello ArkTS'
            })
        }
        .width('100%')
        .height('100%')
      }
    }
    

    Index 组件里定义了 message 变量,而 message 前面就加了 @State 装饰器,如果没有这个装饰器,message 就是一个普通的变量,但是呢,正是我们给它加上了 @State 装饰器,所以,它就变成了一个状态变量。

  • 视图(View):基于 UI 描述渲染得到的用户界面

    @Entry
    @Component
    struct Index {
      @State message: string = 'Hello World'
    
      build() {
        Column() {
          Text(this.message)
            .fontSize(50)
            .onClick(() => {
              this.message = 'Hello ArkTS'
            })
        }
        .width('100%')
        .height('100%')
      }
    }
    

    build 函数内部就是 UI 的描述,我们这里就描述了一个列式的容器,容器里有一个普通的文本,文本的内容就是 message 的值,所以最终渲染出来的视图就是在屏幕上显示一个 Hello World。

视图渲染好了以后,用户就可以对视图中的页面元素产生交互,比如去触摸、点击、拖拽等事件。这些互动事件就有可能改变状态变量的值,比如说我们这个示例里,给 Text 绑定了一个点击事件,一旦用户点击,就会修改 message 的值,而在 ArkUI 的内部,有一种机制去监控状态变量的值,一旦发现发生了变更,就会触发视图的重新渲染,所以,按照我们这个示例来看,如果现在去点击这个 Hello World 文字,就会触发点击事件,修改 message 的值,把它变成 Hello ArkTS,而一旦这个变量值发生变更,视图重新渲染,于是,屏幕上显示的文字从 Hello World 变成 Hello ArkTS。

所以像这种状态视图之间相互作用的机制,我们就称之为状态管理机制。有了这种机制以后,我们将来开发的时候,不需要自己操作页面,只需要描述页面的结构,然后定义好对应的事件,在事件里面去操作状态,就可以了,这样每当用户去产生互动时,自然就会引起页面的动态刷新。所以一个动态页面就很容易的实现了。这也就是状态管理的好处。

一、@State 装饰器

  1. @State 装饰器标记的变量必须初始化,不能为空值。

    比如上面的示例代码,message 一声明,就给它初始化了一个 Hello World。

  2. @State 装饰器支持的类型是有限制的。

    支持 Object、class、string、number、boolean、enum 类型以及这些类型的数组。

    注:虽然以上这些类型都是允许的,但是有两个特殊场景:

    1. 嵌套类型:@State 修饰的变量是 Object,如果 Object 里面的属性发生了变更其实是能触发视图的更新,但是如果 Object 里面的某个属性它又是一个 Object,也就是 Object 套 Object,那就是嵌套类型,那么内部嵌套的那个 Object 它里面的属性再发生变更,就无法触发视图更新。

    2. 数组:数组中的元素不是简单类型,而是一个对象,那么对象里面的属性发生变更,同样无法触发视图更新。

二、@Prop@Link 装饰器

  1. 首先看一段代码,这是实现任务统计的示例代码:

     // 任务类
     class Task {
       static id: number = 1
       // 任务名称
       name: string = `任务${Task.id++}`
       // 任务状态
       finished: boolean = false
     }
    
     // 统一的卡片样式
     @Styles function card() {
       .width('95%')
       .padding(20)
       .backgroundColor(Color.White)
       .borderRadius(15)
       .shadow({ radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4 })
     }
    
     // 任务完成样式
     @Extend(Text) function finishedTask() {
       .decoration({ type: TextDecorationType.LineThrough })
       .fontColor('#B2B2B1')
     }
    
     @Entry
     @Component
     struct PropPage {
       // 总任务数量
       @State totalTask: number = 0
       // 已完成任务数量
       @State finishTask: number = 0
       // 任务数组
       @State tasks: Task[] = []
    
       build() {
         Column({ space: 10 }) {
           // 任务进度卡片
           Row() {
             Text('任务进度:')
               .fontSize(30)
               .fontWeight(FontWeight.Bold)
             Stack() {
               Progress({
                 value: this.finishTask,
                 total: this.totalTask,
                 type: ProgressType.Ring
               })
                 .width(100)
               Row() {
                 Text(this.finishTask.toString())
                   .fontSize(24)
                   .fontColor('#0000FF')
                 Text(' / ' + this.totalTask.toString())
                   .fontSize(24)
               }
             }
           }
           .card()
           .margin({ top: 20, bottom: 10 })
           .justifyContent(FlexAlign.SpaceEvenly)
    
           // 新增任务按钮
           Button('新增任务')
             .width(200)
             .margin({ top: 10 })
             .onClick(() => {
               // 新增任务数据
               this.tasks.push(new Task())
               // 更新任务总数量
               this.totalTask = this.tasks.length
             })
    
           // 任务列表
           List({ space: 10 }) {
             ForEach(
               this.tasks,
               (item: Task, index) => {
                 ListItem() {
                   Row() {
                     Text(item.name)
                       .fontSize(20)
                     Checkbox()
                       .select(item.finished)
                       .onChange(val => {
                         // 更新当前任务状态
                         item.finished = val
                         // 更新已完成任务数量
                         this.finishTask = this.tasks.filter(item => item.finished).length
                       })
                   }
                   .card()
                   .justifyContent(FlexAlign.SpaceBetween)
                 }
                 .swipeAction({ end: this.DeleteButton(index) })
               }
             )
           }
           .width('100%')
           .layoutWeight(1)
           .alignListItem(ListItemAlign.Center)
         }
         .width('100%')
         .height('100%')
         .backgroundColor('#F1F2F3')
       }
    
       @Builder DeleteButton(index: number) {
         Button() {
           Image($r('app.media.delete'))
             .fillColor(Color.White)
             .width(20)
         }
         .width(40)
         .height(40)
         .type(ButtonType.Circle)
         .backgroundColor(Color.Red)
         .margin(5)
         .onClick(() => {
           this.tasks.splice(index, 1)
           this.totalTask = this.tasks.length
           this.finishTask = this.tasks.filter(item => item.finished).length
         })
       }
     }
  2. 概念

    当父子组件之间需要数据同步时,可以使用 @Prop@Link 装饰器。

    Q:什么是父子组件?什么又是数据同步

    A:看上面这段示例代码,我们会发现代码是从上到下一股脑写的,写了上百行代码,整个代码的可读性是比较差的。要解决这个问题,可以把整个功能分成几个模块,然后按模块封装成一个一个的组件,这样在入口组件(@Entry)当中就不用写太多代码,而是去引用其他模块对应的组件。整个代码结构会更加清晰,复用性也会更好。所以,入口组件就是一个父组件,它引用了其他的组件,那么这些被引用的组件就是子组件。所以这时候组件之间就出现了这种引用关系,而组件之间引用的过程中可能就会有数据传递的需求。比如在父组件里定义了一些数据,然后在子组件里需要用,这时候就需要把父组件的数据传给子组件,单纯的传递还不够,每当数据发生变更,还要去通知子组件,这就叫数据同步。数据同步利用 @State 装饰器是实现不了的,那就需要用 @Prop@Link 装饰器来实现。

  3. @Prop@Link 装饰器对比

    @prop @link
    同步类型 单向同步 双向同步
    允许装饰的变量类型 · 父子类型一致:string、number、boolean、enum
    · 父组件是对象类型,子组件是对象属性
    · 不可以是数组、any
    · 父子类型一致:string、number、boolean、enum、object、class,以及它们的数组
    · 数组中元素增、删、替换会引起刷新
    · 嵌套类型以及数组中的对象属性无法触发视图更新
    初始化方式 允许子组件初始化 父组件传递,禁止子组件初始化
  4. 使用 @Prop 对示例代码进行封装和改造

    假设父组件中的变量采用 @State 装饰器,与之对应的子组件采用 @Prop 装饰器,那这时候就可以实现单项同步,当父组件对 @State 装饰的变量进行任意的修改时,就会立刻把这个数据传递给子组件,但反过来,子组件如果对这个数据进行了修改,是不会反向传递到父组件那里。所以,这种同步被称之为单向同步。实现原理就是拷贝

     ...
    
     @Entry
     @Component
     struct PropPage {
       // 总任务数量
       @State totalTask: number = 0
       // 已完成任务数量
       @State finishTask: number = 0
       // 任务数组
       @State tasks: Task[] = []
    
       build() {
         Column({ space: 10 }) {
           // 任务进度卡片
           TaskStatistics({ finishTask: this.finishTask, totalTask: this.totalTask })
    
           ...
         }
         .width('100%')
         .height('100%')
         .backgroundColor('#F1F2F3')
       }
    
       ...
     }
    
     @Component
     struct TaskStatistics {
       @Prop finishTask: number
       @Prop totalTask: number
    
       ...
     }
  5. 使用 @Link 对示例代码进行封装和改造

    假设父组件中的变量采用 @State 装饰器,与之对应的子组件采用 @Link 装饰器,此时就是双向同步,当父组件对 @State 装饰的变量进行任意的修改时,就会立刻把这个数据传递给子组件,反过来,子组件如果对这个数据进行了修改,也会把这个数据传递给父组件。所以,这种同步被称之为双向同步。实现原理就是引用

     ...
    
     @Entry
     @Component
     struct PropPage {
       // 总任务数量
       @State totalTask: number = 0
       // 已完成任务数量
       @State finishTask: number = 0
    
       build() {
         Column({ space: 10 }) {
           // 任务进度卡片
           ...
    
           // 任务列表
           TaskList({ finishTask: $finishTask, totalTask: $totalTask })
         }
         .width('100%')
         .height('100%')
         .backgroundColor('#F1F2F3')
       }
     }
    
     ...
    
     @Component
     struct TaskList {
       // 总任务数量
       @Link totalTask: number
       // 已完成任务数量
       @Link finishTask: number
       // 任务数组
       @State tasks: Task[] = []
    
       ...
     }
  6. 使用数组对示例代码进行封装和改造

     ...
    
     // 任务统计信息
     class StatisticsInfo {
       totalTask: number = 0
       finishTask: number = 0
     }
    
     @Entry
     @Component
     struct PropPage {
       // 任务统计信息
       @State info: StatisticsInfo = new StatisticsInfo()
    
       build() {
         Column({ space: 10 }) {
           // 任务进度卡片
           TaskStatistics({ finishTask: this.info.finishTask, totalTask: this.info.totalTask })
    
           // 任务列表
           TaskList({ info: $info })
         }
         .width('100%')
         .height('100%')
         .backgroundColor('#F1F2F3')
       }
     }
    
     @Component
     struct TaskStatistics {
       @Prop finishTask: number
       @Prop totalTask: number
    
       ...
     }
    
     @Component
     struct TaskList {
       @Link info: StatisticsInfo
    
       ...
     }

    结论:@Prop 不支持对象类型,@Link 支持对象类型。@Prop@Link 该怎么选?如果子组件拿到父组件的值以后,只是用来展示,不做修改,用 @Prop,如果子组件需要修改父组件的值,用 @Link

四、@Provide@Consume

@Provide@Consume 可以跨组件提供类似于 @State@Link 的双向同步。

使用 @Provide@Consume 对示例代码进行封装和改造:

...

// 任务统计信息
class StatisticsInfo {
  totalTask: number = 0
  finishTask: number = 0
}

@Entry
@Component
struct PropPage {
  // 任务统计信息
  @Provide info: StatisticsInfo = new StatisticsInfo()

  build() {
    Column({ space: 10 }) {
      // 任务进度卡片
      TaskStatistics()

      // 任务列表
      TaskList()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F2F3')
  }
}

@Component
struct TaskStatistics {
  @Consume info: StatisticsInfo

  ...
}

@Component
struct TaskList {
  @Consume info: StatisticsInfo

  ...
}

结论:@Provide@Consume 不需要显示的传参,内部会帮你去实现,但是代价是资源上面的损耗,所以,多数情况下,能用 @State@Prop@Link 就不要用 @Provide@Consume 了,除非是跨组件那种的场景。

五、@Observed@ObjectLink

作用@Observed@ObjectLink 装饰器用于在涉及嵌套对象数组元素为对象的场景中进行双向数据同步。

  1. 嵌套对象

     class Person {
       name: string
       age: number
       friend: Person
    
       constructor(name: string, age: number, friend?: Person) {
         this.name = name
         this.age = age
         this.friend = friend
       }
     }
     @Entry
     @Component
     struct Parent {
       @State p: Person = new Person('Xxx', 20, new Person('Yyy', 20))
    
       build() {
         Column() {
           Text(`${this.p.friend.name} : ${this.p.friend.age},`)
             .onClick(() => this.p.friend.age++)
         }
       }
     }

    通过上面这两段代码可以发现 Xxx 这个对象持有了 Yyy 对象,这就是嵌套对象。利用 Text 去渲染 Xxx 的 Friend 的 name 和 age,当发生点击事件时,去修改 Yyy 的 age,但是我们知道嵌套对象它的属性变更是无法被感知到,因此就无法触发视图的更新。要解决这个问题,需要做两件事:

    (1)需要给嵌套对象它所对应的类型上面加上 @Observed 装饰器

     @Observed
     class Person {
       ...
     }

    (2)需要给嵌套对象内部的对象加上 @ObjectLink 装饰器

     @Component
     struct Child {
       @ObjectLink p: Person
    
       build() {
         Column() {
           Text(`${this.p.name} : ${this.p.age}`)
         }
       }
     }
    
     @Entry
     @Component
     struct Parent {
       @State p: Person = new Person('Xxx', 20, new Person('Yyy', 20))
    
       build() {
         Column() {
           Child({ p: this.p.friend })
             .onClick(() => this.p.friend.age++)
         }
       }
     }
  2. 数组元素为对象

     @Observed
     class Person {
       name: string
       age: number
       friend: Person
    
       constructor(name: string, age: number, friend?: Person) {
         this.name = name
         this.age = age
         this.friend = friend
       }
     }
    
     @Component
     struct Child {
       @ObjectLink p: Person
    
       build() {
         Column() {
           Text(`${this.p.name} : ${this.p.age}`)
         }
       }
     }
    
     @Entry
     @Component
     struct Parent {
       @State p: Person = new Person('Xxx', 20, new Person('Yyy', 20))
       @State ps: Person[] = [new Person('Aaa', 20), new Person('Bbb', 20)]
    
       build() {
         Column() {
           Child({ p: this.p.friend })
             .onClick(() => this.p.friend.age++)
           Text('==== 朋友列表 ====')
           ForEach(
             this.ps,
             p => {
               Child({ p: p }).onClick(() => p.age++)
             }
           )
         }
       }
     }

    只要有了 @Observed,然后传递子组件的属性时,加上 @ObjectLink,那么,也能够触发视图的更新了。

  3. 示例代码

     // 任务类
     @Observed
     class Task {
       static id: number = 1
       // 任务名称
       name: string = `任务${Task.id++}`
       // 任务状态
       finished: boolean = false
     }
    
     // 统一的卡片样式
     @Styles function card() {
       .width('95%')
       .padding(20)
       .backgroundColor(Color.White)
       .borderRadius(15)
       .shadow({ radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4 })
     }
    
     // 任务完成样式
     @Extend(Text) function finishedTask() {
       .decoration({ type: TextDecorationType.LineThrough })
       .fontColor('#B2B2B1')
     }
    
     // 任务统计信息
     class StatisticsInfo {
       totalTask: number = 0
       finishTask: number = 0
     }
    
     @Entry
     @Component
     struct PropPage {
       // 任务统计信息
       @Provide info: StatisticsInfo = new StatisticsInfo()
    
       build() {
         Column({ space: 10 }) {
           // 任务进度卡片
           TaskStatistics()
    
           // 任务列表
           TaskList()
         }
         .width('100%')
         .height('100%')
         .backgroundColor('#F1F2F3')
       }
     }
    
     @Component
     struct TaskStatistics {
       @Consume info: StatisticsInfo
    
       build() {
         Row() {
           Text('任务进度:')
             .fontSize(30)
             .fontWeight(FontWeight.Bold)
           Stack() {
             Progress({
               value: this.info.finishTask,
               total: this.info.totalTask,
               type: ProgressType.Ring
             })
               .width(100)
             Row() {
               Text(this.info.finishTask.toString())
                 .fontSize(24)
                 .fontColor('#0000FF')
               Text(' / ' + this.info.totalTask.toString())
                 .fontSize(24)
             }
           }
         }
         .card()
         .margin({ top: 20, bottom: 10 })
         .justifyContent(FlexAlign.SpaceEvenly)
       }
     }
    
     @Component
     struct TaskList {
       @Consume info: StatisticsInfo
       // 任务数组
       @State tasks: Task[] = []
    
       handleTaskChange() {
         // 更新任务总数量
         this.info.totalTask = this.tasks.length
         // 更新已完成任务数量
         this.info.finishTask = this.tasks.filter(item => item.finished).length
       }
    
       build() {
         Column() {
           // 新增任务按钮
           Button('新增任务')
             .width(200)
             .margin({ top: 10, bottom: 10 })
             .onClick(() => {
               // 新增任务数据
               this.tasks.push(new Task())
               // 更新任务总数量
               this.handleTaskChange()
             })
    
           // 任务列表
           List({ space: 10 }) {
             ForEach(
               this.tasks,
               (item: Task, index) => {
                 ListItem() {
                   TaskItem({ item: item, onTaskChange: this.handleTaskChange.bind(this) })
                 }
                 .swipeAction({ end: this.DeleteButton(index) })
               }
             )
           }
           .width('100%')
           .layoutWeight(1)
           .alignListItem(ListItemAlign.Center)
         }
       }
    
       @Builder DeleteButton(index: number) {
         Button() {
           Image($r('app.media.delete'))
             .fillColor(Color.White)
             .width(20)
         }
         .width(40)
         .height(40)
         .type(ButtonType.Circle)
         .backgroundColor(Color.Red)
         .margin(5)
         .onClick(() => {
           this.tasks.splice(index, 1)
           this.handleTaskChange()
         })
       }
     }
    
     @Component
     struct TaskItem {
       @ObjectLink item: Task
       onTaskChange: () => void
    
       build() {
         Row() {
           if (this.item.finished) {
             Text(this.item.name)
               .finishedTask()
           } else {
             Text(this.item.name)
           }
           Checkbox()
             .select(this.item.finished)
             .onChange(val => {
               // 更新当前任务状态
               this.item.finished = val
               // 更新已完成任务数量
               this.onTaskChange()
             })
         }
         .card()
         .justifyContent(FlexAlign.SpaceBetween)
       }
     }
  4. 运行效果,如图2所示:

    图2

  5. 总结

    @Observed@ObjectLink 主要用来解决嵌套对象里面,对象属性变更无法触发数组刷新和数组里的元素式对象属性变更无法触发视图更新的问题。解决方案是给对象上面添加 @Observed 装饰器,同时给嵌套的对象或数组元素对象的变量上加 @ObjectLink 装饰器;当子组件调用父组件方法,我们的办法是把父组件的方法作为参数传递进来,但是传递过程中会有 this 的丢失,解决办法是传递这个函数过程当中,用 bind 把这个 this 绑定进去。

Stage 模型 - 页面及组件生命周期

示例代码:

@Entry
@Component
struct Index {
  @State show: boolean = true

  build() {
    Column() {
      Button('show')
        .onClick(() => {
          this.show = !this.show
        })
      if (this.show) {
        ComponentA()
      }
    }
  }
}

@Component
struct ComponentA {
  build() {
    Row() {
      Text('Component A')
    }
  }
}

如示例代码所示,这是一个 Column 容器,里面有一个 Button 按钮,当点击这个按钮,会修改变量 show 的值,如果 show 为 true,就会渲染 ComponentA。

尽管他俩都是自定义组件,但是他俩的角色是不一样的。ComponentA 是被 Index 引用的,所以可以理解为 ComponentA 是 Index 的子组件,Index 加上了 @entry 装饰器,也就是这个页面的入口组件。

因此,一个页面要加载,首先会加载入口组件 Index,一个组件要加载,先创建组件实例。但是呢,示例创建出来不代表页面就有了,因为组件对应的页面是靠 build 函数绘制的,所以,在组件实例化以后必须执行 build 函数。当 build 函数全部执行完成,页面才算绘制完成。在示例代码中,调用了 ComponentA,也就是说,在绘制的时候用到了别的组件,也就需要把 ComponentA 加载进来。加载 ComponentA 也需要先创建组件实例再去执行 build 函数。在入口组件当中,不管用到了多少个子组件,都必须把子组件实例创建出来,执行子组件 build 函数,再回过头执行入口组件的代码。当 build 函数内所有子组件包括它自己全部加载完成,页面才能真正绘制成功,也就可以展示页面了。但是呢,页面加载出来,用户访问过程中,必然会去做各种各样的操作,不可能一直停留在这个页面,所以,当用户返回跳转新页面,都有可能离开当前页面,当前页面就会从展示页面变成隐藏页面。如果页面隐藏了,页面中组件是隐藏还是销毁呢?不一定,这去取决于当前页面在隐藏后有没有被销毁,假如我们利用 router.pushUrl() 去做跳转,新页面创建出来,会入栈,放到栈顶,原来的栈顶的会被压到栈内部,并没有被销毁,只是隐藏了,这时候,对应的组件还在;如果页面跳转采用 router.replaceUrl(),那么创建的新页面会压入栈中,放入栈顶,原来的栈顶直接销毁,也就会销毁组件,入口组件都销毁了,也就会销毁子组件实例。不仅仅是跳转,在返回时,会把栈顶的页面移除并销毁,然后把紧挨着栈顶的给挪上来,这时候原先栈顶的页面被销毁了,它所对应的组件也就被销毁了。反过来,如果页面还在,是不是说它里面所有组件也一定也在呢?可不一定,示例代码中,Button 按钮点击会修改 show 的值,show 默认是 true,所以对应的 ComponentA 一上来就会被渲染出来。但是如果现在点击按钮,会把 show 从 true 变成 false,ComponentA 就不能再渲染,本来已经渲染好了,现在不再需要,那么这个 ComponentA 会被销毁。所以页面还在,但组件没了。所以,用户在操作的过程中,很有可能会导致部分子组件被销毁,所以子组件有没有被销毁跟页面跟页面并没有必然的联系。

以上就是页面及组件从创建到销毁完整的生命周期。在这个过程中,Stage 模型提供了一些生命周期的钩子,允许我们在其中完成我们想要做的事:

  1. 组件实例创建成功以后,build 函数执行以前,这里呢有一个钩子,叫 aboutToAppear。可以在这里面完成对一些数据的初始化,初始化以后,在 build 函数里就可以利用数据完成渲染。

  2. 在组件被销毁之前还有一个钩子,叫 aboutToDisappear,可以在这里面完成数据持久化等保存操作。

  3. 页面展示之后有一个钩子,叫 onPageShow

  4. 页面隐藏之前有一个钩子,叫 onPageHide

  5. 当用户点击返回上一页时有一个钩子,叫 onBackPress

注:onPageShowonPageHideonBackPress 都属于页面生命周期钩子,而 aboutToAppearaboutToDisappear 则属于组件生命周期钩子。页面展示肯定是从入口组件开始的,因此页面生命周期钩子只能在加了 @Entry 装饰器的入口组件中使用,普通的自定义组件里不能使用。而跟组件生命周期钩子可以在入口组件,也可以在任何普通自定义组件中使用。

如图所示:

总结:

页面和组件的生命周期主要有五个,aboutToAppear 是在组件创建之后然后 build 函数执行之前去触发,往往在这里做一些数据初始化和准备工作,准备好了以后 build 再去执行,就可以利用这些数据完成渲染。然后是页面的生命周期,页面展示出来就会有 onPageShow,页面被隐藏就会有 onPageHide,点击返回就会有 onBackPress,也可以在这三个里面做一些功能性的逻辑。最后是 aboutToDisappear,这个是在组件被销毁时,组件被销毁有可能一些关键性的数据需要去做保存,所以这里面可以做一些数据保存或者资源释放之类的操作。

一篇文章带你了解 Flow

数据流(flow)以协程(coroutines)为基础构建,可提供多个值。数据流使用挂起函数通过异步方式生成和使用值,这就是说,例如,数据流可安全地发出网络请求以生成下一个值,而不会阻塞主线程。

一、为什么需要 Flow

首先我们来回顾下 Kotlin 中我们如何使用挂起函数,我们在 main 方法中,调用挂起函数返回一组数据,代码如下所示:

suspend fun loadData(): List<Int> {
    delay(1000)
    return listOf(1, 2, 3)
}

fun main() {
    runBlocking {
        loadData().forEach { value -> println(value) }
    }
}

运行 main 函数,结果为 1 秒后输出:

1
2
3

那么我们想一下,如果 loadData 中的数据集合,并不是一起返回的,比如从网络中先获取到了 1 再获取到了 2 最后再获取到了 3 ,那么这样如果我们仍然在返回最后一个结果(其实也不知道)时一并返回数据,会造成资源浪费并且用户体验不好,那么我们如何解决这个问题呢?

上面挂起函数的返回类型是 List 类型,那么就必定只能一次性返回数据,此时,Flow 就出场了~

Flow 包含三个实体

  • 提供方(producer)会生成添加到数据流中的数据。得益于协程,数据流还可以异步生成数据。
  • (可选)中介(Intermediaries)可以修改发送到数据流的值,或修正数据流本身。
  • 使用方(consumer)则使用数据流中的值。

二、Flow 的基础使用

  1. 构建器

    我们改写 loadData 方法,返回类型修改为 Flow,并构造一个 flow,在 flow 中,每隔一秒,发送一个数据用来模拟延迟获取值,代码如下所示:

    fun loadData() = flow {
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }
    
    fun main() {
        runBlocking {
            loadData().collect {
                println("collect $it")
            }
        }
    }

    运行结果即是,每隔 1 秒钟,打印出来一个数字:

    emit 1
    collect 1
    emit 2
    collect 2
    emit 3
    collect 3
    

    emit 方法用于发射值,collect 方法是收集值,这里需要注意的是,我们可以看到在 main 方法协程中,我们可以直接调用 loadData 的方法,这是因为 flow 构建块中的代码就是一个 suspend 函数。这样一来我们就实现了对数据的逐步加载,而不需要等待所有的数据返回。

    接下来我们在 main 方法中调用多次 loadData 方法而不调用 collect,看会有什么现象。修改代码如下所示:

    fun loadData() = flow {
        println("进入加载数据的方法")
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }
    
    fun main() {
        runBlocking {
            println("第一次准备调用加载数据的方法")
            val first = loadData()
            println("第二次准备调用加载数据的方法")
            val second = loadData()
            second.collect {
                println("collect $it")
            }
        }
    }

    然后我们运行 main 方法,打印结果如下所示:

    第一次准备调用加载数据的方法
    第二次准备调用加载数据的方法
    进入加载数据的方法
    emit 1
    collect 1
    emit 2
    collect 2
    emit 3
    collect 3
    

    我们会发现,如果我们没有调用 flow 的 collect 方法,其实不会进入 flow 的代码块中,也就是说 flow 中的代码直到被 collect 调用的时候才会运行,否则会立即返回。

  2. Flow 的取消

    如果我们需要定时取消 flow 中代码块的执行,只需要使用 withTimeoutOrNull 函数添加超时时间即可,比如上述方法我们是在 3 秒内返回1、2、3,我们限定其在 2500 毫秒内执行完毕:

    fun loadData() = flow {
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }
    
    fun main() {
        runBlocking {
            withTimeoutOrNull(2500) {
                loadData().collect {
                    println("collect $it")
                }
            }
        }
    }

    我们运行 main 方法,则只有 1、2 两个数字进行了打印:

    emit 1
    collect 1
    emit 2
    collect 2
    

三、Flow 的操作符

  1. map

    使用 map 我们可以将最终结果映射为其他类型,代码如下所示:

    fun loadData() = flow {
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }
    
    fun changeData(value: Int): String {
        return "打印的结果是:$value"
    }
    
    fun main() {
        runBlocking {
            loadData().map {
                changeData(it)
            }.collect {
                println("collect $it")
            }
        }
    }

    我们通过 map 操作符将结果映射为字符串的形式,运行 main 打印结果如下所示:

    emit 1
    collect 打印的结果是:1
    emit 2
    collect 打印的结果是:2
    emit 3
    collect 打印的结果是:3
    
  2. filter

    通过 filter 我们可以对结果集添加过滤条件,如下所示,我们仅打印出大于 1 的值:

    fun loadData() = flow {
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }
    
    fun main() {
        runBlocking {
            loadData().filter {
                it > 1
            }.collect {
                println("collect $it")
            }
        }
    }

    打印结果如下所示:

    emit 1
    emit 2
    collect 2
    emit 3
    collect 3
    
  3. toList

    使用 toList 可以转换为list集合:

    fun loadData() = flow {
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }
    
    fun main() {
        runBlocking {
            val list = loadData().toList()
            println("toList: $list")
        }
    }

    打印结果如下所示:

    emit 1
    emit 2
    emit 3
    toList: [1, 2, 3]
    
  4. reduce

    使用 reduce 可以将所有元素组合到一个单一的结果中:

    fun loadData() = flow {
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }
    
    fun main() {
        runBlocking {
            val data = loadData().reduce { a, b ->
                a + b
            }
            println("data: $data")
        }
    }

    打印结果如下所示:

    emit 1
    emit 2
    emit 3
    data: 6
    
  5. fold

    使用 fold 可以将所有元素组合到一个单一的结果中。和 reduce 不同的是,fold 允许你提供一个初始值作为组合操作的起点:

    fun loadData() = flow {
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }
    
    fun main() {
        runBlocking {
            val data = loadData().fold(10) { a, b ->
                a + b
            }
            println("data: $data")
        }
    }

    打印结果如下所示:

    emit 1
    emit 2
    emit 3
    data: 16
    
  6. flowOn

    flow 的代码块是执行在执行时的上下文中,比如我们不能通过在 flow 中指定线程来运行 flow 代码中的代码,如下所示:

    fun loadData() = flow {
        withContext(Dispatchers.Default) {
            for (i in 1..3) {
                delay(1000)
                println("emit $i")
                emit(i)
            }
        }
    }
    
    fun main() {
        runBlocking {
            loadData().collect {
                println("collect $it")
            }
        }
    }

    此种运行方式,将会抛出异常:

    Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
            Flow was collected in [BlockingCoroutine{Active}@654ef498, BlockingEventLoop@39459354],
            but emission happened in [DispatchedCoroutine{Active}@47445fb2, Dispatchers.Default].
    

    那么我们如何指定 flow 代码块中的上下文呢,我们需要使用 flowOn 操作符,我们将 flow 代码块中的代码指定在 IO 线程中,代码如下所示:

    fun loadData() = flow {
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }.flowOn(Dispatchers.IO)
    
    fun main() {
        runBlocking {
            loadData().collect {
                println("collect $it")
            }
        }
    }

    这样我们就把 flow 代码块中的事情放到了 IO 线程中。

  7. buffer

    协程可以提升并发请求的效率,而在 flow 代码块中,每当有一个处理结果,我们就可以收到,但如果处理结果也是耗时操作,我们来看下需要多长时间来处理,我们在打印前间隔 2 秒,并记录开始和完成的时间,代码如下所示:

    var startTime: Long = 0L
    var endTime: Long = 0L
    
    fun loadData() = flow {
        startTime = System.currentTimeMillis()
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }
    
    fun main() {
        runBlocking {
            loadData().collect {
                delay(2000)
                println("collect $it")
            }
            endTime = System.currentTimeMillis()
            println("处理时间:${endTime - startTime}ms")
        }
    }

    运行 main 方法得到结果如下:

    emit 1
    collect 1
    emit 2
    collect 2
    emit 3
    collect 3
    处理时间:9021ms
    

    我们可以看到,处理三个数据,一共使用了 9 秒钟的时间。

    buffer 操作符可以使发射和收集的代码并发运行,从而提高效率,我们添加 buffer 代码如下所示:

    var startTime: Long = 0L
    var endTime: Long = 0L
    
    fun loadData() = flow {
        startTime = System.currentTimeMillis()
        for (i in 1..3) {
            delay(1000)
            println("emit $i")
            emit(i)
        }
    }
    
    fun main() {
        runBlocking {
            loadData().buffer().collect {
                delay(2000)
                println("collect $it")
            }
            endTime = System.currentTimeMillis()
            println("处理时间:${endTime - startTime}ms")
        }
    }

    再次运行 main 方法,结果如下所示:

    emit 1
    emit 2
    collect 1
    emit 3
    collect 2
    collect 3
    处理时间:7025ms
    

    由此看出,时间较少了将近 2 秒,不要小看这小小的 2 秒,运行在手机上还是相当重要的~

  8. zip

    使用 zip 可以合并两个 flow,代码如下所示:

    fun loadData1() = flow {
        for (i in 1..3) {
            delay(1000)
            emit("data1:$i")
        }
    }
    
    fun loadData2() = flow {
        for (i in 1..3) {
            delay(1000)
            emit("data2: $i")
        }
    }
    
    fun main() {
        runBlocking {
            loadData1().zip(loadData2()) { a, b ->
                "$a, $b"
            }.collect {
                delay(2000)
                println("collect $it")
            }
        }
    }

    运行结果如下所示:

    collect data1:1, data2: 1
    collect data1:2, data2: 2
    collect data1:3, data2: 3
    
  9. combine

    使用 combine 可以将两个或更多的 flow 组合成一个单一的值,代码如下所示:

    fun loadData1() = flow {
        for (i in 1..3) {
            delay(1000)
            emit("data1:$i")
        }
    }
    
    fun loadData2() = flow {
        for (i in 1..3) {
            delay(1000)
            emit("data2: $i")
        }
    }
    
    fun main() {
        runBlocking {
            loadData1().combine(loadData2()) { a, b ->
                "$a, $b"
            }.collect {
                delay(2000)
                println("collect $it")
            }
        }
    }

    运行结果如下所示:

    collect data1:1, data2: 1
    collect data1:3, data2: 2
    collect data1:3, data2: 3
    

    注意,combine 与 zip 不同,zip 会等待每个流发出一个新的值,然后将这两个值组合在一起。而 combine 只要任何一个流发出新的值,就会使用最新的值进行组合。因此,如果两个流的发射速率不同,combine 的结果可能会包含相同的值。

  10. flatMapConcat

    使用 flatMapConcat 可以将每个原始流中的值转换为一个新的流,并将这些新流的值连接到一个单一的结果流中,代码如下所示:

    fun loadData(i: Int) = flow {
        for (j in 1..3) {
            delay(1000)
            emit("data $i: $j")
        }
    }
    
    fun main() {
        runBlocking {
            flowOf(1, 2, 3).flatMapConcat { i ->
                loadData(i)
            }.collect {
                println("collect $it")
            }
        }
    }

    运行结果如下所示:

    collect data 1: 1
    collect data 1: 2
    collect data 1: 3
    collect data 2: 1
    collect data 2: 2
    collect data 2: 3
    collect data 3: 1
    collect data 3: 2
    collect data 3: 3
    
  11. flatMapMerge

    使用 flatMapMerge 可以将每个原始流中的值转换为一个新的流,并将这些新流的值合并到一个单一的结果流中,代码如下所示:

    fun loadData(i: Int) = flow {
        for (j in 1..3) {
            delay(1000)
            emit("data $i: $j")
        }
    }
    
    fun main() {
        runBlocking {
            flowOf(1, 2, 3).flatMapMerge { i ->
                loadData(i)
            }.collect {
                println("collect $it")
            }
        }
    }

    运行结果如下所示:

    collect data 1: 1
    collect data 2: 1
    collect data 3: 1
    collect data 1: 2
    collect data 2: 2
    collect data 3: 2
    collect data 1: 3
    collect data 2: 3
    collect data 3: 3
    

ArkUI - Image、Text、TextInput、Button、Slider 基本用法

一、Image - 图片显示组件

  1. 声明 Image 组件并设置图片源

    Image(src: string | PixelMap | Resource)
    

    src:图片源。是一个联合类型(Union Types),支持三种格式

    • string 格式,通常用来加载网络图片

      Image('https://xxx.png')
      

      注:使用 Stage 模型的应用,需要在 module.json5 文件中声明网络访问权限

      "requestPermissions": [
       {
          "name": 'ohos.permission.INTERNET'
       }
      ]
      
    • PixelMap 格式,可以加载像素图,常用在图片编辑中

      Image(pixelMapObject)
      
    • Resource 格式,加载本地图片

      Image($r('app.media.icon')) -> 读取 media 目录里的图片
      
      或
      
      Image($rawfile('icon.png')) -> 读取 rawfile 目录里的图片
      
  2. 添加图片属性

    • 设置组件通用属性:

      Image($r('app.media.icon'))
        .width(100)                             // 设置宽度
        .height(100)                            // 设置高度
        .borderRadius(40)                       // 边框圆角
      
    • 设置组件特有属性:

      Image($r('app.media.icon'))
        .interpolation(ImageInterpolation.High) // 图片插值
      
      图片插值的作用:
      有些低分辨率的图片一旦放大以后就会出现锯齿,看起来不清晰。设置图片插值,就会弥补图片的锯齿,视觉效果是看起来清晰度变高了。
      

二、Text - 文本显示组件

  1. 声明 Text 组件并设置文本内容

    Text(content?: string | Resource)
    

    content:展示的文本内容。是一个联合类型(Union Types),支持两种格式

    • string 格式,直接填写文本内容

      Text('文字内容')
      
    • Resource 格式,读取本地资源文件

      Text($r('app.string.module_desc')) -> 读取 element 目录里的 string.json
      
  2. 添加文本属性

    Text('一段文字')
      .lineHeight(32)              // 行高
      .fontSize(20)                // 字体大小
      .fontColor('#FF00FF')        // 字体颜色
      .fontWeight(FontWeight.Bold) // 字体粗细
    

三、TextInput - 文本输入框组件

  1. 声明 TextInput 组件:

    TextInput({ placeholder?:ResourceStr, text?:ResourceStr })
    
    • placeholder:输入框无输入时的提示文本
      TextInput({ placeholder: '无输入时的提示文本' })
      
    • text:输入框当前的文本内容
      TextInput({ text: '当前的文本内容' })
      
  2. 添加属性和事件

    • 设置组件通用属性:

      TextInput({ text: '当前的文本内容' })
        .width(200)                      // 宽
        .height(50)                      // 高
        .backgroundColor('FFF')          // 背景色
      
    • 设置组件特有属性:

      TextInput({ text: '当前的文本内容' })
        .type(InputType.PhoneNumber)  // 输入框类型
        .onChange(value => {
          // value 是用户输入的文本内容
        })
      

      输入框类型 type:

      名称 类型
      Normal 基本输入模式。支持输入数字、字母、下划线、空格、特殊字符。
      Number 纯数字输入模式。
      PhoneNumber 电话号码输入模式。支持输入数字、+、-、*、#,长度不限。
      Email 邮箱地址输入模式。支持数字、字母、下划线、以及@字符。
      Password 密码输入模式。支持输入数字、字母、下划线、空格、特殊字符。

      事件方法 onChange:

      当用户在文本输入框输入内容发生变化以后触发的事件,value 就是最新的文本内容。

四、Button - 按钮组件

  1. 声明 Button 组件:

    Button(label? : ResourceStr)
    

    label:是按钮上面要显示的文字,可选参数,可以传也可以不传

    • 如果参数,就是一个文字型按钮

      Button('按钮')
      
    • 如果不传参数,就没有文字描述,就需要给按钮内部嵌套其他组件,就是一个自定义按钮

      Button() {
        Image($r('app.media.icon')).width(20).margin(10)
      }
      
  2. 添加属性和事件

    • 设置组件通用属性:

      Button('按钮')
        .width(100)              // 宽
        .height(30)              // 高
      
    • 设置组件特有属性:

      Button('按钮')
        .type(ButtonType.Normal) // 按钮类型
        .onClick(() => {
          // 处理点击事件
        })
      

      按钮类型 type:

      名称 描述
      Capsule 胶囊型按钮(圆角默认为高度的一半)。
      Circle 圆形按钮。
      Normal 普通按钮(默认不带圆角)。

      事件方法 onClick:

      当按钮点击时会触发 onClick 事件

五、Slider - 滑动条组件

  1. 声明 Slider 组件:

    Slider(options?: SliderOptions)
    

    options:滑动条配置属性

  2. 添加属性和事件

    Slider({
      min: 0,                     // 最小值
      max: 100,                   // 最大值
      value:30,                   // 当前值
      step:10,                    // 滑动步长
      style:SliderStyle.OutSet,   // 样式
      direction:Axis.Horizontal,  // 方向
      reverse:false               // 是否反向滑动
    })
    
    • step:

      滑动步长。每滑动一次 value 会增加或减少的值,默认值是 1。
    • style:

      滑动条的样式。

      默认值是 Outset,也就是滑块在滑动条的外面。

      InSet,滑块在滑动条的里面。
    • direction:

      滑动条的方向。

      默认值是 Horizontal,水平方向。

      Vertical,垂直方向。(默认情况下,最下面是最大值,最上面是最小值)
  • 设置组件通用属性:

    Slider()
      .width('90%') // 宽度
    
  • 设置组件特有属性:

    Slider()
      .trackThickness(10)     // 滑轨粗细
      .showTips(true)         // 是否展示 value 百分比提示
      .blockColor('#000')     // 滑块颜色
      .onChange(value => {
        // value 就是当前滑块值
      })
    

    事件方法 onChange:

    滑块在滑动的过程中,它所对应的值在变更,每一次值变更就会触发 onChange 事件,value 就是当前滑块对应的值。

Stage 模型 - UIAbility 的启动模式

Stage 模型这样的应用,它在启动的时候会先准备 Ability Stage 舞台,接着呢,就可以基于它去创建 UIAbility 的实例,并去启动它。

UIAbility 组件启动模式 有四种:

  • singleton
  • standard
  • multiton
  • specified

修改模块的 module.json5 来改变启动模式:

{
  "module": {
    ...

    "abilities": [
      {
        ...

        "launchType": "singleton",
        
        ...
      }
    ]
  }
}

一、singleton 启动模式

👉 官方文档

singleton 是单实例的意思,所以这种模式对应的 UIAbility 不管你给它启动多少次,它只会存在唯一的实例。事实上,我们的应用默认 Ability 都是这种模式。

UIAbility 的实例其实对应到操作系统任务列表中的一个任务。所以,如果你的 Ability 是 singleton 这种模式,不管启动多少次,去查看手机的任务列表,会发现这个 Ability 在任务列表中只会存在一个任务。


运行日志:

  1. 点击图标启动 app:

    Ability onCreate
    Ability onWindowStageCreate
    Ability onForeground
    
  2. 点击 Home 回到桌面:

    Ability onBackground
    
  3. 再次点击图标启动 app:

    Ability onForeground
    

二、standard、multiton 启动模式

👉 官方文档

在官网上管 standard 模式叫 multiton 模式,但是经过测试,这两种模式是不一样的。但是这两个模式又非常接近,这两种模式在每次启动 UIAbility 时都会创建一个新实例。

在 standard 模式当中,创建一个新的实例,之前那个旧的实例也会存在,也就是多个实例并存;而 multiton 模式则不一样,它在每次创建一个新实例,旧的实例则会被移除。

对于 standard 模式来讲,每创建一个新的实例,都会存在,那是不是意味着同一个 Ability 它的多个实例是并存的。因此在任务列表中会发现一个 Ability 可能会存在一个或多个任务。


multiton 运行日志:

  1. 点击图标启动 app:

    Ability onCreate
    Ability onWindowStageCreate
    Ability onForeground
    
  2. 点击 Home 回到桌面:

    Ability onBackground
    
  3. 再次点击图标启动 app:

    Ability onCreate
    Ability onWindowStageCreate
    Ability onForeground
    
  4. 点击任务列表

    发现 App 只存在一个实例
    

standard 运行日志:

  1. 点击图标启动 app:

    Ability onCreate
    Ability onWindowStageCreate
    Ability onForeground
    
  2. 点击 Home 回到桌面:

    Ability onBackground
    
  3. 再次点击图标启动 app:

    Ability onCreate
    Ability onWindowStageCreate
    Ability onForeground
    
  4. 点击任务列表

    发现 App 存在两个实例
    

三、specified 启动模式

👉 官方文档

specified 顾名思义就是指定的意思,在使用这种模式时,启动一个 UIAbility 时是需要指定一个 key,这个 key 会作为 UIAbility 实例的一个唯一标识。

所以,当启动时,会先看一下指定的这个 key 对应的 UIAbility 是否存在。如果不存在,就会创建一个新的 UIAbility 实例,然后把这个 key 作为这个实例的标识,下次再启动 UIAbility 时,还需要指定 key,还需要判断这个 key 对应的实例是否已经存在。如果已经存在,就可以直接把它唤醒,不用重新创建。

这种模式和 standard 模式有点像,就是一个 Ability 会存在多个实例,但是呢又不一样,在创建实例时可以指定 key,key 不存在才需要创建,key 存在就不用重复创建了,这样就可以快速的找到以前创建好的实例。

当前 UIAbility 调用 startAbility 方法拉起目标 UIAbility:

// 获取上下文
context = getContext(this) as common.UIAbilityContext

// 指定要跳转到 UIAbility 的信息
let want = {
    deviceId: '',
    bundleName: 'com.tyhoo.hmos.myapplication',
    abilityName: 'TestAbility',
    moduleName: 'entry',
    parameters: {
      instanceKey: this.getInstanceKey()
    }
}

// 尝试拉起目标 UIAbility 实例
this.context.startAbility(want)
  • 调用一个全局的 getContext 方法,来获取 UIAbilityContext 的上下文对象。

  • 拿到上下文之后,可以调用它的 startAbility 方法,从而来去拉起一个目标的 UIAbility,或者说把它的实例给创建好。

  • 传入参数 want 来告诉具体拉起哪个实例。

    • deviceId:设备信息。如果什么都不传,代表的是本设备。
    • bundleName:包名/应用名。它是设备的唯一标识。
    • abilityName:目标 UIAbility 的名称。
    • moduleName:模块名。调用的 Ability 在哪个模块。
    • parameters:参数。

在 AbilityStage 的生命周期回调中为目标 UIAbility 实例生成 key:

export default class MyAbilityStage extends AbilityStage {
  onAcceptWant(want: Want): string {
    if (want.abilityName === 'TestAbility') {
      return `TestAbility_${want.parameters.instanceKey}`
    }
    return '';
  }
}

在 module.json5 配置文件中,通过 srcEntry 参数指定 AbilityStage 路径:

{
  "module": {
    ...

    "srcEntry": "./ets/myabilitystage/MyAbilityStage.ts"
  }
}

Stage 模型 - 基本概念

一、项目结构

如图1所示:

图1

从项目结构来看,这个应用的内部包含了一个子模块叫 entry,模块是应用的基本功能单元,它里面包含源代码、资源、配置文件等。

像这样的模块在应用内部可以创建很多。但模块整体来讲就分成两大类:

  • 第一类就像 entry 这样,它的内部其实是开发这个应用内的一些特殊能力的,像这样的模块,我们称之为 Ability Module,顾名思义就是能力模块。一个应用的内部它的能力有很多,我们就可以把不同的能力放到不同的模块里去开发。举个例子,支付宝,它的核心功能是支付,像我们熟悉的付款、转账等都属于是支付类的,这部分能力我们就可以把它们放到一个模块里。后来随着支付宝的发展,又增加了其他的能力,比如小程序、视频、看书等,这些能力相互之间都是独立的,所以它们也都可以放到独立的 Ability Module 里去开发。这样一来整个应用的能力就被清晰的划分出来,管理起来也非常的方便。

  • 第二类是在开发的过程肯定有一些通用的工具、资源、配置或者组件等,这些如果每一个模块都各自去开发显然是一种重复和浪费,所以我们就可以把这些通用的东西抽取起来,放到一个单独的模块里去,这样的模块我们称之为 Library Module,顾名思义就是一种共享依赖类型的模块。Ability 类型的 Module 就可以去引用 Library 类型的 Module。

如图2所示:

图2

二、项目编译、打包

源码将来肯定要去编译、打包,最终整个项目会被编译成一个 APP,也就是我们熟悉的那个安装包。只不过,在 Stage 模型里面,为了降低不同功能模块之间的耦合,事实上每一个模块都是可以去独立编译和运行的。所有的 Ability 类型的模块,将来就会被编译成 HAP 文件,而所有的 Library 类型的模块,则会被编译成 HSP 文件。所谓的 HAPHarmony Ability Package,也就是鸿蒙能力类型的包。而 HSP 则是 Harmony Shared Package,也就是鸿蒙共享类型的包。HAP 的包在运行的过程中就可以去引用和依赖 HSP 的包。

一个应用内部可能会包含多个不同的能力,就会分成很多的 Ability Module,因此往往就会有多个 HAP 的文件。但是,尽管都是 HAP 类型的文件,但是它们是有差异的,比如 entry 模块,entry 是项目的入口,在这个能力模块里面,主要开发的是项目入口界面等信息,还有就是整个项目或应用的主能力模块,也就是最核心的部分。剩下的一些特殊的能力模块,比如小程序、视频、看书等,这些能力模块,都是拓展功能。一个应用它的入口只能有一个,所以说如果我们的应用内部有很多的 HAP 文件,但是作为入口的 HAP 文件只有一个,那么这个 HAP 文件就叫 Entry 类型的 HAP,剩下的拓展功能就叫 Feature 类型的 HAP。所以,一个应用内部有且只有一个 Entry 类型的 HAP,但是 Feature 类型的 HAP 可以有多个。

HAP 有很多很多文件,将来最终要合并在一起。合并在一起之后称之为 Bundle。这个 Bundle 有一个自己的名字也就是 Bundle Name,这个 Name 可以理解成整个应用的唯一标识,将来整个 Bundle 最后合并打包会变成一个 APP。

如图3所示:

图3

之所以要采用这种多 HAP 文件打包模式,是因为:

  1. 降低不同模块之间的耦合,每个 HAP 都可以去独立编译和运行。
  2. 我们的应用在去下载安装时,可以选择性的安装,不是说一上来就把全部都安装好了,比如我们可以先安装它的核心模块 Entry,其它的 Feature 可以选择性安装。这样就可以降低应用安装时所占用的体积。

三、项目运行

每一个 HAP 都是可以独立运行的,而 HAP 在运行的时候,为了展示我们看到的界面和它的能力,都会创建一个名为 AbilityStage 的实例,我们称它为应用组件的“舞台”。为啥叫舞台呢?舞台肯定是在上面展示东西的,顾名思义展示的就是应用组件。

应用能力组件又有很多类型:

  • UIAbility:包含UI界面的应用组件,是系统调度的基本单元。
  • ExtensionAbility:拓展能力组件,例如在桌面展示的应用卡片等。
  • ...

UIAbility 作为UI界面的组件,将来要展示UI界面,需要注意,它不是直接展示,首先会持有一个 WindowStage 的实例对象,WindowStage 跟 AbilityStage 类似,都是一个舞台,但是 WindowStage 是组件内窗口的舞台,也就是说UI组件内部先要有一个窗口(这个窗口是要展示在舞台上),所以 WindowStage 它的上面就会持有 Window 对象,也就是窗口,窗口里面展示我们的UI界面。因此这个 Window 就是用来绘制UI页面的窗口。

如图4所示:

图4

综上所述,Stage 模型采用的是舞台的机制,先来一个 AbilityStage(用来展示这些组件的舞台),然后在舞台上创建 Ability,这个组件将来要去渲染UI,它要持有一个窗口的舞台(WindowStage),在窗口的舞台上有一个窗口(Window),接着呢就可以在窗口里绘制页面。所以正是因为有很多很多的舞台,在舞台上展示东西,这种模型就被称之为 Stage 模型

为什么 Stage 模型要用这种舞台机制呢?正是由于存在这种舞台机制,Ability 和 Window 就分隔开,它们之间就解耦了,将来在开发跨设备的应用时,就可以针对不同的设备对于窗口进行单独的裁剪,去适应不同的设备。

HUAWEI DevEco Studio 下载地址汇总

目录


说明

  • Full SDK:面向OEM厂商提供,包含了需要使用系统权限的系统接口。

  • Public SDK:面向应用开发者提供,不包含需要使用系统权限的系统接口。

  • HUAWEI DevEco Studio:OpenHarmony 应用开发推荐使用。

  • HUAWEI DevEco Device Tool:OpenHarmony 智能设备集成开发环境推荐使用。


5.0-Beta1

软件 版本 备注
OpenHarmony 5.0-Beta1
Public SDK Ohos_sdk_public 5.0.3.405
(API Version 12 Beta1)
HUAWEI DevEco Studio
Windows 64位
5.0-Beta1 下载地址
HUAWEI DevEco Studio
macOS(Intel)
5.0-Beta1 下载地址
HUAWEI DevEco Studio
macOS(Apple Silicon)
5.0-Beta1 下载地址
HUAWEI DevEco Device Tool

备注:官方链接

4.1 Release

软件 版本 备注
OpenHarmony 4.1 Release
Public SDK Ohos_sdk_public 4.1.7.5
(API Version 11 Release)
HUAWEI DevEco Studio
Windows 64位
4.1 Release 下载地址
HUAWEI DevEco Studio
macOS(Intel)
4.1 Release 下载地址
HUAWEI DevEco Studio
macOS(Apple Silicon)
4.1 Release 下载地址
HUAWEI DevEco Device Tool 4.0 Release 获取地址

备注:官方链接

4.0 Release

软件 版本 备注
OpenHarmony 4.0 Release
Public SDK Ohos_sdk_public 4.0.10.13
(API Version 10 Release)
HUAWEI DevEco Studio
Windows 64位
4.0 Release 下载地址
HUAWEI DevEco Studio
macOS(Intel)
4.0 Release 下载地址
HUAWEI DevEco Studio
macOS(Apple Silicon)
4.0 Release 下载地址
HUAWEI DevEco Device Tool 4.0 Release 获取地址

备注:官方链接

3.2.1 Release

软件 版本 备注
OpenHarmony 3.2.1 Release
Public SDK Ohos_sdk_public 3.2.12.5
(API Version 9 Release)
HUAWEI DevEco Studio
Windows 64位
3.1 Release 下载地址
HUAWEI DevEco Studio
macOS(Intel)
3.1 Release 下载地址
HUAWEI DevEco Studio
macOS(Apple Silicon)
3.1 Release 下载地址
HUAWEI DevEco Device Tool 3.1 Release 获取地址

备注:官方链接

3.2 Release

软件 版本 备注
OpenHarmony 3.2 Release
Public SDK Ohos_sdk_public 3.2.11.9
(API Version 9 Release)
HUAWEI DevEco Studio
Windows 64位
3.1 Release 下载地址
HUAWEI DevEco Studio
macOS(Intel)
3.1 Release 下载地址
HUAWEI DevEco Studio
macOS(Apple Silicon)
3.1 Release 下载地址
HUAWEI DevEco Device Tool 3.1 Release 获取地址

备注:官方链接

3.1.3 Release

软件 版本
OpenHarmony 3.1.3 Release
Full SDK Ohos_sdk_full 3.1.7.7
(API Version 8 Release)
Public SDK Ohos_sdk_full 3.1.7.7
(API Version 8 Release)
HUAWEI DevEco Studio 3.0 Release for OpenHarmony
HUAWEI DevEco Device Tool 3.0 Release

备注:官方链接

3.1.2 Release

软件 版本
OpenHarmony 3.1.2 Release
Full SDK Ohos_sdk_full 3.1.7.7 (API Version 8 Relese)
Ohos_sdk_full 3.1.7.5 (API Version 8 Relese)
Public SDK Ohos_sdk_public 3.1.7.7 (API Version 8 Release)
Ohos_sdk_public 3.1.7.5 (API Version 8 Release)
HUAWEI DevEco Studio 3.0 Beta4 for OpenHarmony
HUAWEI DevEco Device Tool 3.0 Release

备注:官方链接

3.1.1 Release

软件 版本
OpenHarmony 3.1.1 Release
Full SDK Ohos_sdk_full 3.1.6.5
(API Version 8 Release)
Public SDK Ohos_sdk_public 3.1.6.6
(API Version 8 Release)
HUAWEI DevEco Studio 3.0 Beta3 for OpenHarmony
HUAWEI DevEco Device Tool 3.0 Release

备注:官方链接

3.1 Release

软件 版本
OpenHarmony 3.1 Release
SDK Ohos_sdk 3.1 Release (API Version 8 Release)
Ohos_sdk 3.2 Canary (API Version 9 Canary)
HUAWEI DevEco Studio 3.0 Beta3 for OpenHarmony
HUAWEI DevEco Device Tool 3.0 Release

备注:官方链接

TCP 的三次握手与四次挥手

通常我们进行 HTTP 连接网络的时候会进行 TCP 的三次握手,然后传输数据,之后再释放连接。

TCP 传输如图1所示:

图1 TCP 传输

TCP三次握手的过程如下:

  • 第一次握手:建立连接。客户端发送连接请求报文段,将 SYN 设置为 1、Sequence Number(seq)为 x;接下来客户端进入 SYN_SENT 状态,等待服务端的确认。

  • 第二次握手:服务器收到客户端的 SYN 报文段,对 SYN 报文段进行确认,设置 Acknowledgment Number(ACK)为 x+1(seq + 1);同时自己还要发送 SYN 请求信息,将 SYN 设置为 1、seq 为 y。服务端将上述所有信息放到 SYN + ACK 报文段中,一并发送给客户端,此时服务端进入 SYN_RCVD 状态。

  • 第三次握手:客户端收到服务端的 SYN + ACK 报文段;然后将 ACK 设置为 y + 1,向服务端发送 ACK 报文段,这个报文段发送完毕后,客户端和服务端都进入 ESTABLISHED (TCP 连接成功)状态,完成 TCP 的三次握手。

当客户端和服务端通过三次握手建立了 TCP 连接以后,当数据传送完毕,断开连接时就需要进行 TCP 的四次挥手。其四次挥手如下所示:

  • 第一次挥手:客户端设置 seq 和 ACK,向服务端发送一个 FIN 报文段。此时,客户端进入 FIN_WAIT_1 状态,表示客户端没有数据要发送给服务端了。

  • 第二次挥手:服务端收到了客户端发送的 FIN 报文段,向客户端回了一个 ACK 报文段。

  • 第三次挥手:服务端向客户端发送 FIN 报文段,请求关闭连接,同时服务端进入 LAST_ACK 状态。

  • 第四次挥手:客户端收到服务端发送的 FIN 报文段,向服务端发送 ACK 报文段,然后客户端进入 TIME_WAIT 状态。服务端收到客户端的 ACK 报文段以后,就关闭连接。此时,客户端等待 2MSL(最大报文段生存时间)后依然没有收到回复,则说明服务端已正常关闭,这样客户端也可以关闭连接了。

现在看图2来加强一下理解:

图2 三次握手与四次挥手

如果有大量的连接,每次在连接、关闭时都要经历三次握手、四次挥手,这很显然会造成性能低下。因此,HTTP 有一种叫作 keepalive connections 的机制,它可以在传输数据后仍然保持连接,当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而无须再次握手,如图3所示:

图3 连接复用

在 Android 应用中调用 C++ 代码并在新线程中执行 Java 静态方法

本文将通过一个使用 Kotlin 和 C++ 编写的 Android 应用案例,展示如何利用 JNI 实现 Java 层与本地 C++ 代码的交互。

Kotlin 代码:

package com.tyhoo.jni

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.tyhoo.jni.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.sampleText.setOnClickListener {
            Log.d(TAG, "nativeMethod")
            nativeMethod()
        }
    }

    private external fun nativeMethod()

    companion object {
        private const val TAG = "Tyhoo"

        // Used to load the 'jni' library on application startup.
        init {
            System.loadLibrary("jni")
        }

        @JvmStatic
        fun staticMethod() {
            Log.d(TAG, "staticMethod")
        }
    }
}

这是 Kotlin 语言编写的 Android 应用程序中的 MainActivity 类,其中声明了一个名为 nativeMethod 的本地方法。该方法会在点击 sampleText 视图时被调用,从而触发 native 方法的执行。在 companion object 中还定义了一个名为 staticMethod 的静态方法,它会在应用程序启动时被加载,用于在 Java 层调用本地方法。这个静态方法可以从 Java 代码中直接调用,而不需要使用 JNI 接口函数。

C++ 代码:

#include <jni.h>
#include <string>
#include <android/log.h>

#define LOG_TAG "Tyhoo"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

#define CLASSNAME "com/tyhoo/jni/MainActivity"

typedef struct {
    JavaVM *vm;
    jclass clazz;
} tyhoo_vm;

static tyhoo_vm javaVM;

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *unused) {
    JNIEnv *env = nullptr;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        LOGD("JNI_OnLoad GetEnv failed\n");
        return -1;
    }

    jclass clazz = env->FindClass(CLASSNAME);
    if (!clazz) {
        LOGD("FindClass %s failed\n", CLASSNAME);
        return -1;
    }

    javaVM.vm = vm;
    javaVM.clazz = (jclass) env->NewGlobalRef(clazz);

    LOGD("JNI_OnLoad, vm: %p, clazz: %p \n", javaVM.vm, javaVM.clazz);
    return JNI_VERSION_1_6;
}

void JNI_OnUnload(JavaVM *vm, void *unused) {
    JNIEnv *env = nullptr;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        LOGD("JNI_OnLoad GetEnv failed\n");
        return;
    }
    env->DeleteGlobalRef(javaVM.clazz);
}

void callJavaStaticMethod() {
    JNIEnv *env = nullptr;
    jint res = javaVM.vm->AttachCurrentThread(&env, nullptr);
    if (res != JNI_OK) {
        return;
    }

    jclass clazz = javaVM.clazz;
    if (clazz == nullptr) {
        return;
    }

    jmethodID method = env->GetStaticMethodID(clazz, "staticMethod", "()V");
    env->CallStaticVoidMethod(clazz, method);
}

void *thread_func(void *arg) {
    callJavaStaticMethod();
    return nullptr;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_tyhoo_jni_MainActivity_nativeMethod(JNIEnv *env, jobject thiz) {
    pthread_t thread;
    pthread_create(&thread, nullptr, thread_func, nullptr);
}

这是 C++ 语言编写的 JNI 接口函数,用于在 Java 层调用本地方法。这个接口函数的作用是创建新线程,并在新线程中调用 callJavaStaticMethod 方法,这个方法会获取当前线程的 JNIEnv 对象和 Java 类对象,并通过这些对象调用 Java 层的静态方法 staticMethod。

探索 Kotlin 中的回调机制:函数类型、高阶函数和接口

当在 Kotlin 中开发应用程序时,经常会遇到需要使用回调机制的情况。回调是一种常见的编程模式,用于在异步操作完成后通知调用方,并处理相应的结果或事件。在 Kotlin 中,有几种不同的方法可以实现回调,包括使用函数类型、高阶函数和接口。每种方法都有其优点和适用场景,因此在选择适当的回调机制时,了解它们的特点是非常重要的。

一、使用函数类型(Function Types)

  1. 定义函数类型

    typealias Callback = (result: String) -> Unit
  2. 接受回调函数的函数

    fun performTask(callback: Callback) {
        // 执行任务
        val result = "Task completed"
    
        // 调用回调函数
        callback(result)
    }
  3. 调用 performTask() 并传递回调函数

    performTask { result ->
        // 在回调函数中处理结果
        println(result)
    }
  4. 说明

    在上面的示例中,我们首先使用 typealias 声明了一个函数类型 Callback,它接受一个 String 参数并返回 Unit。

    然后,我们定义了一个名为 performTask 的函数,它接受一个 Callback 参数,并在执行完任务后调用该回调函数。

    最后,我们通过 lambda 表达式传递一个匿名函数作为回调函数,并在 lambda 表达式中处理回调的结果。

二、使用高阶函数(Higher-Order Functions)

  1. 定义高阶函数

    fun performTask(callback: (result: String) -> Unit) {
        // 执行任务
        val result = "Task completed"
    
        // 调用回调函数
        callback(result)
    }
  2. 定义回调函数

    fun handleResult(result: String) {
        // 在回调函数中处理结果
        println(result)
    }
  3. 调用 performTask() 并传递回调函数

    performTask(::handleResult)
  4. 说明

    在上面的示例中,我们定义了一个名为 performTask 的高阶函数,它接受一个函数类型参数作为回调。

    然后,我们定义了一个名为 handleResult 的函数,它接受一个 String 参数,并在回调函数中处理结果。

    最后,我们通过 :: 运算符将 handleResult 函数作为回调函数传递给 performTask 函数。

三、使用接口(interface)

  1. 定义回调接口

    interface Callback {
        fun onResult(result: String)
    }
  2. 实现回调接口的类

    class TaskPerformer {
        fun performTask(callback: Callback) {
            // 执行任务
            val result = "Task completed"
    
            // 调用回调方法
            callback.onResult(result)
        }
    }
  3. 使用回调接口

    val performer = TaskPerformer()
    performer.performTask(object : Callback {
        override fun onResult(result: String) {
            // 处理回调结果
            println(result)
        }
    })
  4. 说明

    在上面的示例中,首先我们定义了一个回调接口 Callback,其中包含了一个名为 onResult 的抽象方法。

    然后,我们创建了一个类 TaskPerformer,它包含了一个 performTask 方法,该方法接受一个 Callback 参数,并在执行完任务后调用回调方法。

    最后,我们通过创建匿名内部类(anonymous inner class)的方式实现了 Callback 接口,并在其中重写了 onResult 方法来处理回调结果。我们创建了 TaskPerformer 的实例,并调用其 performTask 方法,传递了实现了 Callback 接口的匿名内部类对象作为参数。

    这样,当任务完成时,TaskPerformer 类会调用传递给它的回调对象的 onResult 方法,并将结果传递给回调函数,从而实现回调机制。

四、总结(优缺点和适用场景)

  1. 使用函数类型(Function Types)

    • 优点

      • 简洁:不需要定义接口和实现类,可以直接使用函数类型作为参数。
      • 灵活性:可以使用 lambda 表达式传递匿名函数作为回调,使代码更为紧凑。
    • 缺点

      • 可读性较差:相比于接口,函数类型可能不够明确,使得代码在阅读时不够清晰。
      • 可重用性差:如果多个地方需要相同的回调逻辑,需要重复定义函数类型,可能引发代码重复。
    • 适用场景

      • 当回调逻辑相对简单且只在一个或少数几个地方使用时,使用函数类型可以提供更简洁的代码。
  2. 使用高阶函数(Higher-Order Functions)

    • 优点

      • 灵活性:高阶函数可以接受任意函数作为参数,使得回调的实现更加灵活。
      • 可读性较好:相比于函数类型,高阶函数的语法和用途更为明确,更易于理解。
    • 缺点

      • 可重用性较差:与函数类型相似,如果多个地方需要相同的回调逻辑,需要重复定义高阶函数,可能引发代码重复。
    • 适用场景

      • 当回调逻辑较为复杂或需要在多个地方重复使用时,使用高阶函数可以提供更灵活和可读性较好的代码。
  3. 使用接口(interface)

    • 优点

      • 契约明确:接口提供了明确的契约,使得代码在阅读和维护时更易理解。
      • 可重用性强:可以定义一个接口,并在多个地方实现该接口,实现回调逻辑的复用。
    • 缺点

      • 冗余代码:需要定义接口以及实现类,相对于函数类型和高阶函数,需要更多的代码量。
      • 静态类型限制:接口需要在编译时确定,不够灵活,难以适应一些动态的回调需求。
    • 适用场景

      • 当回调逻辑较为复杂,需要在多个地方重复使用,并且需要明确的契约和类型安全时,使用接口是最合适的选择。
  4. 综上所述

    选择合适的回调机制取决于具体的需求和代码结构。函数类型和高阶函数适用于简单的回调逻辑或需要灵活性的场景,而接口适用于复杂的回调逻辑、需要明确契约和类型安全的场景,并且能够实现回调的复用。

线程的状态

Java 线程在运行的声明周期中可能会处于6种不同的状态,这6种线程状态分别为如下所示:

  • New:新创建状态。线程被创建,还没有调用 start 方法,在线程运行之前还有一些基础工作要做。

  • Runnable:可运行状态。一旦调用 start 方法,线程就处于 Runnable 状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。

  • Blocked:阻塞状态。表示线程被锁阻塞,它暂时不活动。

  • Waiting:等待状态。线程暂时不活动,并且不运行任何代码,这消耗最少的资源,直到线程调度器重新激活它。

  • Timed waiting:超时等待状态。和等待状态不同的是,它是可以在指定的时间自行返回的。

  • Terminated:终止状态。表示当前线程已经执行完毕。导致线程终止有两种情况:第一种就是 run 方法执行完毕正常退出;第二种就是因为一个没有捕获的异常而终止了 run 方法,导致线程进入终止状态。

线程的状态如图所示:

线程创建后,调用 Thread 的 start 方法,开始进入运行状态,当线程执行 wait 方法后,线程进入等待状态,进入等待状态的线程需要其他线程通知才能返回运行状态。超时等待相当于在等待状态加上了时间限制,如果超过时间限制,则线程返回运行状态。当线程调用到同步方法时,如果线程没有获得锁则进入阻塞状态,当阻塞状态的线程获取到锁时则重新回到运行状态。当线程执行完毕或者遇到意外异常终止时,都会进入终止状态。

坐标系

Android 系统中有两种坐标系,分别为 Android 坐标系和 View 坐标系。了解这两种坐标系能够帮助我们实现 View 的各种操作,比如我们要实现 View 的滑动,你连这个 View 的位置都不知道,那如何去操作呢?

一、Android 坐标系

在 Android 中,将屏幕左上角的顶点作为 Android 坐标系的原点,这个原点向右是 X 轴正方向,向下是 Y 轴正方向,如图1所示。另外在触控事件中,使用 getRawX() 和 getRawY() 方法获得的坐标也是 Android 坐标系的坐标。

图1 Android 坐标系

二、View 坐标系

View 坐标系与 Android 坐标系并不冲突,两者是共同存在的,它们一起来帮助开发者更好地控制 View。对于 View 坐标系,我们只需要搞明白图2中提供的信息就好了。

图2 View 坐标系
  1. View 获取自身的宽和高

    根据图2可以得到很多结论,首先我们能算出 View 的宽和高:

    width = getRight() - getLeft()
    height = getBottom() - getTop()

    当然这样做显然有些麻烦,因为系统已经向我们提供了获取 View 宽和高的方法。getWidth() 用来获取 View 自身的宽度,getHeight() 用来获取 View 自身的高度。从 View 的源码来看,getWidth() 和 getHeight() 获取 View 自身的宽度和高度的算法与上面从图2中得出的结论是一致的。View 源码中的 getWidth() 方法和 getHeight() 方法如下所示:

    public final int getWidth() {
        return mRight - mLeft;
    }
    
    public final int getHeight() {
        return mBottom - mTop;
    }
  2. View 自身的坐标

    通过如下方法可以获得 View 到其父控件(ViewGroup)的距离。

    • getTop():获取View自身顶边到其父布局顶边的距离。
    • getLeft():获取View自身左边到其父布局左边的距离。
    • getRight():获取View自身右边到其父布局左边的距离。
    • getBottom():获取View自身底边到其父布局顶边的距离。
  3. MotionEvent 提供的方法

    图2中的那个蓝色圆点,假设就是我们触摸的点。我们知道无论是 View 还是 ViewGroup,最终的点击事件都会由 onTouchEvent(MotionEvent event)方法来处理。MotionEvent 在用户交互中作用重大,其内部提供了很多事件常量,比如我们常用的 ACTION_DOWN、ACTION_UP 和 ACTION_MOVE。此外,MotionEvent 也提供了获取焦点坐标的各种方法。

    • getX():获取点击事件距离控件左边的距离,即视图坐标。
    • getY():获取点击事件距离控件顶边的距离,即视图坐标。
    • getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标。
    • getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标。

Ubuntu 快捷方式存放位置

  1. /usr/share/applications 下的 xxx.desktop

  2. /usr/local/share/applications 下的 xxx.desktop

  3. ~/.local/share/applications 下的 xxx.desktop

  4. 如果以上路径都没有,通过 find 命令查找

    find ./ -name *.desktop | grep xxx

阻塞队列

介绍阻塞队列的定义、种类、实现原理以及应用。

一、阻塞队列简介

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

  1. 常见阻塞场景

    阻塞队列有两个常见的阻塞场景,它们分别是:

    (1)当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。

    (2)当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

    支持以上两种阻塞场景的队列被称为阻塞队列。

  2. BlockingQueue 的核心方法

    放入数据:

    • offer(anObject):表示如果可能的话,将 anObject 加到 BlockingQueue 里。即如果 BlockingQueue 可以容纳,则返回 true,否则返回 false。(本方法不阻塞当前执行方法的线程。)
    • offer(E o, long timeout, TimeUnit unit):可以设定等待的时间。如果在指定的时间内还不能往队列中加入 BlockingQueue,则返回失败。
    • put(anObject):将 anObject 加到 BlockingQueue 里。如果 BlockQueue 没有空间,则调用此方法的线程被阻断,直到 BlockingQueue 里面有空间再继续。

    获取数据:

    • poll(time):取走 BlockingQueue 里排在首位的对象。若不能立即取出,则可以等 time 参数规定的时间。取不到时返回 null。
    • poll(long timeout, TimeUnit unit):从 BlockingQueue 中取出一个队首的对象。如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据;否则直到时间超时还没有数据可取,返回失败。
    • take():取走 BlockingQueue 里排在首位的对象。若 BlockingQueue 为空,则阻断进入等待状态,直到 BlockingQueue 有新的数据被加入。
    • drainTo():一次性从 BlockingQueue 获取所有可用的数据对象(还可以指定获取数据的个数)。通过该方法,可以提升获取数据的效率;无须多次分批加锁或释放锁。

二、Java 中的阻塞队列

在 Java 中提供了7个阻塞队列,它们分别如下所示:

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  • DelayQueue:使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue:不存储元素的阻塞队列。
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

下面分别介绍这些阻塞队列:

  1. ArrayBlockingQueue

    它是用数组实现的有界阻塞队列,并按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证线程公平地访问队列。公平访问队列就是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列。即先阻塞的生产者线程,可以先往队列里插入元素;先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列,如下所示:

    ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(2000, true);
  2. LinkedBlockingQueue

    它是基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,此队列按照先进先出(FIFO)的原则对元素进行排序,其内部也维持着一个数据缓冲队列(该队列由一个链表构成)。当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到缓存容量的最大值时(LinkedBlockingQueue 可以通过构造方法指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒。反之,对于消费者这端的处理也基于同样的原理。而 LinkedBlockingQueue 之所以能够高效地处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步。这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。作为开发者,我们需要注意的是,如果构造一个 LinkedBlockingQueue 对象,而没有指定其容量大小,LinkedBlockingQueue 会默认一个类似无限大小的容量(Integer.MAX_VALUE)。这样一来,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。ArrayBlockingQueue 和 LinkedBlockingQueue 是两个最普通也是最常用的阻塞队列。一般情况下,在处理多线程间的生产者-消费者问题时,使用这两个类足已。

  3. PriorityBlockingQueue

    它是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。这里可以自定义实现 compareTo() 方法来指定元素进行排序规则;或者初始化 PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。但其不能保证同优先级元素的顺序。

  4. DelayQueue

    它是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现。队列中的元素必须实现 Delayed 接口。创建元素时,可以指定元素到期的时间,只有在元素到期时才能从队列中取走。

  5. SynchronousQueue

    它是一个不存储元素的阻塞队列。每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此此队列内部其实没有任何一个元素,或者说容量是0,严格来说它并不是一种容器。由于队列没有容量,因此不能调用 peek 操作(返回队列的头元素)。

  6. LinkedTransferQueue

    它是一个由链表结构组成的无界阻塞 TransferQueue 队列。LinkedTransferQueue 实现了一个重要的接口 TransferQueue。该接口含有5个方法,其中有3个重要的方法,它们分别如下所示:

    (1)transfer(E e):若当前存在一个正在等待获取的消费者线程,则立刻将元素传递给消费者;如果没有消费者在等待接收数据,就会将元素插入到队列尾部,并且等待进入阻塞状态,直到有消费者线程取走该元素。

    (2)tryTransfer(E e):若当前存在一个正在等待获取的消费者线程,则立刻将元素传递给消费者;若不存在,则返回 false,并且不进入队列,这是一个不阻塞的操作。与 transfer 方法不同的是,tryTransfer 方法无论消费者是否接收,其都会立即返回;而 transfer 方法则是消费者接收了才返回。

    (3)tryTransfer(E e, long timeout, TimeUnit unit):若当前存在一个正在等待获取的消费者线程,则立刻将元素传递给消费者;若不存在则将元素插入到队列尾部,并且等待消费者线程取走该元素。若在指定的超时时间内元素未被消费者线程获取,则返回 false;若在指定的超时时间内其被消费者线程获取,则返回 true。

  7. LinkedBlockingDeque

    它是一个由链表结构组成的双向阻塞队列。双向队列可以从队列的两端插入和移出元素,因此在多线程同时入队时,也就减少了一半的竞争。由于是双向的,因此 LinkedBlockingDeque 多了 addFirst、addLast、offerFirst、offerLast、peekFirst、peekLast 等方法。其中,以 First 单词结尾的方法,表示插入、获取或移除双端队列的第一个元素;以 Last 单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。

三、阻塞队列的实现原理

以 ArrayBlockingQueue 为例,我们先来看看源码,如下所示:

public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {

    private static final long serialVersionUID = -817911632652898426L;
    final Object[] items;
    int takeIndex;
    int putIndex;
    int count;
    final ReentrantLock lock;
    private final Condition notEmpty;
    private final Condition notFull;

    ...
}

从上面的代码可以看出 ArrayBlockingQueue 是维护一个 Object 类型的数组,takeIndex 和 putIndex 分别表示队首元素和队尾元素的下标,count 表示队列中元素的个数,lock 则是一个可重入锁,notEmpty 和 notFull 是等待条件。

接下来我们看看关键方法 put,源码如下所示:

public void put(E e) throws InterruptedException {
    Objects.requireNonNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

从 put 方法的实现可以看出,它先获取了锁,并且获取的是可中断锁,然后判断当前元素个数是否等于数组的长度,如果相等,则调用 notFull.await() 进行等待。当此线程被其他线程唤醒时,通过 enqueue(e) 方法插入元素,最后解锁。

接下来看看 enqueue(e) 方法,如下所示:

private void enqueue(E e) {
    final Object[] items = this.items;
    items[putIndex] = e;
    if (++putIndex == items.length) putIndex = 0;
    count++;
    notEmpty.signal();
}

插入成功后,通过 notEmpty 唤醒正在等待取元素的线程。

再来看看 take 方法,如下所示:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

跟 put 方法实现类似,put 方法等待的是 notFull 信号,而 take 方法等待的是 notEmpty 信号。在 take 方法中,如果可以取元素,则通过 dequeue 方法取得元素。

下面是 dequeue 方法的实现:

private E dequeue() {
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E e = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length) takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return e;
}

跟 enqueue 方法类似,在获取元素后,通过 notFull 的 signal 方法来唤醒正在等待插入元素的线程。

四、阻塞队列的使用场景

除了线程池的实现使用阻塞队列外,我们还可以在生产者-消费者模式中使用阻塞队列:首先使用 Object.wait()、Object.notify() 和非阻塞队列实现生产者-消费者模式,代码如下所示:

public class Test {
    private int queueSize = 10;

    private PriorityQueue<Integer> queue = new PriorityQueue<>(queueSize);

    public static void main(String[] args) {
        Test test = new Test();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();
        producer.start();
        consumer.start();
    }

    class Consumer extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.isEmpty()) {
                        try {
                            System.out.println("队列空,等待数据");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }

                    // 每次移走队首元素
                    queue.poll();
                    queue.notify();
                }
            }
        }
    }

    class Producer extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == queueSize) {
                        try {
                            System.out.println("队列满,等待有空余空间");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }

                    // 每次插入一个元素
                    queue.offer(1);
                    queue.notify();
                }
            }
        }
    }
}

下面是使用阻塞队列实现的生产者-消费者模式,代码如下所示:

public class Test {
    private int queueSize = 10;

    private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(queueSize);

    public static void main(String[] args) {
        Test test = new Test();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();
        producer.start();
        consumer.start();
    }

    class Consumer extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    class Producer extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    queue.put(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

很显然,使用阻塞队列实现无须单独考虑同步和线程间通信的问题,其实现起来很简单。

线程池

在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要开一个新线程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。这时就需要线程池来对线程进行管理。在 Java 中提供了 Executor 框架用于把任务的提交和执行解耦,任务的提交交给 Runnable 或者 Callable,而 Executor 框架用来处理任务。Executor 框架中最核心的成员就是 ThreadPoolExecutor,它是线程池的核心实现类。

一、ThreadPoolExecutor

可以通过 ThreadPoolExecutor 来创建一个线程池,ThreadPoolExecutor 类一共有4个构造方法。其中,拥有最多参数的构造方法如下所示:

public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler) {
    ...
}

这些参数的作用如下所示:

  • corePoolSize:核心线程数。默认情况下线程池是空的,只有任务提交时才会创建线程。如果当前运行的线程数少于 corePoolSize,则创建新线程来处理任务;如果等于或者多于 corePoolSize,则不再创建。如果调用线程池的 prestartAllcoreThread 方法,线程池会提前创建并启动所有的核心线程来等待任务。

  • maximumPoolSize:线程池允许创建的最大线程数。如果任务队列满了并且线程数小于 maximumPoolSize 时,则线程池仍旧会创建新的线程来处理任务。

  • keepAliveTime:非核心线程闲置的超时时间。超过这个时间则回收。如果任务很多,并且每个任务的执行事件很短,则可以调大 keepAliveTime 来提高线程的利用率。另外,如果设置 allowCoreThreadTimeOut 属性为 true 时,keepAliveTime 也会应用到核心线程上。

  • TimeUnit:keepAliveTime 参数的时间单位。可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、秒(SECONDS)、毫秒(MILLISECONDS)等。

  • workQueue:任务队列。如果当前线程数大于 corePoolSize,则将任务添加到此任务队列中。该任务队列是 BlockingQueue 类型的,也就是阻塞队列。

  • ThreadFactory:线程工厂。可以用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置该参数。

  • RejectedExecutionHandler:饱和策略。这是当任务队列和线程池都满了时所采取的应对策略,默认是 AbordPolicy,表示无法处理新任务,并抛出 RejectedExecutionException 异常。此外还有3种策略,它们分别如下:

    • CallerRunsPolicy:用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
    • DiscardPolicy:不能执行的任务,并将该任务删除。
    • DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。

二、线程池的处理流程和原理

当提交一个新的任务到线程池时,线程池的处理流程如图1所示:

图1 线程池的处理流程

从图1可以得知线程的处理流程主要分为3个步骤,如下所示:

  1. 提交任务后,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达到核心线程数,则创建核心线程处理任务;否则,就执行下一步操作。

  2. 接着线程池判断任务队列是否满了。如果没满,则将任务添加到任务队列中;否则,就执行下一步操作。

  3. 接着因为任务队列满了,线程池就判断线程数是否达到了最大线程数。如果未达到,则创建非核心线程处理任务;否则,就执行饱和策略,默认会抛出 RejectedExecutionException 异常。

上面介绍了线程池的处理流程,但还不是很直观。下面结合图2,我们就能更好地了解线程池的原理了:

图2 线程池执行示意图

从图2中可以看到,如果我们执行 ThreadPoolExecutor 的 execute 方法,会遇到各种情况:

  1. 如果线程池中的线程数未达到核心线程数,则创建核心线程处理任务。

  2. 如果线程数大于或者等于核心线程数,则将任务加入任务队列,线程池中的空闲线程会不断地从任务队列中取出任务进行处理。

  3. 如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务。

  4. 如果线程数超过了最大线程数,则执行饱和策略。

三、线程池的种类

通过直接或者间接地配置 ThreadPoolExecutor 的参数可以创建不同类型的 ThreadPoolExecutor,其中有 4 种线程池比较常用,它们分别是 FixedThreadPool、CachedThreadPool、SingleThreadExecutor 和 ScheduledThreadPool。

下面分别介绍这4种线程池:

  1. FixedThreadPool

    FixedThreadPool 是可重用固定线程数的线程池。在 Executors 类中提供了创建 FixedThreadPool 的方法,它的创建源码如下所示:

     public static ExecutorService newFixedThreadPool(int nThreads) {
         return new ThreadPoolExecutor(nThreads, nThreads,
                                       0L, TimeUnit.MILLISECONDS,
                                       new LinkedBlockingQueue<Runnable>());
     }

    FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都设置为创建 FixedThreadPool 指定的参数 nThreads,也就意味着 FixedThreadPool 只有核心线程,并且数量是固定的,没有非核心线程。keepAliveTime 设置为 0L 意味着多余的线程会被立即终止。因为不会产生多余的线程,所以 keepAliveTime 是无效的参数。另外,任务队列采用了无界的阻塞队列 LinkedBlockingQueue。

    FixedThreadPool 的 execute 方法的执行示意图如图3所示:

    图3 FixedThreadPool 的执行示意图

    从图3中可以看出,当执行 execute 方法时,如果当前运行的线程未达到 corePoolSize(核心线程数)时就创建核心线程来处理任务,如果达到了核心线程数则将任务添加到 LinkedBlockingQueue 中。FixedThreadPool 就是一个有固定数量核心线程的线程池,并且这些核心线程不会被回收。当线程数超过 corePoolSize 时,就将任务存储在任务队列中;当线程池有空闲线程时,则从任务队列中去取任务执行。

  2. CachedThreadPool

    CachedThreadPool 是一个根据需要创建线程的线程池,它的创建源码如下所示:

     public static ExecutorService newCachedThreadPool() {
         return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                       60L, TimeUnit.SECONDS,
                                       new SynchronousQueue<Runnable>());
     }

    CachedThreadPool 的 corePoolSize 为 0,maximumPoolSize 设置为 Integer.MAX_VALUE,这意味着 CachedThreadPool 没有核心线程,非核心线程是无界的。keepAliveTime 设置为 60L,则空闲线程等待新任务的最长时间为 60 秒。在此用了阻塞队列 SynchronousQueue,它是一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。

    CachedThreadPool 的 execute 方法的执行示意图如图4所示:

    图4 CachedThreadPool 的执行示意图

    当执行 execute 方法时,首先会执行 SynchronousQueue 的 offer 方法来提交任务,并且查询线程池中是否有空闲的线程执行 SynchronousQueue 的 poll 方法来移除任务。如果有则配对成功,将任务交给这个空闲的线程处理;如果没有则配对失败,创建新的线程去处理任务。当线程池中的线程空闲时,它会执行 SynchronousQueue 的 poll 方法,等待 SynchronousQueue 中新提交的任务。如果超过 60 秒没有新任务提交到 SynchronousQueue,则这个空闲线程将终止。因为 maximumPoolSize 是无界的,所以如果提交的任务大于线程池中线程处理任务的速度就会不断地创建新线程。另外,每次提交任务都会立即有线程去处理。所以,CachedThreadPool 比较适于大量的需要立即处理并且耗时较少的任务。

  3. SingleThreadExecutor

    SingleThreadExecutor 是使用单个工作线程的线程池,它的创建源码如下所示:

     public static ExecutorService newSingleThreadExecutor() {
         return new FinalizableDelegatedExecutorService
             (new ThreadPoolExecutor(1, 1,
                                     0L, TimeUnit.MILLISECONDS,
                                     new LinkedBlockingQueue<Runnable>()));
     }

    corePoolSize 和 maximumPoolSize 都为 1,意味着 SingleThreadExecutor 只有一个核心线程。keepAliveTime 设置为 0L 意味着多余的线程会被立即终止。因为不会产生多余的线程,所以 keepAliveTime 是无效的参数。另外,任务队列采用了无界的阻塞队列 LinkedBlockingQueue。

    SingleThreadExecutor的execute方法的执行示意图如图5所示:

    图5 SingleThreadExecutor 的执行示意图

  4. ScheduledThreadPool

    ScheduledThreadPool 是一个能实现定时和周期性任务的线程池,它的创建源码如下所示:

     public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
         return new ScheduledThreadPoolExecutor(corePoolSize);
     }

    这里创建了 ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor,它主要用于给定延时之后的运行任务或者定期处理任务。ScheduledThreadPoolExecutor 的构造方法如下所示:

     public ScheduledThreadPoolExecutor(int corePoolSize) {
         super(corePoolSize, Integer.MAX_VALUE,
               DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
               new DelayedWorkQueue());
     }

    从上面的代码可以看出,ScheduledThreadPoolExecutor 的构造方法最终调用的是 ThreadPoolExecutor 的构造方法。corePoolSize 是传进来的固定数值,maximumPoolSize 的值是 Integer.MAX_VALUE。因为采用的 DelayedWorkQueue 是无界的,所以 maximumPoolSize 这个参数是无效的。

    ScheduledThreadPoolExecutor 的 execute 方法的执行示意图如图6所示:

    图6 ScheduledThreadPoolExecutor 的执行示意图

    当执行 ScheduledThreadPoolExecutor 的 scheduleAtFixedRate 或者 scheduleWithFixedDelay 方法时,会向 DelayedWorkQueue 添加一个实现 RunnableScheduledFuture 接口的 ScheduledFutureTask(任务的包装类),并会检查运行的线程是否达到 corePoolSize。如果没有则新建线程并启动它,但并不是立即去执行任务,而是去 DelayedWorkQueue 中取 ScheduledFutureTask,然后去执行任务。如果运行的线程达到了 corePoolSize 时,则将任务添加到 DelayedWorkQueue 中。DelayedWorkQueue 会将任务进行排序,先要执行的任务放在队列的前面。其跟此前介绍的线程池不同的是,当执行完任务后,会将 ScheduledFutureTask 中的 time 变量改为下次要执行的时间并放回到 DelayedWorkQueue 中。

同步

在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。竞争条件最容易理解的例子如下:比如电影院售卖电影票,电影票数量是一定的,但卖电影票的窗口到处都有,每个窗口就相当于一个线程。这么多的线程共用所有的电影票资源,如果不使用同步是无法保证其原子性的。在一个时间点上,两个线程同时使用电影票资源,那其取出的电影票是一样的(座位号一样),这样就会给顾客造成麻烦。解决方法如下:当一个线程要使用电影票这个资源时,我们就交给它一把锁,等它把事情做完后再把锁给另一个要用这个资源的线程。这样就不会出现上述情况了。

一、重入锁与条件对象

重入锁 ReentrantLock 就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。用 ReentrantLock 保护代码块的结构如下所示:

Lock lock = new ReentrantLock();
lock.lock();
try {
    ...
} finally {
    lock.unlock();
}

这一结构确保任何时刻只有一个线程进入临界区,临界区就是在同一时刻只能有一个任务访问的代码区。一旦一个线程封锁了锁对象,其他任何线程都无法进入 Lock 语句。把解锁的操作放在 finally 中是十分必要的。如果在临界区发生了异常,锁是必须要释放的,否则其他线程将会永远被阻塞。进入临界区时,却发现在某一个条件满足之后,它才能执行。这时可以使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程,条件对象又被称作条件变量。通过下面的例子来说明为何需要条件对象。假设一个场景需要用银行转账。我们首先写了银行的类,它的构造方法需要传入银行账户的数量和每个账户的账户金额。

public class Bank {

    private double[] accounts;

    private Lock bankLock;

    public Bank(int number, double money) {
        accounts = new double[number];
        bankLock = new ReentrantLock();
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = money;
        }
    }
}

接下来我们要转账,写一个转账的方法,from 是转账方,to 是接收方,amount 是转账金额,如下所示:

public void transfer(int from, int to, int amount) {
    bankLock.lock();
    try {
        while (accounts[from] < amount) {
            // wait
        }
    } finally {
        bankLock.unlock();
    }
}

结果我们发现转账方余额不足;如果有其他线程给这个转账方再转足够的钱,就可以转账成功了。但是这个线程已经获取了锁,它具有排他性,别的线程无法获取锁来进行存款操作,这就是我们需要引入条件对象的原因。一个锁对象拥有多个相关的条件对象,可以用 newCondition 方法获得一个条件对象,我们得到条件对象后调用 await 方法,当前线程就被阻塞了并放弃了锁。整理以上代码,加入条件对象,代码如下所示:

public class Bank {

    private double[] accounts;

    private Lock bankLock;

    private Condition condition;

    public Bank(int number, double money) {
        accounts = new double[number];
        bankLock = new ReentrantLock();
        // 得到条件对象
        condition = bankLock.newCondition();
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = money;
        }
    }

    public void transfer(int from, int to, int amount) {
        bankLock.lock();
        try {
            while (accounts[from] < amount) {
                // 阻塞当前线程,并放弃锁
                condition.await();
            }
        } finally {
            bankLock.unlock();
        }
    }
}

一旦一个线程调用 await 方法,它就会进入该条件的等待集并处于阻塞状态,直到另一个线程调用了同一个条件的 signalAll 方法时为止。当另一个线程转账给我们此前的转账方时,只要调用 condition.signalAll(),就会重新激活因为这一条件而等待的所有线程。代码如下所示:

public void transfer(int from, int to, int amount) {
    bankLock.lock();
    try {
        while (accounts[from] < amount) {
            // 阻塞当前线程,并放弃锁
            condition.await();
        }

        // 转账的操作
        accounts[from] = accounts[from] - amount;
        accounts[to] = accounts[to] + amount;
        condition.signalAll();
    } finally {
        bankLock.unlock();
    }
}

当调用 signalAll 方法时并不是立即激活一个等待线程,它仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,通过竞争实现对对象的访问。还有一个方法是 signal,它则是随机解除某个线程的阻塞。如果该线程仍然不能运行,则再次被阻塞。如果没有其他线程再次调用 signal,那么系统就死锁了。

二、同步方法

Lock 和 Condition 接口为程序设计人员提供了高度的锁定控制,然而大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到 Java 语言内部的机制。从 Java 1.0 版开始,Java 中的每一个对象都有一个内部锁。如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。也就是如下代码:

public synchronized void method() {
}

等价于:

Lock lock = new ReentrantLock();
public void method() {
    lock.lock();
    try {
        ...
    } finally {
        lock.unlock();
    }
}

对于上面银行转账的例子,我们可以将 Bank 类的 transfer 方法声明为 synchronized,而不是使用一个显式的锁。内部对象锁只有一个相关条件,wait 方法将一个线程添加到等待集中,notifyAll 或者 notify 方法解除等待线程的阻塞状态。也就是说 wait 相当于调用 condition.await(),notifyAll 等价于 condition.signalAll()。上面例子中的 transfer 方法也可以这样写:

public synchronized void transfer(int from, int to, int amount) throws InterruptedException {
    while (accounts[from] < amount) {
        wait();
    }

    // 转账的操作
    accounts[from] = accounts[from] - amount;
    accounts[to] = accounts[to] + amount;
    notifyAll();
}

可以看到使用 synchronized 关键字来编写代码要简洁很多。当然要理解这一代码,你必须要了解每一个对象有一个内部锁,并且该锁有一个内部条件。由该锁来管理那些试图进入 synchronized 方法的线程,由该锁中的条件来管理那些调用 wait 的线程。

三、同步代码块

上面我们说过,每一个 Java 对象都有一个锁,线程可以调用同步方法来获得锁。还有另一种机制可以获得锁,那就是使用一个同步代码块,如下所示:

synchronized(obj) {
}

其获得了 obj 的锁,obj 指的是一个对象。再来看看 Bank 类,我们用同步代码块进行改写:

public class Bank {

    private double[] accounts;

    private Object lock = new Object();

    public Bank(int number, double money) {
        accounts = new double[number];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = money;
        }
    }

    public void transfer(int from, int to, int amount) {
        synchronized (lock) {
            // 转账的操作
            accounts[from] = accounts[from] - amount;
            accounts[to] = accounts[to] + amount;
        }
    }
}

在这里创建了一个名为 lock 的 Object 类,为的是使用 Object 类所持有的锁。同步代码块是非常脆弱的,通常不推荐使用。一般实现同步最好用 java.util.concurrent 包下提供的类,比如阻塞队列。如果同步方法适合你的程序,那么请尽量使用同步方法,这样可以减少编写代码的数量,减少出错的概率。如果特别需要使用 Lock/Condition 结构提供的独有特性时,才使用 Lock/Condition。

四、volatile

有时仅仅为了读写一个或者两个实例域就使用同步的话,显得开销过大;而 volatile 关键字为实例域的同步访问提供了免锁的机制。如果声明一个域为 volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。再讲到 volatile 关键字之前,我们需要了解一下内存模型的相关概念以及并发编程中的3个特性:原子性、可见性和有序性。

  1. Java 内存模型

    Java 中的堆内存用来存储对象实例,堆内存是被所有线程共享的运行时内存区域,因此,它存在内存可见性的问题。而局部变量、方法定义的参数则不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。Java 内存模型定义了线程和主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。需要注意的是本地内存是 Java 内存模型的一个抽象概念,其并不真实存在,它涵盖了缓存、写缓冲区、寄存器等区域。Java 内存模型控制线程之间的通信,它决定一个线程对主存共享变量的写入何时对另一个线程可见。

    Java 内存模型的抽象示意图如图1所示:

    线程 A 与线程 B 之间若要通信的话,必须要经历下面两个步骤:

    (1)线程 A 把线程 A 本地内存中更新过的共享变量刷新到主存中去。

    (2)线程 B 到主存中去读取线程 A 之前已更新过的共享变量。

    由此可见,如果我们执行下面的语句:

    int i = 3;
    

    执行线程必须先在自己的工作线程中对变量 i 所在的缓存行进行赋值操作,然后再写入主存当中,而不是直接将数值3写入主存当中。

  2. 原子性、可见性和有序性

    (1)原子性

    对基本数据类型变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行完毕,要么就不执行。现在看一下下面的代码,如下所示:

    x = 3;   // 语句1
    y = x;   // 语句2
    x++;     // 语句3

    在上面3个语句中,只有语句1是原子性操作,其他两个语句都不是原子性操作。语句2虽说很短,但它包含了两个操作,它先读取 x 的值,再将 x 的值写入工作内存。读取 x 的值以及将 x 的值写入工作内存这两个操作单拿出来都是原子性操作,但是合起来就不是原子性操作了。语句3包括3个操作:读取 x 的值、对 x 的值进行加1、向工作内存写入新值。通过这3个语句我们得知,一个语句含有多个操作时,就不是原子性操作,只有简单地读取和赋值(将数字赋值给某个变量)才是原子性操作。java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。例如 AtomicInteger 类提供了方法 incrementAndGet 和 decrementAndGet,它们分别以原子方式将一个整数自增和自减。可以安全地使用 AtomicInteger 类作为共享计数器而无须同步。另外这个包还包含 AtomicBoolean、AtomicLong 和 AtomicReference 这些原子类,这仅供开发并发工具的系统程序员使用,应用程序员不应该使用这些类。

    (2)可见性

    可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。当一个共享变量被 volatile 修饰时,它会保证修改的值立即被更新到主存,所以对其他线程是可见的。当有其他线程需要读取该值时,其他线程会去主存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,并不会立即被写入主存,何时被写入主存也是不确定的。当其他线程去读取该值时,此时主存中可能还是原来的旧值,这样就无法保证可见性。

    (3)有序性

    Java 内存模型中允许编译器和处理器对指令进行重排序,虽然重排序过程不会影响到单线程执行的正确性,但是会影响到多线程并发执行的正确性。这时可以通过 volatile 来保证有序性,除了 volatile,也可以通过 synchronized 和 Lock 来保证有序性。我们知道,synchronized 和 Lock 保证每个时刻只有一个线程执行同步代码,这相当于是让线程顺序执行同步代码,从而保证了有序性。

  3. volatile 关键字

    当一个共享变量被 volatile 修饰之后,其就具备了两个含义,一个是线程修改了变量的值时,变量的新值对其他线程是立即可见的。换句话说,就是不同线程对这个变量进行操作时具有可见性。另一个含义是禁止使用指令重排序。

    这里提到了重排序,那么什么是重排序呢?重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。

    下面我们来看一段代码,假设线程1先执行,线程2后执行,如下所示:

    // 线程1
    boolean stop = false;
    while (!stop) {
        // 执行业务代码
    }
    
    // 线程2
    stop = true;

    很多开发人员在中断线程时可能会采用这种方式。但是这段代码不一定会将线程中断。虽说无法中断线程这个情况出现的概率很小,但是一旦发生这种情况就会造成死循环。为何有可能无法中断线程?在前面我提到每个线程在运行时都有私有的工作内存,因此线程1在运行时会将stop变量的值复制一份放在私有的工作内存中。当线程2更改了 stop 变量的值之后,线程2突然需要去做其他的操作,这时就无法将更改的 stop 变量写入主存当中,这样线程1就不会知道线程2对 stop 变量进行了更改,因此线程1就会一直循环下去。当 stop 用 volatile 修饰之后,那么情况就变得不同了,当线程2进行修改时,会强制将修改的值立即写入主存,并且会导致线程1的工作内存中变量 stop 的缓存行无效,这样线程1再次读取变量 stop 的值时就会去主存读取。

  4. volatile 不保证原子性

    我们知道 volatile 保证了操作的可见性,下面我们来分析 volatile 是否能保证对变量的操作是原子性的。现在先阅读以下代码:

    public class VolatileTest {
    
        public volatile int inc = 0;
    
        public void increase() {
            inc++;
        }
    
        public static void main(String[] args) {
            final VolatileTest test = new VolatileTest();
            for (int i = 0; i < 10; i++) {
                new Thread() {
                    public void run() {
                        for (int j = 0; j < 1000; j++) {
                            test.increase();
                        }
                    }
                }.start();
            }
    
            // 如果有子线程就让出资源,保证所有子线程都执行完
            while (Thread.activeCount() > 2) {
                Thread.yield();
            }
            System.out.println(test.inc);
        }
    }

    这段代码每次运行,结果都不一致。在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加 1、写入工作内存。也就是说,自增操作的 3 个子操作可能会分割开执行。假如某个时刻变量 inc 的值为9,线程1对变量进行自增操作,线程1先读取了变量 inc 的原始值,然后线程1被阻塞了。之后线程2对变量进行自增操作,线程2也去读取变量 inc 的原始值,然后进行加1操作,并把10写入工作内存,最后写入主存。随后线程1接着进行加1操作,因为线程1在此前已经读取了 inc 的值为9,所以不会再去主存读取最新的数值,线程1对 inc 进行加1操作后 inc 的值为10,然后将10写入工作内存,最后写入主存。两个线程分别对 inc 进行了一次自增操作后,inc 的值只增加了1,因此自增操作不是原子性操作,volatile 也无法保证对变量的操作是原子性的。

  5. volatile 保证有序性

    volatile 关键字能禁止指令重排序,因此 volatile 能保证有序性。volatile 关键字禁止指令重排序有两个含义:一个是当程序执行到 volatile 变量的操作时,在其前面的操作已经全部执行完毕,并且结果会对后面的操作可见,在其后面的操作还没有进行;在进行指令优化时,在 volatile 变量之前的语句不能在 volatile 变量后面执行;同样,在 volatile 变量之后的语句也不能在 volatile 变量前面执行。

  6. 正确使用 volatile 关键字

    synchronized 关键字可防止多个线程同时执行一段代码,那么这就会很影响程序执行效率。而 volatile 关键字在某些情况下的性能要优于 synchronized。但是要注意 volatile 关键字是无法替代 synchronized 关键字的,因为 volatile 关键字无法保证操作的原子性。通常来说,使用 volatile 必须具备以下两个条件:

    (1)对变量的写操作不会依赖于当前值。

    (2)该变量没有包含在具有其他变量的不变式中。

    第一个条件就是不能是自增、自减等操作,上文已经提到 volatile 不保证原子性。关于第二个条件,我们来举一个例子,它包含了一个不变式:下界总是小于或等于上界,代码如下所示:

     public class NumberRanger {
    
         private volatile int lower, upper;
    
         public int getLower() {
             return lower;
         }
    
         public int getUpper() {
             return upper;
         }
    
         public void setLower(int lower) {
             if (lower > upper) {
                 throw new IllegalArgumentException();
             }
             this.lower = lower;
         }
    
         public void setUpper(int upper) {
             if (upper < lower) {
                 throw new IllegalArgumentException();
             }
             this.upper = upper;
         }
     }

    这种方式将 lower 和 upper 字段定义为 volatile 类型不能够充分实现类的线程安全。如果当两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,如果初始状态是 (0, 5),在同一时间内,线程 A 调用 setLower(4) 并且线程 B 调用 setUpper(3),虽然这两个操作交叉存入的值是不符合条件的,但是这两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4 ,3)。这显然是不对的,因此使用 volatile 无法实现 setLower 和 setUpper 操作的原子性。

    使用 volatile 有很多种场景,这里介绍其中的两种。

    (1)状态标志

     volatile boolean shutdownRequested;
    
     public void shutdown() {
         shutdownRequested = true;
     }
    
     public void doWork() {
         while (!shutdownRequested) {
             ...
         }
     }

    如果在另一个线程中调用 shutdown 方法,就需要执行某种同步来确保正确实现 shutdownRequested 变量的可见性。但是,使用 synchronized 块编写循环要比使用 volatile 状态标志编写麻烦很多。在这里推荐使用 volatile,状态标志 shutdownRequested 并不依赖于程序内的任何其他状态,并且还能简化代码。因此,此处适合使用 volatile。

    (2)双重检查模式(DCL)

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

    getInstance 方法中对 Singleton 进行了两次判空,第一次是为了不必要的同步,第二次是只有在 Singleton 等于 null 的情况下才创建实例。在这里用到了 volatile 关键字会或多或少地影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。DCL 的优点是资源利用率高,第一次执行 getInstance 方法时单例对象才被实例化,效率高。其缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷(虽然发生的概率很小)。

  7. 小结

    与锁相比,volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件,即变量真正独立于其他变量和自己以前的值,在某些情况下可以使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代码往往比使用锁的代码更加容易出错。在前面的第6小节中介绍了可以使用 volatile 代替 synchronized 的最常见的两种用例,在其他情况下我们最好还是使用 synchronized。

ArkUI - 页面布局(Column、Row)

一、线性布局组件

  1. Column 容器

    从上到下,纵向排列

    示例:

    Column() {
      Text('文字一')
      Text('文字二')
      Text('文字三')
      Text('文字四')
      Text('文字五')
    }
    

    效果:
    Column

  2. Row 容器

    从左到右,横向排列

    示例:

    Row() {
      Text('文字一')
      Text('文字二')
      Text('文字三')
      Text('文字四')
      Text('文字五')
    }
    

    效果:
    Row

二、常见布局属性

布局属性

在容器内部,元素排列是有方向的。Column 容器的元素是从上往下,垂直方向的排列;Row 容器的元素是从左往右,水平方向的排列。把容器内部元素排列方向上的这个轴线称之为主轴,把与主轴垂直的轴线称之为交叉轴,元素的对其方式就跟这两个轴有关系。除了对其以外,元素与元素之间还会有一定的间隔,称之为 Space,默认值是 0,也就是说默认情况下元素与元素之间是紧贴着的,在创建容器时可以对 Space 赋值,来改变容器的元素间隔。

  1. 对齐方式

    属性方法名 说明 参数
    justifyContent 设置子元素在主轴方向的对齐格式 FlexAlign 枚举
    alignItems 设置子元素在交叉轴方向的对齐格式 Row 容器使用 VerticalAlign 枚举
    Column 容器使用 HorizontalAlign 枚举

    示例:

    @Entry
    @Component
    struct Index {
      build() {
        Column({ space: 10 }) {
          Text('文字一')
          Text('文字二')
          Text('文字三')
          Text('文字四')
          Text('文字五')
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
      }
    }
    
    • 首先声明一个 Column 容器,在容器初始化参数里传了一个对象 space(间隔),这样容器内部的元素就会有间隔。
    • 然后在 Column 内部定义四个 Text,由于是 Column 布局,纵向排列,所以四个 Text 是从上往下排列。
    • 最后设置 Column 的属性方法,宽、高 100% 占满整个屏幕,定义 justifyContent 来控制元素在主轴方向的对齐方式,定义 alignItems 来控制元素在交叉轴方向的对齐方式。

    效果:
    对齐方式

  2. FlexAlign 枚举(以 Column 容器为例)

    • FlexAlign.Start:元素与主轴方向的起点位置对齐
    • FlexAlign.Center:元素与主轴方向的中间位置对齐,元素到起点和终点的位置是一致的
    • FlexAlign.End:元素与主轴方向的终点位置对齐
    • FlexAlign.SpaceBetween:把主轴空间平均分配到元素之间
    • FlexAlign.SpaceAround:首尾元素和起点、终点的距离是元素之间距离的一半
    • FlexAlign.SpaceEvenly:首尾元素和起点、终点的距离等同于是元素之间距离
  3. HorizontalAlign 枚举(VerticalAlign 同理)

    • HorizontalAlign.Start:在交叉轴的起点位置
    • HorizontalAlign.Center:在交叉轴的中间位置
    • HorizontalAlign.End:在交叉轴的终点位置

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.