如何更好地进行 Android 组件化开发——路由原理篇

news2025/1/8 5:58:19

前言

组件化开发的会实现代码隔离,在开发时访问不到模块的代码,降低代码耦合度。那么如何跳转组件的页面、如何进行组件间的通信是个问题。这通常会使用到 ARouter、TheRouter、WMRouter 等路由框架。可能有不少人只知道怎么去调用,并不知道其中的实现原理。其实了解路由原理后再进行组件化开发会更加得心应手。

网上很多讲路由原理的文章都是直接分析 ARouter 源码,这对于刚入门组件化开发的人来说比较晦涩难懂。所以个人尝试用一种手写迭代路由框架的方式,让大家能从中学习到:

  • 如何从零开始设计一个路由框架;
  • 路由框架需要考虑怎么样的使用场景;
  • 了解路由框架的核心实现原理;

对这些有一定了解后,我们就能更容易地去阅读其它路由框架的源码了。

搭建简易的路由框架

先了解下什么是路由,维基百科 的介绍是:路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。这是互联网中的路由概念,其思想是通过一样东西找到另一样我们需要的东西。将路由的思想运用到 Android 的组件化中,就是希望能用某个东西能找到对应的类去实现对应的功能。

那我们就来实现一下,首先要有一个 String 和 Class 的映射表,可以用一个 Map 来保存。写一个 SimpleRouter 提供一个设置映射关系和导航的方法。

object SimpleRouter {

  private val routes = HashMap<String, Class<*>>()

  fun putRoute(path: String, clazzName: String) = routes.apply {
    put(path, Class.forName(clazzName))
  }

  fun navigation(path: String): Class<*>? {
    return routes[path]
  }
}

这就实现了一个简易的路由框架,有人可能会说:就这?一个完善好用的路由框架肯定不止这些代码,但是最核心的代码就是路由表。

我们先来用一下,比如将登录页面和 /account/sign_in 字符串映射起来。

SimpleRouter.putRoute("/account/sign_in", "com.dylanc.simplerouter.user.SignInActivity")

后面即使我们做了代码隔离,不能直接访问到对应的 Class,那么我们能通过一个字符串去找到 Class 对象,有了 Class 对象就能跳转页面或实例化。

val clazz = SimpleRouter.navigation("/account/sign_in")
if (clazz != null) {
  startActivity(Intent(this, clazz))
}

手动给路由表的 Map 添加类的信息看起来有点蠢,后面可以改成用注解配置:

@Route(path = "/account/sign_in")
class SignInActivity : AppCompatActivity() {
  // ...
}

APT 解析注解生成文件,其实是最终做的事情是一样的,都是往一个 Map 保存类的信息,只是 put 的代码是自己手写还是注解自动生成而已。本文主要还是了解路由的实现原理,就先用简单的方式实现。

小结一下,第一版我们实现了路由框架的核心功能,建立映射表后就能通过一个字符串去找到类对象去实现想要的功能。

完善页面导航跳转

通过第一版的路由框架能得到所需的类对象了,想跳转一个 Activity 也没有问题,但是跳转的代码总是要判断类对象非空后调用 startActivity(intent)。

val clazz = SimpleRouter.navigation("/account/sign_in")
if (clazz != null) {
  startActivity(Intent(this, clazz))
}

每次跳转页面都要这么写挺繁琐的,我们可以优化一下调用 SimpleRouter.navigation(path) 时会把 startActivity(intent) 给执行了,这样就只需写一行代码就能跳转页面了。

SimpleRouter.navigation("/account/sign_in")

那么问题又来了,怎么传参呢?仅仅只有一个 path 字符串是不够的,需要有另外一个类来设置更多的信息。我们创建一个 Postcard 类,Postcard 是明信片的意思,类似我们能往明信片上写东西,我们能给 Postcard 对象设置 path 和 Bundle 相关的数据。

class Postcard(
  val path: String,
  var bundle: Bundle = Bundle(),
) {

  fun with(bundle: Bundle): Postcard = apply {
    this.bundle = bundle
  }

  fun withString(key: String, value: String): Postcard = apply {
    bundle.putString(key, value)
  }

  fun withInt(key: String, value: Int): Postcard = apply {
    bundle.putInt(key, value)
  }

  fun withLong(key: String, value: Long): Postcard = apply {
    bundle.putLong(key, value)
  }

  fun withFloat(key: String, value: Float): Postcard = apply {
    bundle.putFloat(key, value)
  }

  fun withDouble(key: String, value: Double): Postcard = apply {
    bundle.putDouble(key, value)
  }

  fun withChar(key: String, value: Char): Postcard = apply {
    bundle.putChar(key, value)
  }

  fun withBoolean(key: String, value: Boolean): Postcard = apply {
    bundle.putBoolean(key, value)
  }

  fun withByte(key: String, value: Byte): Postcard = apply {
    bundle.putByte(key, value)
  }

  fun withCharSequence(key: String, value: CharSequence): Postcard = apply {
    bundle.putCharSequence(key, value)
  }

  // ...
}

把前面 navigation() 函数的 String 参数改成 Postcard 类型,可以顺便把 startActivityForResult() 适配了。

object SimpleRouter {

  // ...

  fun navigation(context: Context, postcard: Postcard, requestCode: Int = -1) {
    val destination = routes[postcard.path]
      ?: throw IllegalStateException("There is no route match the path [${postcard.path}]")
    val intent = Intent(context, destination).putExtras(postcard.bundle)
    if (context !is Activity) {
      intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
    }
    if (requestCode >= 0) {
      if (context is Activity) {
        context.startActivityForResult(intent, requestCode)
      }
    } else {
      context.startActivity(intent)
    }
  }
}

之后就可以创建 Postcard 对象去带参跳转页面了。

val postcard = Postcard("/uset/login").withString("email", email)
SimpleRouter.navigation(this, postcard)
// SimpleRouter.navigation(this, postcard, REQUEST_CODE_SIGN_IN)

不过总是要创建个 Postcard 对象也不是方便,最好是能改成链式调用。实现起来也挺简单,我们给 SimpleRouter 增加一个 build(path) 函数返回一个 Postcard 对象。

object SimpleRouter {

  // ...

  fun build(path: String): Postcard {
    return Postcard(path)
  }
}

然后再给 Postcard 类增加两个 navigation() 函数 。

class Postcard(
  val path: String,
  var bundle: Bundle = Bundle(),
) {

  // ...

  fun navigation(context: Context) {
    return SimpleRouter.navigation(context, this, -1)
  }

  fun navigation(activity: Activity, requestCode: Int) {
    return SimpleRouter.navigation(activity, this, requestCode)
  }
}  

这样就能用一行链式代码跳转页面。

SimpleRouter.build("/user/login")
  .withString("email","xxxxx@gmail.com")
  .navigation(this)
// .navigation(this, REQUEST_CODE_SIGN_IN)

至此我们就把路由工具核心的用法给实现了,ARouter 或其它路由框架都是类似的用法。

小结一下,第二版在第一版的基础上,增加了一个 Postcard 类,保存跳转页面时所需的信息。简化了路由工具跳转页面的用法,只需一行代码就能跳转,并且支持传参,支持调用 startActivityForResult(intent)。

支持创建 Fragment

虽然前面把路由工具的核心用法实现了,但是只处理了 Activity 一种情况,还可能会有其他的 Class,比如很常见的 Fragment,我们这就来适配一下。

通常我们是会创建 Fragment 对象来使用,那么可以在获得 Class 后判断一下是不是 Fragment 类型,是的话就实例化无参的构造函数。

目前只是简单地用 String 和 Class 做映射,有时候还是不太够用,最好能区分下类型。所以我们再增加一个 RouteMeta 类来保存更多的路由信息,并且添加一个 RouteType 的路由类型枚举,来区分一下是 Activity 或 Fragment,要支持其它类型只需再添加。

class RouteMeta(
  val destination: Class<*>,
  val type: RouteType,
)

enum class RouteType {
  ACTIVITY, FRAGMENT, UNKNOWN
}

我们把原来路由表的 HashMap<String, Class<*>> 缓存改成 HashMap<String, RouteMeta>,初始化路由表的代码也要更改一下。

object SimpleRouter {
  private val routes = HashMap<String, RouteMeta>()

  fun putRoute(path: String, clazzName: String) = routes.apply {
    val clazz = Class.forName(clazzName)
    val type = when {
      Activity::class.java.isAssignableFrom(clazz) -> RouteType.ACTIVITY
      Fragment::class.java.isAssignableFrom(clazz) -> RouteType.FRAGMENT
      else -> RouteType.UNKNOWN
    }
    put(path, RouteMeta(clazz, type))
  }

  // ...
}

这样我们就能从映射表中查出对应的类型,然后在 navigation() 函数根据类型做不同的事,是 Activity 就跳转页面,是 Fragment 就实例化 Fragment 对象,并且把 Bundle 参数给设置了。

object SimpleRouter {

  // ...

  private lateinit var application: Application

  fun init(application: Application) {
    this.application = application
  }

  fun navigation(ctx: Context? , postcard: Postcard, requestCode: Int = -1): Any? {
    val context = ctx ?: application
    val routeMeta = routes[postcard.path]
      ?: throw IllegalStateException("There is no route match the path [${postcard.path}]")
    val destination = routeMeta.destination
    return when (routeMeta.type) {
      RouteType.ACTIVITY -> {
        val intent = Intent(context, destination).putExtras(postcard.bundle)
        if (context !is Activity) {
          intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
        }
        if (requestCode >= 0) {
          if (context is Activity) {
            context.startActivityForResult(intent, requestCode)
          }
        } else {
          context.startActivity(intent)
        }
        null
      }
      RouteType.FRAGMENT -> {
        val fragmentMeta: Class<*> = destination
        try {
          val instance = fragmentMeta.getConstructor().newInstance()
          if (instance is Fragment) instance.arguments = postcard.bundle
          instance
        } catch (e: Exception) {
          null
        }
      }
      else -> null
    }
  }
}

我们来测试一下,配置个人页面 Fragment 的路由信息。

// 以后会改成用 @Router 注解来初始化路由表
SimpleRouter.putRoute("/account/me", "com.dylanc.simplerouter.user.MeFragment")

然后就能在没有直接依赖其它模块代码的情况下,通过路由去创建 Fragment。

val fragment = SimpleRouter.build("/account/me").navigation() as? Fragment

虽然跳转 Activity 和创建 Fragment 都用了同一个 navigation() 函数,但是一个会用到返回值,一个没有使用返回值,用法上会有点差异,因为根据不同的类型做了不同的事情。

小结,第三版代码在第二版的基础上,支持了 Fragment 的使用场景,如果路由表对应的 Class 是 Fragment,那么会实例化该 Fragment。增加了 RouteType 类和 RouteMeta 类,用于区分类型和保存更多的路由信息。

支持模块间通信

目前我们适配了跳转 Activity 和创建 Fragment,但是组件化开发还有一个很常见的使用场景,就是模块间的通信,获取一个模块的某些信息或者让一个模块做某些事。

这个接口的实现类在哪我们不用关心,因为我们可以用路由工具去得到实例对象。但是怎么区分这是用于模块间通信的类呢?

首先我们要区分出哪些类用于模块间的通信的,定义一个 IProvider 接口,如果有类是实现了该接口,那么该类是会用于模块间通信。

interface IProvider {
  fun init(context: Context)
}

给 RouteType 再加个 PROVIDER 类型,IProvider 子类对应的是 PROVIDER 类型。

enum class RouteType {
  ACTIVITY, FRAGMENT, PROVIDER, UNKNOWN
}

判断是 PROVIDER 类型,我们就用类似 Fragment 的处理方式反射无参的构造函数,但是又有些不一样,没必要每次都实例化 IProvider 对象,可以缓存起来。那么现在缓存的东西开始变多了,我们可以创建一个 Warehouse 类专门来持有需要缓存的数据,把之前的路由表也放到该类中。Warehouse 和 Repository 一样是仓库的意思。

object Warehouse {
  val routes = HashMap<String, RouteMeta>()
  val providers = HashMap<Class<*>, IProvider>()
}

这样我们就可以在 navigation() 函数增加 PROVIDER 类型的逻辑了,不过路由工具类的代码就越来越多了,我们可以优化一下,将初始化缓存和读取缓存的逻辑交给另一个类处理。

我们再创建一个 LogisticsCenter 类,把之前的初始化路由表的函数放到该类处理。还增加一个 completion() 函数去给 Postcard 补充信息,从仓库的路由表查出对应的类信息设置到 Postcard 中。如果路由的类型是 PROVIDER 类型,要获取仓库的 IProvider 对象缓存,没缓存就先实例化存到 Warehouse 中。

object LogisticsCenter {

  private lateinit var context: Application

  fun init(application: Application) {
    context = application
  }

  fun putRoute(path: String, clazzName: String) {
    val clazz = Class.forName(clazzName)
    val type = when {
      Activity::class.java.isAssignableFrom(clazz) -> RouteType.ACTIVITY
      Fragment::class.java.isAssignableFrom(clazz) -> RouteType.FRAGMENT
      IProvider::class.java.isAssignableFrom(clazz) -> RouteType.PROVIDER
      else -> RouteType.UNKNOWN
    }
    Warehouse.routes[path] = RouteMeta(clazz, type)
  }

  @Suppress("UNCHECKED_CAST")
  fun completion(postcard: Postcard) {
    val routeMeta = Warehouse.routes[postcard.path]
      ?: throw IllegalStateException("There is no route match the path [${postcard.path}]")
    postcard.destination = routeMeta.destination
    postcard.type = routeMeta.type
    if (routeMeta.type == RouteType.PROVIDER) {
      val providerClazz = routeMeta.destination as Class<IProvider>
      var instance = Warehouse.providers[providerClazz]
      if (instance == null) {
        try {
          val provider = providerClazz.getConstructor().newInstance()
          provider.init(context)
          Warehouse.providers[providerClazz] = provider
          instance = provider
        } catch (e: Exception) {
          throw IllegalStateException("Init provider failed!")
        }
      }
      postcard.provider = instance
    }
  }
}

修改一下 navigation() 函数,先执行 LogisticsCenter.completion(postcard) 补充信息,如果是 PROVIDER 类型就返回 postcard.provider。

object SimpleRouter {

  // ...

  fun putRoute(path: String, clazzName: String){
    LogisticsCenter.putRoute(path, clazzName)
  }

  fun navigation(context: Context, postcard: Postcard, requestCode: Int = -1): Any? {
    LogisticsCenter.completion(postcard)
    return when (postcard.type) {
      RouteType.ACTIVITY -> {
        // ...
      }
      RouteType.FRAGMENT -> {
        // ...
      }
      RouteType.PROVIDER -> postcard.provider
      else -> null
    }
  }
}

我们在一个公共模块新建一个 AccountService 接口提供登录组件的功能,需要继承 IProvider 接口。

interface AccountService : IProvider {
  val isSignIn: Boolean
  fun logout()
}

然后在用户模块写一个 AccountService 接口的实现类,把功能实现出来。

class AccountServiceProvider : UserService {
  override val isSignIn: Boolean
    get() = // ...

  override fun logout() {
    // ...
  }

  override fun init(context: Context) = Unit
}

在路由表注册该类的路由信息。

// 以后会改成用 @Router 注解来初始化路由表
SimpleRouter.putRoute("/account/service", "com.dylanc.simplerouter.account.AccountServiceProvider")

之后就能通过路由工具得到该接口的实例。

val accountService = SimpleRouter.build("/account/service").navigation() as? AccountService
if (accountService?.isSignIn == true) {
  // 已登录
} else {
  // 未登录
}

小结一下,第四版在第三版的基础上,支持了模块间通信,能获取一个模块的某些信息或者让一个模块做某些事。增加了 IProvider 接口来区分类型,增加了 Warehouse 类持有缓存信息,增加了 LogisticsCenter 类初始化和读取缓存。

路由原理分析

目前我们已经实现了一个简易的路由框架并实现了常用的路由功能,代码虽然比较简单,但其实都是路由框架的核心代码,我们可以从中了解到路由的实现原理。

首先路由框架一般会用个 Map 作为路由表,初始化的时候会给路由表添加每个 path 对应的路由信息,包含 Class 对象、类型等。目前的简易路由框架是手动 put 数据,改成使用注解也只是能优化了用法,实际上做的事是一样的。

然后就是通过 navigation() 函数进行路由导航,会用 path 到路由表查出对应的路由信息。虽然可以直接返回 Class 对象,但是开发起来不太方便,所以会根据不同的类型去做对应的事:

  • 如果 Class 对象是 Activity 类型,就给 intent 设置参数并调用 startActivity() 或者 startActivityForResult() 跳转页面;
  • 如果 Class 对象是 Fragment 类型,就反射无参的构造函数实例化 Fragment 对象,并且给 arguments 设置参数;
  • 如果 Class 对象是 IProvider 类型,也会反射无参的构造函数实例化对象,但是不需要多次实例化,所以会缓存起来;

大部分路由框架都是这样的路由流程,只是实现上会有些区别,大家可以将这个流程作为线索去阅读源码,看看完善的路由框架除了这些功能之外还会额外做些什么处理。

另外前面迭代的简易路由框架所实现的类和函数其实都尽量与 ARouter 的源码保持了一致,让大家顺便了解一下 ARouter 的整体结构可能是怎么样设计出来的,后续阅读源码也更加容易理解,ARouter 在面试中还是比较常会问到的。

总结

本文带着大家迭代了一个路由框架,从一个简单的工具类慢慢完善常见的路由场景,实现模块间跳转页面、创建 Fragment、通信。我们可以从中了解到路由的实现原理,其中最核心的是路由表,初始化的时候给路由表添加路由信息,然后路由导航的时候就能查出路由信息,再根据不同的类型去做不同的事情。大多数路由框架都是这样的流程,我们对此了解后就能更容易去阅读路由框架的源码。


下面整理了《Android 架构学习手册》+《深入理解Gradle框架》学习笔记,根据自己学习中所做的一些笔录来整的,主要也是方便后续好复习翻阅,省掉在去网上查找的时间,以免在度踩坑,如果大家有需要的可以直接通过 →点击此处← 进行参考学习:https://qr21.cn/CaZQLo?BIZ=ECOMMERCE

Android 架构学习手册

深入理解Gradle框架

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/118591.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Pod内容详情梳理

本篇是笔者的一篇读书笔记&#xff0c;用于梳理pod的详情&#xff0c;方便理解和学习&#xff0c;也方便后续自己查询。一、Pod的概述Pod是k8s里面典型的CR&#xff0c;从它的元数据来看&#xff0c;具有所有CR的基本数据构成&#xff0c;分别是 version、kind&#xff0c;以及…

迅为RK3568开发板支持多屏同显/异显动态方案

iTOP-RK3568开发板采用四核Cortex-A55处理器&#xff0c;芯片内置VOP控制器&#xff0c;支持HDMI、LVDS、MIPI、EDP四种显示接口的多屏同显、异显和异触&#xff0c;可有效提高行业定制的拓展性。 三屏同显&#xff1a; 三屏异显&#xff1a; 双屏同显&#xff1a; 双屏异显&am…

Docker容器里进程的 pid 是如何申请出来的?

大家好&#xff0c;我是飞哥&#xff01;如果大家有过在容器中执行 ps 命令的经验&#xff0c;都会知道在容器中的进程的 pid 一般是比较小的。例如下面我的这个例子。# ps -ef PID USER TIME COMMAND1 root 0:00 ./demo-ie13 root 0:00 /bin/bash21 root …

编程小技巧9-如何生成没有水印的代码图片(IDEA carbon-now-sh插件使用教程)

陈老老老板&#x1f9b8;&#x1f468;‍&#x1f4bb;本文专栏&#xff1a;快速变成小技巧&#xff08;主要讲一些平时常用的、有助于提高开发素的内容&#xff09;&#x1f468;‍&#x1f4bb;本文简述&#xff1a;本文讲一下使用carbon-now-sh插件生成图片超详细教程。&…

STM32

一&#xff1a;生成独立的他.h和.c文件 勾选后&#xff0c;生成单独的.h和.c文件。不勾选的话都在main里面。 二&#xff1a;常用。 1&#xff1a;电平输出。 HAL_GPIO_WritePin(PIN_LED_1_GPIO_Port, PIN_LED_1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(PIN_LED_1_GPIO_Port, …

django 登录流程实现

一、简介&#xff1a; 1、用户输入正确的用户名、密码、验证码点击登录即可跳转到管理员页面。 2、用户输入错误的用户名或者密码或者验证码需要错误信息提示&#xff08;数据校验&#xff09; 二、实现步骤 1、新建一个项目&#xff08;创建项目过程和数据库略&#xff0c;…

签完三方后无法去实习,有什么可以弥补的吗?

作者&#xff1a;阿秀校招八股文学习网站&#xff1a;https://interviewguide.cn这是阿秀的第「228」篇原创你好&#xff0c;我是阿秀。2023届秋招已经步入尾声&#xff0c;很多小伙伴都已经找到工作&签约三方&#xff0c;慢慢结束了自己的秋招之旅&#xff0c;不过也有一些…

Local Attention和动态深度卷积间的关系

摘要 Local Vision Transformer 是分别在一个个小的局部窗口中进行注意力计算。 作者将局部注意力重新定义为通道级的局部连接层&#xff08;channel-wise locally-connected layer&#xff09;&#xff0c;并4个方面进行分析&#xff1a;两种网络的正则化方式&#xff0c;稀疏…

C语言实现九大排序算法(建议收藏!)

文章目录排序算法稳定性1. 插入排序原理排序过程代码实现性能分析2. 希尔排序原理排序过程关于增量取值代码实现性能分析3. 选择排序原理排序过程代码实现性能分析4. 堆排序原理排序过程代码实现性能分析5. 冒泡排序原理排序过程代码实现性能分析6. 快速排序原理Hoare法挖坑法前…

Easy App Locker - 给你的 mac 应用加锁保护你的隐私

Easy App Locker - 给你的 mac 应用加锁保护你的隐私 Easy App Locker可以对Mac上的单个应用进行密码保护。维护Mac上的隐私。 像如果你的某个应用存在隐私数据就可以使用该软件将此应用上锁&#xff0c;这样当你的朋友使用你的 mac 时你就不用担心你的隐私被泄露了&#xff0…

Java中创建线程的五种方式

目录&#xff1a; 前言 1.进程与线程的区别&#xff1f; 2.进程是操作系统进行资源分配的基本单位&#xff0c;而操作系统是以线程为单位进行调度的。 3. Java操作多线程&#xff0c;依赖最核心的类Thread。 4.关于start和run的区别&#xff1f; 5.使用JDK自带的工具jcon…

ArcGIS基础实验操作100例--实验7分割多部分要素

本实验专栏来自于汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 基础编辑篇--实验7 分割多部分要素 目录 一、实验背景 二、实验数据 &#xff08;1&#xff09;查看多…

第05讲:Redis主从复制

一、关于主从复制 1.1、什么是主从复制 主机数据更新后根据配置和策略&#xff0c; 自动同步到备机的master/slaver机制&#xff0c;Master以写为主&#xff0c;Slave以读为主 1.2、主从复制的作用 读写分离&#xff0c;性能扩展容灾快速恢复 二、一主多从的实验 2.1、原…

GitHub+HEXO博客设置主题

文章目录安装主题修改站点配置文件修改主题配置文件部署到github补充由于之前使用HexoGitHub搭建了个人博客用的是最原始的主题&#xff0c;丑的一批QAQ 用一下Github上面Star最高Next主题look look&#xff01; 上节博客&#xff1a;HexoGitHub搭建个人博客 主题选取网站&a…

CnOpenData中国工业企业基本信息扩展数据

一、数据简介 拉动中国经济的三个产业中&#xff0c;工业企业占有特殊的地位&#xff0c;是推动国内经济发展的重要产业。工业是最主要的物质生产部门&#xff0c;为居民生活、各行业的经济活动提供物质产品&#xff0c;这一重要作用是其他任何产业部门都无法替代的。工业企业为…

拆串后结构化,其中按行对齐

【问题】 I have a bit weired scenario where i need to fetch data i have following three products product1 product2 product3and each product has different ids(e.g. p1345,p3453,p2345) and then each froduct have different options which are having different…

算法题刷累了就来试试游戏吧----2048小游戏----C语言实现

目录 1. 代码前的准备 2. 游戏思路及代码分析 2.1 game.h 代码分析 2.2 test.cpp代码分析 3. 完整代码 3.1 game.h 3.2 game.cpp 3.3 test.cpp 嘿嘿嘿&#xff0c;写游戏还是挺高兴的撒&#xff0c;如果你还不知道2048这个小游戏的规则&#xff0c;那么快去试试吧。不然…

项目管理误区:项目不确定性≠项目风险

项目失败的原因千千万万&#xff0c;罪魁祸首肯定跟“它”脱不了关系&#xff01; 前段时间&#xff0c;偶然看到一个求助贴引发了各路网友&#xff08;项目经理&#xff09;的热议。求助人的问题是&#xff1a;“如何管理项目中的不确定性&#xff1f;” 下面的回复已然偏离答…

Linux系统运行时参数命令--文件IO性能监控

目录 4 文件IO性能监控 4.1 I/O 的两种方式(缓存 I/O 和直接 I/O) 1 缓存 I/O 2 直接 I/O 4.2 监控磁盘I/O的命令 1 iostat IO状态 2 swapon查看分区使用情况 3 df硬盘使用情况 4 du目录文件大小 4.3 文件IO写入频繁案例分析 C/CLinux服务器开发/后台架构师【零声教育…

IU8689+IU5706 单声道100W/立体声60W同步升压+功放IC大功率拉杆音箱应用组合方案

引言 目前中大功率拉杆音箱主要采用12V铅酸电池为供电电源&#xff0c;在电源直供的时候&#xff0c;一般的功放芯片输出功率在20W左右&#xff08;喇叭为4欧、THD10%&#xff09;。超过50W的功率现阶段市场上主要采用升压芯片TPA3116的组合解决方案。 随着竞争的加剧&#x…