Jetpack Compose中的startActivityForResult的正确姿势

news2024/11/18 16:53:27

之前在 Jetpack Compose中的导航路由 里简单的提到了从 Compose 导航到其他 Activity 页面的方式,对于不带返回结果的则就是跟以前一样简单的启动Activity的代码,而如果是startActivityForResult方式的,需要使用带回调的方式去启动,那么在以前,我们要么是使用三方库,要么是自己封装一个简单的库来使用 (至于背后原理也不是什么新鲜事了) 。

后来研究了一下,在ComposestartActivityForResult也有了新的姿势,要理解ComposestartActivityForResult的姿势,这还得从androidxstartActivityForResult的姿势说起,因为Compose 就是在androidx的基础上利用其API简单封装了一下而已。

倒也不是说以前的不能用了,毕竟有系统自带的,谁还去用第三方的呀,用第三方的还得导入一个依赖库不是,能少依赖三方的就尽量少依赖吧。

androidx之后如何正确的startActivityForResult

如何使用

如果是在ActivityFragment内部使用的话,直接调用registerForActivityResult方法即可。

例如,选择文件:

 val launcher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
     uri?.apply { showToast(uri.toString()) }
 }
 
 launcher.launch("image/*") 

ActivityResultContracts.GetContent()的方式获取文件launch时需要通过指定 mime type来过滤文件类型, 例如 image/* ,这会打开系统自带的一个文件选择器供你选择文件。

录制视频:

 val outVideoFile = File(externalCacheDir, "/${System.currentTimeMillis()}.mp4")
 val videoUri = FileProvider.getUriForFile(this, "${packageName}.provider", outVideoFile)
 val launcher = registerForActivityResult(ActivityResultContracts.CaptureVideo()) { isSuccess ->
     if (isSuccess) {
         showToast(outVideoFile.path)
     }
 }
 
 launcher.launch(videoUri)

拍照:

val outPictureFile = File(externalCacheDir, "/${System.currentTimeMillis()}.jpeg")
val pictureUri = FileProvider.getUriForFile(this, "${packageName}.provider", outPictureFile)
val launcher = registerForActivityResult(ActivityResultContracts.TakePicture()) { isSuccess ->
    if (isSuccess) {
        showToast(outPictureFile.path)
    }
}

launcher.launch(pictureUri)}

这两种方式需要指定Uri, 这个Uri获取有点费劲,需要先进行FileProvider配置,不过配置好就很方便了。(如果你的minSdk配置的是29,那么需要另外配置,可自行查阅相关资料,不过国内基本不会兼容这么低的版本,一般minSdk配置的会是21)

自己的内部业务Activity之间的跳转:

// MainActivity.kt 
val target = Intent(this, OtherActivity::class.java).apply {
    putExtra("name", "张三")
    putExtra("uid", 123)
}
val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
    activityResult.data?.apply {
        val name = getStringExtra("name")
        name?.let { showToast(it) }
    }
}

launcher.launch(target)
// OtherActivity.kt
class OtherActivity: ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val name = intent.getStringExtra("name") 
        val uid = intent.getIntExtra("uid", -1)
        setContent {
            MyComposeApplicationTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    Column {
                        Text("name: $name fromCompose: $fromCompose uid: $uid", fontSize = 20.sp)
                        Button(onClick = {
                            val data = Intent().apply { putExtra("name", "小明") }
                            setResult(RESULT_OK, data)
                            finish()
                        }) { Text("back with result") }
                    }
                }
            }
        }
    }
}

registerForActivityResult的回调接口lambda中,仍然可以像以前那样使用activityResult.resultCode == RESULT_OK 来判断是正常返回了,还是取消操作。

在Activity/Fragment以外的类中使用:

class MyLifecycleObserver(private val registry : ActivityResultRegistry) : DefaultLifecycleObserver {
    lateinit var getContent : ActivityResultLauncher<String>

    override fun onCreate(owner: LifecycleOwner) {
        getContent = registry.register("key", owner, ActivityResultContracts.GetContent()) { uri ->
            // Handle the returned Uri
        }
    }

    fun selectImage() {
        getContent.launch("image/*")
    }
}

很简单,将代码移到一个LifecycleObserver的实现类中即可,然后在Activity/Fragment中将这个LifecycleObserver的实现类的观察者对象添加到其本身的lifecycle中即可。

class MyFragment : Fragment() {
    lateinit var observer : MyLifecycleObserver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        observer = MyLifecycleObserver(requireActivity().activityResultRegistry)
        lifecycle.addObserver(observer)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val selectButton = view.findViewById<Button>(R.id.select_button)

        selectButton.setOnClickListener {
            // Open the activity to select an image
            observer.selectImage()
        }
    }
}

使用总结:

  • 整体来说,终于支持callback回调方式回传结果了,
  • 而且启动时也不用带requestCode了,
  • 并且还支持在Activity/Fragment以外的类中使用。

源码追踪

看一下ComponentActivity中registerForActivityResult 方法的实现:

   // androidx.activity.ComponentActivity.java 1.6.1
    @NonNull
    @Override
    public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultRegistry registry,
            @NonNull final ActivityResultCallback<O> callback) {
        return registry.register(
                "activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
    }

    @NonNull
    @Override
    public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull ActivityResultContract<I, O> contract,
            @NonNull ActivityResultCallback<O> callback) {
        return registerForActivityResult(contract, mActivityResultRegistry, callback);
    }

这里2个重载方法,下面的方法其实是调用了上面的方法,所以本质就是上面的三个参数的方法,也就是 registry.register() 方法,而在Fragment中也有两个类似的方法:

    // androidx.fragment.app.Fragment.java 1.5.4
	@MainThread
    @NonNull
    @Override
    public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultCallback<O> callback) {
        return prepareCallInternal(contract, new Function<Void, ActivityResultRegistry>() {
            @Override
            public ActivityResultRegistry apply(Void input) {
                if (mHost instanceof ActivityResultRegistryOwner) {
                    return ((ActivityResultRegistryOwner) mHost).getActivityResultRegistry();
                }
                return requireActivity().getActivityResultRegistry();
            }
        }, callback);
    }

    @MainThread
    @NonNull
    @Override
    public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultRegistry registry,
            @NonNull final ActivityResultCallback<O> callback) {
        return prepareCallInternal(contract, new Function<Void, ActivityResultRegistry>() {
            @Override
            public ActivityResultRegistry apply(Void input) {
                return registry;
            }
        }, callback);
    }

如果继续跟踪prepareCallInternal方法会发现,Fragment中最终还是依托mHost宿主的ActivityResultRegistry对象来执行registry.register() 方法的。

也就是说其实在androidx以后,任意Activity对象中调用getActivityResultRegistry() 方法,它都会返回一个ActivityResultRegistry 对象,然后调用该对象的register方法来注册回调接口,而在Fragment中自然能获取到宿主Activity对象也能拿到这个ActivityResultRegistry 对象。

着重看一下registry.register() 方法:

    public final <I, O> ActivityResultLauncher<I> register(
            @NonNull final String key,
            @NonNull final LifecycleOwner lifecycleOwner,
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultCallback<O> callback) {
            ......
   }

可以看出register方法它需要四个参数:

  • key: 唯一标识字符串,代替以前的requestCode,其实内部还是使用的整型的requestCode,只不过是根据传入的key生成随机数然后存入一个Map中,用的时候再从Map中获取。
  • lifecycleOwnerlifecycle持有者,我们知道在androidx以后ActivityFragment默认就是LifecycleOwner接口的实现者
  • contractActivityResultContract<I, O> 类型,契约类,它有两个泛型,一个输入类型,一个输出类型
  • callback:回调接口,代替以前的onActivityResult方法,内部会将前面的key参数和callback保存到一个Map中,以便后面需要的时候方便获取

register方法的返回值是一个 ActivityResultLauncher<I>类型的对象,它就是最终真正用来执行启动动作的对象,通过执行它的launch方法进行启动。

register方法还有一个重载方法:

    public final <I, O> ActivityResultLauncher<I> register(
            @NonNull final String key,
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultCallback<O> callback) {
    	......
   }

这个方法与上面那个方法的区别就是少了一个lifecycleOwner参数,但是少了这个参数也就意味着你需要自己的onDestroy方法中执行对应的 ActivityResultLauncher.unregister() 方法,不如上面那个方便了。因为带lifecycleOwner参数的register方法内部其实处理好了:

public final <I, O> ActivityResultLauncher<I> register(
            @NonNull final String key,
            @NonNull final LifecycleOwner lifecycleOwner,
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultCallback<O> callback) {

        Lifecycle lifecycle = lifecycleOwner.getLifecycle();

        if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
            throw new IllegalStateException("LifecycleOwner " + lifecycleOwner + " is "
                    + "attempting to register while current state is "
                    + lifecycle.getCurrentState() + ". LifecycleOwners must call register before "
                    + "they are STARTED.");
        }

        registerKey(key); // 注册
        ...
        LifecycleEventObserver observer = new LifecycleEventObserver() {
            @Override
            @SuppressWarnings("deprecation")
            public void onStateChanged(
                    @NonNull LifecycleOwner lifecycleOwner,
                    @NonNull Lifecycle.Event event) {
                if (Lifecycle.Event.ON_START.equals(event)) {
                    mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));
                    if (mParsedPendingResults.containsKey(key)) {
                        @SuppressWarnings("unchecked")
                        final O parsedPendingResult = (O) mParsedPendingResults.get(key);
                        mParsedPendingResults.remove(key);
                        callback.onActivityResult(parsedPendingResult);
                    }
                    final ActivityResult pendingResult = mPendingResults.getParcelable(key);
                    if (pendingResult != null) {
                        mPendingResults.remove(key);
                        callback.onActivityResult(contract.parseResult(
                                pendingResult.getResultCode(),
                                pendingResult.getData()));
                    }
                } else if (Lifecycle.Event.ON_STOP.equals(event)) {
                    mKeyToCallback.remove(key);
                } else if (Lifecycle.Event.ON_DESTROY.equals(event)) {
                    unregister(key); // 反注册
                }
            }
        };

这里借助生命周期感知那一套,确保了register方法在调用的时候必须在STARTED之前注册,而在ON_DESTROY状态之后则会自动帮你反注册。

对于contract参数,在ActivityResultContracts中提供了常用的内置类型,看名字也可以猜出是干嘛的,比如拍视频、选图片等,如果有需要,开发者可以之前从其中选择,如果是普通的Activity之间的跳转就选择StartActivityForResult这个类型即可。

在这里插入图片描述
不过这些类型你也可以统统不用,自己创建也可以,只需实现ActivityResultContract类满足它的输入输出类型约束即可。

对于ActivityResultLauncher.launch方法也没有什么神秘的,内部调用了ActivityResultRegistryonLaunch方法,而ActivityResultRegistry 是一个抽象类,其实例对象是在ComponentActivity的构造方法创建的:

public abstract class ActivityResultRegistry {
	...
	@MainThread
    public abstract <I, O> void onLaunch(
            int requestCode,
            @NonNull ActivityResultContract<I, O> contract,
            @SuppressLint("UnknownNullness") I input,
            @Nullable ActivityOptionsCompat options);
	....
	@NonNull
    public final <I, O> ActivityResultLauncher<I> register(
            @NonNull final String key,
            @NonNull final LifecycleOwner lifecycleOwner,
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultCallback<O> callback) {
    	.......
    	return new ActivityResultLauncher<I>() {
            @Override
            public void launch(I input, @Nullable ActivityOptionsCompat options) {
                Integer innerCode = mKeyToRc.get(key);
                if (innerCode == null) {
                    throw new IllegalStateException("Attempting to launch an unregistered "
                            + "ActivityResultLauncher with contract " + contract + " and input "
                            + input + ". You must ensure the ActivityResultLauncher is registered "
                            + "before calling launch().");
                }
                mLaunchedKeys.add(key);
                try {
                    onLaunch(innerCode, contract, input, options);
                } catch (Exception e) {
                    mLaunchedKeys.remove(key);
                    throw e;
                }
            }
            @Override
            public void unregister() {
                ActivityResultRegistry.this.unregister(key);
            }
            @NonNull
            @Override
            public ActivityResultContract<I, ?> getContract() {
                return contract;
            }
        };
    }
}
public ComponentActivity() {
		this.mActivityResultRegistry = new ActivityResultRegistry() {
            public <I, O> void onLaunch(final int requestCode, @NonNull ActivityResultContract<I, O> contract, I input, @Nullable ActivityOptionsCompat options) {
                ComponentActivity activity = ComponentActivity.this;
                	... 
                    ActivityCompat.startActivityForResult(activity, intent, requestCode, optionsBundle);
                }
            }
        };
}

这里最终还是执行的Activity的startActivityForResult方法,而在onActivityResult方法中又将结果转发回mActivityResultRegistry对象中:

public class ComponentActivity {
	...
    @Deprecated
    @CallSuper
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        if (!this.mActivityResultRegistry.dispatchResult(requestCode, resultCode, data)) {
            super.onActivityResult(requestCode, resultCode, data);
        }
    } 
}
public abstract class ActivityResultRegistry {
	...
    @MainThread
    public final boolean dispatchResult(int requestCode, int resultCode, @Nullable Intent data) {
        String key = mRcToKey.get(requestCode);
        if (key == null) {
            return false;
        }
        doDispatch(key, resultCode, data, mKeyToCallback.get(key));
        return true;
    }
    
	private <O> void doDispatch(String key, int resultCode, @Nullable Intent data,
            @Nullable CallbackAndContract<O> callbackAndContract) {
        if (callbackAndContract != null && callbackAndContract.mCallback != null
                && mLaunchedKeys.contains(key)) {
            ActivityResultCallback<O> callback = callbackAndContract.mCallback;
            ActivityResultContract<?, O> contract = callbackAndContract.mContract;
            callback.onActivityResult(contract.parseResult(resultCode, data));
            mLaunchedKeys.remove(key);
        } else {
            // Remove any parsed pending result
            mParsedPendingResults.remove(key);
            // And add these pending results in their place
            mPendingResults.putParcelable(key, new ActivityResult(resultCode, data));
        }
    }
}

很明显了,这里根据resultCode得到的key就是当初我们在register方法中传入的key,而传入的key又和传入的callback建立了Map映射关系,因此这里可以拿到我们在register方法中传入的callback进行结果回调。

总而言之一句话,只要官方想玩,源码在手,随时可以改,加个回调也是分分钟的事,就是这件事做的有点晚了,在androidx以前开发者对于startActivityForResult操作还是十分痛苦的(明明一整套的姿势,你非得给我拆开)。但是迟来总比没有好,相信很多公司目前仍有很多老项目有大量的代码甚至迁移到androidx都比较困难。

Jetpack Compose中如何正确的startActivityForResult

简单了解了androidx中如何正确的startActivityForResult后,在Jetpack Compose中使用就非常简单了,在Composable中使用主要通过一个rememberLauncherForActivityResult函数,可以看一下其实现源码:

@Composable
public fun <I, O> rememberLauncherForActivityResult(
    contract: ActivityResultContract<I, O>,
    onResult: (O) -> Unit
): ManagedActivityResultLauncher<I, O> {
    // Keep track of the current contract and onResult listener
    val currentContract = rememberUpdatedState(contract)
    val currentOnResult = rememberUpdatedState(onResult)

    // It doesn't really matter what the key is, just that it is unique
    // and consistent across configuration changes
    val key = rememberSaveable { UUID.randomUUID().toString() }

    val activityResultRegistry = checkNotNull(LocalActivityResultRegistryOwner.current) {
        "No ActivityResultRegistryOwner was provided via LocalActivityResultRegistryOwner"
    }.activityResultRegistry
    val realLauncher = remember { ActivityResultLauncherHolder<I>() }
    val returnedLauncher = remember {
        ManagedActivityResultLauncher(realLauncher, currentContract)
    }

    // DisposableEffect ensures that we only register once
    // and that we unregister when the composable is disposed
    DisposableEffect(activityResultRegistry, key, contract) {
        realLauncher.launcher = activityResultRegistry.register(key, contract) {
            currentOnResult.value(it)
        }
        onDispose {
            realLauncher.unregister()
        }
    }
    return returnedLauncher
}

可以看到,它就是在DisposableEffect这个副作用Api中调用了以前的activityResultRegistry.register而已,返回的是一个ManagedActivityResultLauncher<I, O>类型的对象,这个对象中包装了以前的realLaunchercontract对象。所以说使用跟之前其实是差不多类似的。

选择文件示例:

@Composable
fun PickImage() {
    var uri by remember { mutableStateOf<Uri?>(null) }
    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
        uri = it
    }
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = { launcher.launch("image/*") }) {
            Text(text = "Pick a picture")
        }
        if (uri != null) {
            Image(
                painter = rememberAsyncImagePainter(uri) , // 使用coil加载图片
                modifier = Modifier.fillMaxWidth(),
                contentDescription = null
            )
        }
    }
}

拍照示例:

@Composable
fun TakePhotoForResult() {
    var resultBitmap by remember { mutableStateOf<Bitmap?>(null) }
    val scope = rememberCoroutineScope()

    val outPictureFile = LocalContext.current.getCacheImgFile()
    val pictureUri = LocalContext.current.getOutUri(outPictureFile)
    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { isSuccess ->
        if (isSuccess) {
            scope.launch { resultBitmap = BitmapFactory.decodeFile(outPictureFile.path) }
        }
    }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = { launcher.launch(pictureUri) }) {
            Text(text = "Take a picture")
        }
        resultBitmap?.let { image ->
            Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
        }
    }
}

private fun Context.getCacheImgFile() : File {
    return File(externalCacheDir, "/${System.currentTimeMillis()}.jpeg")
}

private fun Context.getOutUri(file: File) : Uri {
    return FileProvider.getUriForFile(this, "${packageName}.provider", file)
}

如果只是想获得一张拍照预览图,可以使用下面的方式:

@Composable
fun TakePhotoForResult2() {
    var resultBitmap by remember { mutableStateOf<Bitmap?>(null) }
    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap ->
        resultBitmap = bitmap
    }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = { launcher.launch() }) {
            Text(text = "Take a picture")
        }
        resultBitmap?.let { image ->
            Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
        }
    }
}

这种方式更加简单,但是这种方式得到的是一个略缩图的尺寸大小,比较模糊。

启动其他的业务Activity:

@Composable
fun StartActivityForResultExample() {
    var result by remember { mutableStateOf("") } 
    val target = Intent(LocalContext.current, OtherActivity::class.java).apply {
        putExtra("name", "张三")
        putExtra("uid", 123)
    }
    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
        activityResult.data?.apply {
            result = getStringExtra("name").toString()
        }
    }
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = { launcher.launch(target) }) {
            Text(text = "StartActivityForResult")
        }
        Text(text = "result: $result", fontSize = 22.sp)
    }
}

可见使用方式跟之前几乎是一致的。

注:本文示例代码所有涉及到使用权限的地方需要自己提前申请好。


参考:
获取 activity 的结果

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

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

相关文章

为什么 TCP 协议有性能问题

TCP 协议可以说是今天互联网的基石&#xff0c;作为可靠的传输协议&#xff0c;在今天几乎所有的数据都会通过 TCP 协议传输&#xff0c;然而 TCP 在设计之初没有考虑到现今复杂的网络环境&#xff0c;当你在地铁上或者火车上被断断续续的网络折磨时&#xff0c;你可能都不知道…

Python 机器学习 数据归一化

众所周知机器学习使计算机从研究数据和统计数据中学习机器学习是向人工智能&#xff08;AI&#xff09;方向迈进的一步。机器学习是一个分析数据并学习预测结果的程序。此篇文章主要介绍Python机器学习的数据归一。1、数据归一化当您的数据具有不同的值&#xff0c;甚至具有不同…

一些神经网络基础知识归纳

神经网络&#xff08;NN&#xff09;复杂度 NN复杂度&#xff0c;多用NN层数和NN参数个数表示 如上图示 空间复杂度 层数隐藏层的层数1个输出层 上图为2层NN 总参数 3*44 4*2226 时间复杂度 乘加运算次数 3*44*220 指数衰减学习率 可以先用较大学习率&#xff0c;快速得…

KingbaseES V8R3数据库运维案例之---不完整的启动包(incomplete startup packet)复现

案例说明&#xff1a; 在KingbaseES V8R3数据库的sys_log日志中&#xff0c;出现以下故障信息“不完整的启动包(incomplete startup packet)”日志信息。本案例复现此日志信息发生的原因。 如下图所示&#xff0c;日志信息&#xff1a; 适用版本&#xff1a; KingbaseES V8R3 1…

计算最大公约数和最小公倍数被Java程序员用代码写出来啦

沉淀、分享、成长&#xff0c;让自己和他人都能有所收获&#xff01;&#x1f604; 一、前言 嘿&#xff0c;怎么突然讲到最大公约数了&#xff1f; 这么想你肯定是没有好好阅读前面章节中讲到的RSA算法&#xff0c;对于与欧拉结果计算的互为质数的公钥e&#xff0c;其实就需…

树莓派系统安装,网络配置,系统配置

如何安装树莓派的系统 以及 树莓派新系统SSH连接被拒绝的解决方法 1. 烧录方式1&#xff08;官方&#xff0c;简单&#xff09; 在下面网站下载 https://www.raspberrypi.com/software/ 打开以后选64位系统 选择安装的u盘 设置ssh&#xff0c;WiFi&#xff0c;登录密码等…

MySQL高级【表级锁】

1&#xff1a;表级锁1.1&#xff1a;介绍表级锁&#xff0c;每次操作锁住整张表。锁定粒度大&#xff0c;发生锁冲突的概率最高&#xff0c;并发度最低。应用在MyISAM、 InnoDB、BDB等存储引擎中。 对于表级锁&#xff0c;主要分为以下三类&#xff1a; 表锁元数据锁&#xff0…

PLC常见的输入设备及其接线方式列举

PLC常见的输入设备有按钮、行程开关、接近开关、转换开关、拨码器、各种传感器等&#xff0c;输出设备有继电器、接触器、电磁阀等。下面&#xff0c;我们来详细看看PLC如何与这些设备正确地连接输入和输出线路。1.PLC与主令电器类设备的连接下图是PLC与按钮、行程开关、转换开…

现代C++并行与并发笔记 附C++17线程池实现项目实战

文章目录让程序在特定时间休眠启动和停止线程互斥量&#xff08;mutex&#xff09;进行延迟初始化——std::call_once将执行的程序推到后台——std::async信号量&#xff08;condition_variable&#xff09;C11 线程池前置知识返回值类型推导 result_of 和 invoke_resultpackag…

天翼物联获中国信通院2022 AIoT先锋企业

近日&#xff0c;由中国信息通信研究院组织开展的2022 AIoT先锋企业评选活动成果发布&#xff0c;中国电信天翼物联凭借为AIoT发展作出的积极贡献获“2022 AIoT先锋企业”&#xff0c;是唯一获得该奖项的通信企业。 2022 AIoT先锋企业评选活动由中国信息通信研究院组织开展&…

IDEA 下载依赖包源码报错Sources not found for: org.springframework.cloud:XXX

IDEA 在使用某些类方法想看下源码时&#xff0c;由于只有 class 反编译的类文件&#xff0c;没有原始 Java 文件&#xff0c;想要将源码下载下来&#xff0c;右下角一直报一个错误 Cannot download sources Sources not found for:XXX&#xff0c;很是烦恼&#xff0c;怎么解决…

数据结构---线性表课后习题详解(朱昌杰编著)

刘佳瑜*&#xff0c;王越 *, 黄扬* , 张钊* (淮北师范大学计算机科学与技术学院&#xff0c;安徽 淮北) *These authors contributed to the work equllly and should be regarded as co-first authors. &#x1f31e;欢迎来到数据结构的世界 &#x1f308;博客主页&#xff1…

【Docker】docker部署前后端分离项目( 前:nginx + vue 后:springboot+ redis + mysql)

目录一.安装docker二.docker安装和配置nginx1.拉取nginx2.创建临时nginx容器3.从nginx容器复制 nginx.conf 文件到宿主机4.删除临时nginx容器5.启动真正的nginx容器6.查看是否挂载成功7.配置nginx.conf 和 vue的包放到指定位置三 docker安装部署redis1.安装redis2.部署redis四 …

如何计算结构体的大小?结构体内存对齐【C语言】

今天我们来讲讲结构体的大小如何来计算 其中涉及到一个结构体中的热门考点&#xff1a;结构体内存对齐 话不多说&#xff0c;开始学习&#xff01; 要想计算结构体的大小&#xff0c;首先要了解结构体的对齐规则。 目录 结构体内存对齐规则 举例 为什么存在内存对齐? 如…

测试用例该怎么设计?—— 日常加更篇(上)

&#x1f60f;作者简介&#xff1a;博主是一位测试管理者&#xff0c;同时也是一名对外企业兼职讲师。 &#x1f4e1;主页地址&#xff1a;【Austin_zhai】 &#x1f646;目的与景愿&#xff1a;旨在于能帮助更多的测试行业人员提升软硬技能&#xff0c;分享行业相关最新信息。…

火山引擎 DataTester 升级:降低产品上线风险,助力产品敏捷迭代

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;并进入官方交流群 在企业竞争加剧的今天&#xff0c;精益开发和敏捷迭代已成为产品重要的竞争力。如何保障每一次 Feature 高效迭代与安全&#xff0c;如何快速实现面对不同用户的精细化运营…

Java设计模式——单例模式

目录 一、设计模式介绍 二、设计模式类型 三、单例设计模式介绍 单例设计模式八种方式 &#xff08;一&#xff09;饿汉式&#xff08;静态常量&#xff09; &#xff08;二&#xff09;饿汉式&#xff08;静态代码块&#xff09; &#xff08;三&#xff09; 懒汉式(线程…

【Flink系列】部署篇(二):独立部署高可用Flink集群实战

服务器操作系统&#xff1a;centos7本机操作系统&#xff1a;MacFlink version: 1.15JDK version: java11HA service: ZookeeperFile System: NFS 资源分配&#xff1a; iphostnamerole10.250.0.1main0JM10.250.0.2main1JM10.250.0.3main2JM10.250.0.4worker1TM10.250.0.5wor…

Spring Cloud Eureka的使用

Spring Cloud Eureka &#x1f43b; 一个服务注册与发现的组件 &#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;ZT&#x1f604;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;…

SAP S/4HANA 采购订单处理操作详解

SAP S 4HANA Cloud 被 IDC 评为全球 SaaS 和云 ERP 系统领导者。SAP S4HANA Cloud是一套接近于零配置的系统&#xff0c;基于最佳业务实践的配置已经内嵌在标准版本中&#xff0c;可以让购买企业在第一时间内获得最全面的解决方案。本文就以其中最为常见的采购订单创建及处理流…