Xamarin.Android实现App内版本更新

news2024/11/17 13:18:09

目录

  • 1、具体的效果
  • 2、代码实现
    • 2.1 基本原理
    • 2.2 开发环境
    • 2.3 具体代码
      • 2.3.1 基本设置
      • 2.3.2 系统的权限授予
      • 2.3.3 进度条的layout文件
      • 2.3.4 核心的升级文件
  • 3、代码下载
  • 4、知识点
  • 5、参考文献

1、具体的效果

有事需要在程序内集成自动更新的功能,网上找了下,改改适配下Xamarin.Android,效果如下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2、代码实现

2.1 基本原理

这个功能本质上,就是使用一个Intent打开一个apk文件进行预览。Android系统遇到预览apk文件时,就会弹出“是否进行安装更新”这类的安装框。

2.2 开发环境

VS2022,.NET7,Xamarin.Android、实体手机的Android版本:11

2.3 具体代码

2.3.1 基本设置

1、允许访问http
为了安全,从Android 7.0之后,不允许直接访问http的资源,因为我们会把安装包放在http的网络环境中,因此需要进行一个设置:在AndroidManifest.xmlapplication节点中,直接添加android:usesCleartextTraffic="true"即可。Android访问http的方案说明
2、设置FileProvider
同样,为了安全,在Android7.0之后,系统安装APP必须使用FileProvider,因此需要在AndroidManifest.xml中进行配置provider
3、权限设置
为了能够下载、存放、读取安装包,需要一系列的权限。需要在AndroidManifest.xml中进行配置

因此最终的配置文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.2" package="com.updateapp" android:installLocation="auto">
	<uses-sdk android:minSdkVersion="28" android:targetSdkVersion="33" />
	
	<!--为了能够安装apk文件,需要下面的一系列授权-->
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.INTERNET" />
	<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
	<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
	<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
	<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
	
	<application 
	android:allowBackup="true" 
	android:icon="@mipmap/ic_launcher" 
	android:label="@string/app_name" 
	android:supportsRtl="true" 
	android:theme="@style/AppTheme" 
	android:usesCleartextTraffic="true">  <!--这句话是为了可以访问http的资源-->
	
		<!--下面的配置,是为了设置FileProvider,其中用到了file_paths配置文件,具体如下-->
		<provider 
				android:name="androidx.core.content.FileProvider" 
				android:authorities="com.updateapp.fileprovider" 
				android:exported="false" 
				android:grantUriPermissions="true">
					<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths">
					</meta-data>
		</provider>
	</application>
</manifest>

Resources文件下创建xml文件夹,并创建file_paths.xml配置文件,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
	<!--安装包文件存储路径-->
	<external-files-path
        name="my_download"
        path="Download" />
	<external-path
        name="."
        path="." />
</paths>

以上,就是第一步,程序的基本配置

2.3.2 系统的权限授予

除了AndroidManifest.xml中进行配置权限外,还需要进行权限的程序判定及授权

protected override void OnCreate(Bundle savedInstanceState)
{
   base.OnCreate(savedInstanceState);
   Xamarin.Essentials.Platform.Init(this, savedInstanceState);
   SetContentView(Resource.Layout.activity_main);

   Toolbar toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
   SetSupportActionBar(toolbar);

   FloatingActionButton fab = FindViewById<FloatingActionButton>(Resource.Id.fab);
   fab.Click += FabOnClick;

   //版本跟踪,这个是和Android不一样的地方
   VersionTracking.Track();
   //初始化自动升级的功能
   autoUpdater=new AutoUpdater(this);

   try
   {
   		//6.0之后才能使用动态授权
       if(Build.VERSION.SdkInt>=BuildVersionCodes.M)
       {
           string[] permissions =
           {
               Manifest.Permission.ReadExternalStorage,
               Manifest.Permission.WriteExternalStorage,
               Manifest.Permission.AccessWifiState,
               Manifest.Permission.Internet
           };


           List<string> permissionList = new List<string>();
           for (int i = 0; i < permissions.Length; i++)
           {
               if(ActivityCompat.CheckSelfPermission(this, permissions[i])!=Permission.Granted)
               { 
                   permissionList.Add(permissions[i]);
               }
           }


           //
           if(permissionList.Count==0)
           {
               //更新程序
               autoUpdater.checkUpdate();
           }
           else
           {
               //获取授权
               ActivityCompat.RequestPermissions(this, permissions, 100);
           }
       }
   }catch(Exception e)
   {
       Toast.MakeText(this,"发生异常:"+e.Message,ToastLength.Long).Show();
   }
}


public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
    Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

    bool checkPermissionFlag = true;
    if (requestCode == 100)
    {
        for(int i = 0; i < permissions.Length; i++)
        {
            if (grantResults[i]== Permission.Granted)
            {
                checkPermissionFlag = checkPermissionFlag && true;
            }
            else
            {
                checkPermissionFlag = checkPermissionFlag && false;
            }
        }


        if(!checkPermissionFlag)
        {
            //授权程序
            Snackbar.Make(View.Inflate(this,Resource.Id.activity_main_layout,null),"需要授权",Snackbar.LengthIndefinite)
                .SetAction("ok",new Action<View>(delegate (View obj)
                           {
                               ActivityCompat.RequestPermissions(this, permissions, 100);
                           })).Show();
        }
        else
        {
            //更新程序
            Toast.MakeText(this, "授权后,可以进行更新程序啦!", ToastLength.Long).Show();
            autoUpdater.checkUpdate();
        }
    }
    base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}

2.3.3 进度条的layout文件

在layout文件夹中添加progress.xml文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

     <LinearLayout
        android:id="@+id/titleBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
 
        <TextView
            android:id="@+id/txtStatus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="状态"
            android:textSize="10sp"
            android:textStyle="normal" />
 
        <ProgressBar
            android:id="@+id/progress"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_toLeftOf="@id/txtStatus" />
    </LinearLayout>
</LinearLayout>

2.3.4 核心的升级文件

using Android.App;
using Android.Content;
using Android.Net;
using Android.OS;
using Android.Runtime;
using Android.Systems;
using Android.Util;
using Android.Views;
using Android.Widget;
using Java.IO;
using Java.Net;
using Java.Util.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Contexts;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Context = Android.Content.Context;
using Environment = Android.OS.Environment;

namespace UpdateApp
{

    public class AutoUpdater
    {
        
        private Android.App.AlertDialog confirmDialog = null; //确认是否下载的对话框
        private Android.App.AlertDialog loadingDialog = null; //正在下载的对话框
       
        public MainActivity mainActivity;
        private UpdateHandler updateHandler;

        // 保存APK的文件名
        private static string saveFileName = "my.apk";
        private static File apkFile;

        // 进度条与通知UI刷新的handler和msg常量
        public ProgressBar mProgress;
        public TextView txtStatus;
        
        public int progress;// 当前进度

        public AutoUpdater(MainActivity activity) {
            mainActivity = activity;
            updateHandler = new UpdateHandler(this);
            apkFile =new File(mainActivity.GetExternalFilesDir(Environment.DirectoryDownloads), saveFileName);
        }



        //主方法
        public void checkUpdate()
        {
            //新开启一个线程,进行下载及逻辑判断
            Task.Run(() => {

                //获取本地的版本名称(一般而言就是1.0、1.1、1.2的纯数字)
                string localVersionName = VersionTracking.CurrentVersion;

                //获取服务器的版本
                string remoteServerVersion = "2.2"; //远程获取服务器上最新版本,这儿省事儿了,直接默认取了一个较大的值

                
                if (Convert.ToDouble(localVersionName)< Convert.ToDouble(remoteServerVersion))
                {
                    //启动升级的界面
                    updateHandler.SendEmptyMessage((int)UpdateStatusEnum.BeginLoad);
                }
            });
        }


			//弹框进行下载
        public void ShowUpdateDialog()
        {
            Android.App.AlertDialog.Builder builder = null;
            builder = new Android.App.AlertDialog.Builder(mainActivity);
            confirmDialog = builder
                .SetTitle("软件版本更新")
                .SetMessage("有最新的软件包,请下载并安装!")
                .SetPositiveButton("立即下载", (s, e) => { //确定按钮及内部方法
                    ShowDownloadDialog();
                    confirmDialog.Dismiss();
                })
                .SetNegativeButton("以后再说", (s, e) => { //界面上的关闭按钮及方法
                    confirmDialog.Dismiss();
                })
                .Create();

            confirmDialog.Show();
        }


        //弹出确认下载的进度条的内容
        private void ShowDownloadDialog()
        {

            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken cancellationToken = cts.Token;

            Android.App.AlertDialog.Builder builder = null;
            builder = new Android.App.AlertDialog.Builder(mainActivity);

            View view = mainActivity.LayoutInflater.Inflate(Resource.Layout.progress, null, false);
            mProgress = view.FindViewById<ProgressBar>(Resource.Id.progress);
            txtStatus = view.FindViewById<TextView>(Resource.Id.txtStatus);

            loadingDialog = builder
                .SetView(view)
                .SetTitle("正在更新")
                .SetNegativeButton("取消下载", (s, e) => { //界面上的关闭按钮及方法
                    cts.Cancel();
                }).Create();

            loadingDialog.Show();
            DownloadApk(cancellationToken);
        }


        //下载APP
        private void DownloadApk(CancellationToken cancellationToken)
        {
            Task.Run(() => {

                try { 
                    URL url = new URL(@"http://xxx/xxx/xxx/com.updateapp.apk");//apk的网络地址
                    URLConnection conn = url.OpenConnection();
                    conn.Connect();
                    int length = conn.ContentLength;
                    System.IO.Stream ins = conn.InputStream;
                    FileOutputStream fos = new FileOutputStream(apkFile);
                    int count = 0;
                    byte[] buf = new byte[1024];

                    while (!cancellationToken.IsCancellationRequested)
                    {
                        int numread = ins.Read(buf);
                        count += numread;
                        progress = (int)(((float)count / length) * 100);

                        //下载进度
                        Message message = new Message();
                        message.What = (int)UpdateStatusEnum.Loading;
                        Bundle extras = new Bundle();
                        extras.PutInt("progress", progress);
                        message.Data = extras;
                        updateHandler.SendMessage(message);

                        if (numread <= 0)
                        {
                            Message msg = new Message();

                            //下载完成
                            msg.What = (int)UpdateStatusEnum.Finish; 
                            extras.PutInt("progress", 100);
                            msg.Data = extras;
                            updateHandler.SendMessage(msg);

                            //关闭下载框
                            if(loadingDialog!=null) loadingDialog.Dismiss();    

                            break;
                        }
                        fos.Write(buf, 0, numread);
                    }



                    fos.Close();
                    ins.Close();

                }catch (System.OperationCanceledException el)
                {
                    Log.Info("info", "用户取消了操作!"+el.Message);
                }catch (AggregateException e)
                {
                    foreach (Exception ex in e.InnerExceptions)
                    {
                        Log.Info("info", "发生异常!" + ex.Message);
                    }
                }
            }, cancellationToken);
        }

        public void installAPK()
        {
            try
            {
                if(!apkFile.Exists())
                {
                    Toast.MakeText(mainActivity, "下载的文件不存在!", ToastLength.Short).Show();
                    return;
                }

							//这儿是整个的核心
                Intent intent = new Intent();
                intent.SetAction(Intent.ActionView);
                intent.AddFlags(ActivityFlags.NewTask);
                intent.AddFlags(ActivityFlags.GrantReadUriPermission);
                intent.AddFlags(ActivityFlags.GrantWriteUriPermission);

                if (Build.VERSION.SdkInt >=Android.OS.BuildVersionCodes.N)
                {
                    string packageName = mainActivity.ApplicationContext.PackageName;
                    string authority = new StringBuilder(packageName).Append(".fileprovider").ToString();
                    Android.Net.Uri apkUri = FileProvider.GetUriForFile(mainActivity, authority, apkFile);
                    intent.SetDataAndType(apkUri, "application/vnd.android.package-archive");
                }
                else
                {
                    intent.SetDataAndType(Android.Net.Uri.FromFile(apkFile), "application/vnd.android.package-archive");
                }

                mainActivity.StartActivity(intent);
            }
            catch (Exception ex)
            {

                Toast.MakeText(mainActivity, "安装installAPK发生异常"+ex.Message, ToastLength.Short).Show();
            }
        }


    }

   //状态枚举 
    public enum UpdateStatusEnum:int
    { 
        BeginLoad=1,
        Loading=2,
        Finish=3
    }

		//Handler事件
		public class UpdateHandler : Android.OS.Handler
    {
        private WeakReference<AutoUpdater> weakReference;

        [Obsolete]
        public UpdateHandler(AutoUpdater autoUpdater)
        {
            weakReference = new WeakReference<AutoUpdater>(autoUpdater);
        }


        public override void HandleMessage(Message msg)
        {
            AutoUpdater targetActivity;

            bool isGetSuccess = weakReference.TryGetTarget(out targetActivity);
            if (isGetSuccess)
            {

                switch (msg.What)
                {
                    case (int)UpdateStatusEnum.BeginLoad:
                        targetActivity.ShowUpdateDialog();
                        break;
                    case (int)UpdateStatusEnum.Loading:

                        //获取状态数据,并进行展示
                        int progress = msg.Data.GetInt("progress");

                        targetActivity.txtStatus.SetText(progress + "%",TextView.BufferType.Normal);
                        targetActivity.mProgress.SetProgress(progress, true);
                        break;
                    case (int)UpdateStatusEnum.Finish:
                        Toast.MakeText(targetActivity.mainActivity, "下载完毕", ToastLength.Long).Show();
                        targetActivity.installAPK();
                        break;
                    default:
                        break;
                }



            }

            base.HandleMessage(msg);
        }

    }

}

上面是这个的核心

3、代码下载

代码下载

4、知识点

1、Handler的用法,C#与Java还是不同的,这里涉及到的知识点是匿名类和委托。C#的匿名类是一个field的集合,不能包含方法
2、Android中更新应用的逻辑

每个 Android 应用均有一个唯一的应用 ID,像 Java 或 Kotlin 软件包名称一样,例如 com.example.myapp。此 ID 可以作为每个应用在设备上的唯一标识。Android 设备一次只能安装一个具有指定应用 ID 的应用。
为了让 Android 平台接受更新,更新必须满足以下条件:
应用更新的应用 ID 必须与已安装应用的应用 ID 相同。
应用更新的签名证书必须与已安装应用的签名证书相同,或者必须包含有效的 proof-of-rotation。
应用更新的版本代码必须高于或等于已安装应用的版本代码。
在某些情况下,用户可能需要接受更新。
请注意,如果多个更新具有相同的签名证书并且具有相同或更高的版本代码,Android 内部并没有防范措施能够阻止不同的安装程序更新应用。
如要安装不符合上述条件的应用,用户必须先卸载当前已安装的版本,而卸载操作会清除设备上的所有应用数据。

5、参考文献

主要参考了前两个
1、Android App自动安装
2、Android APP 自动更新实现(适用Android9.0)
3、【Android】APP检测版本升级更新、apk安装
4、Andrioid FileProvider在Xamarin.Forms中的使用
5、Xamarin.Android 中 Handler 的使用

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

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

相关文章

【ARM AMBA AXI 入门 11 - AXI 总线 AWCACHE 和 ARCACHE 介绍】

文章目录 1.1 AXI 传输事务属性1.1.1 slave type1.1.2 系统级缓存 1.2 Memory Attributes1.2.1 Bufferable&#xff0c;AxCACHE[0]1.2.2 Modifiable, AxCACHE[1]1.2.3 cache-allocate 1.3 Memory types 转自&#xff1a;https://zhuanlan.zhihu.com/p/148813963 如有侵权请联系…

学习记忆——英语篇

文章目录 英语字母形象起源右脑记忆单词的原则四大步骤第一步&#xff1a;摄取信息第二步&#xff1a;处理信息第三步&#xff1a;储存信息第四步&#xff1a;提取信息 训练例子字母形象训练 右脑记忆单词5大方法字源法编码法字母编码法字母组合编码法 拼音法全拼法拼音组合 熟…

前K个高频单词-c++实现

692. 前K个高频单词 - 力扣&#xff08;LeetCode&#xff09; 给定一个单词列表 words 和一个整数 k &#xff0c;返回前 k 个出现次数最多的单词。 返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率&#xff0c; 按字典顺序 排序。 示例 1&#xff…

关于Linux服务器.sh文件启动问题

问题描述 在linux服务器上使用文本编辑&#xff08;并非vim操作&#xff09;对.sh脚本文件进行修改后无法启动&#xff0c;显示’\r’识别错误等。 错误如下&#xff1a; 错误原因 因为.sh文件在经过这种编辑后格式产生了错误&#xff0c;由unix转为了doc格式&#xff0c;需…

Ae 效果:CC Particle Systems II

模拟/CC Particle Systems II Simulation/CC Particle Systems II CC Particle Systems II&#xff08;CC 粒子系统 II&#xff09;可用于生成和模拟各种类型的粒子系统&#xff0c;包括火焰、雨、雪、爆炸、烟雾等等。 与 CC Particle World 效果相比有许多类似的属性。最大的…

华为云云耀云服务器L实例评测|部署功能强大的监控和可视化工具Grafana

应用场景 Grafana介绍 Grafana是一个功能强大的监控和可视化工具&#xff0c;适用于各种行业和应用场景&#xff0c;如IT运维监控、网络监控、能源管理、金融市场分析等。它提供了灵活的数据源支持、强大的可视化功能和告警机制&#xff0c;以及注释和过滤功能&#xff0c;使…

阿里云服务器全方位介绍_优势_使用场景_限制说明

阿里云服务器是什么&#xff1f;云服务器ECS是一种安全可靠、弹性可伸缩的云计算服务&#xff0c;云服务器可以降低IT成本提升运维效率&#xff0c;免去企业或个人前期采购IT硬件的成本&#xff0c;阿里云服务器让用户像使用水、电、天然气等公共资源一样便捷、高效地使用服务器…

使用cpolar配合Plex打造个人媒体站,畅享私人影音娱乐空间

文章目录 1.前言2. Plex网站搭建2.1 Plex下载和安装2.2 Plex网页测试2.3 cpolar的安装和注册 3. 本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1.前言 用手机或者平板电脑看视频&#xff0c;已经算是生活中稀松平常的场景了&#xff0c;特别是各…

langchain+GPT+neo4j 图数据库

neo4j版本是5.11.0,langchain的版本 0.0.288下载apoc插件 https://neo4j.com/docs/apoc/current/installation/ neo4j.conf文件把apoc.*添加到dbms.security.procedures.unrestricted配置项 使用return apoc.version()来查看是否安装成功 pip install neo4j图 参考官网&…

以太网的简单概念、MAC地址与IP地址的关系

以太网 DIX Ethernet V2标准的局域网------以太网。 IEEE 802.3标准和DIX Ethernet V2标准很相似&#xff0c;只有些许区别&#xff0c;不严格的来说&#xff0c;802.3局域网也叫做以太网。以太网是一个局域网&#xff0c;信息以广播的形式发送。 IEEE 802标准定义的局域网参…

哈工大校园网显示IP地址错误连接不上

您当前获取到的IP地址有误&#xff0c;请重新开关无线获取IP地址(注:电脑端还可以通过cmd窗口&#xff0c;输入ipconfig /release、ipconfig /renew命令)。如未解决此问题请联系网络安全和信息化办公室处理。 当校园网登录时会出现如上情况&#xff0c;并且当你按照他的方法尝试…

vue2使用vuedraggable实现拖拽删除添加重置功能

需求&#xff1a;要输入xx阶段&#xff0c;之后输入后显示但是要可以自己手动排序和删除&#xff0c;以免写错了&#xff0c;并且做了判断&#xff0c;如果重复输入的话会提示&#xff0c;不会让他添加&#xff0c;点击重置功能后一键清空所有输入 1.效果 2.下载插件 我直接下…

【Linux】自动化构建工具:make/Makefile

​&#x1f47b;内容专栏&#xff1a; Linux操作系统基础 &#x1f428;本文概括&#xff1a; 工具使用的背景、理解make/makefile工具、探索工作原理(文件修改时间的对比)、.PHONY伪目标、特性等。 &#x1f43c;本文作者&#xff1a; 阿四啊 &#x1f438;发布时间&#xff1…

18.3 【Linux】登录文件的轮替(logrotate)

18.3.1 logrotate 的配置文件 logrotate 主要是针对登录文件来进行轮替的动作&#xff0c;他必须要记载“ 在什么状态下才将登录文件进行轮替”的设置。logrotate 这个程序的参数配置文件在&#xff1a; /etc/logrotate.conf /etc/logrotate.d/ logrotate.conf 才是主要的参…

无涯教程-JavaScript - ADDRESS函数

描述 给定指定的行号和列号,您可以使用ADDRESS函数获取工作表中单元格的地址。 如,ADDRESS(2,3)返回$C $2。再举一个Example,ADDRESS(77,300)返回$KN $77。您可以使用其他函数(如ROW和COLUMN函数)为ADDRESS函数提供行号和列号参数。 语法 ADDRESS (row_num, column_num, [a…

跨境电商和TikTok广告:突破地理界限的机会

随着全球数字化的快速发展&#xff0c;跨境电商已经成为了现代商业的关键驱动力。同时&#xff0c;TikTok作为一款全球范围内广受欢迎的社交媒体平台&#xff0c;也在商业领域崭露头角。 本文将探讨跨境电商如何与TikTok广告相结合&#xff0c;为企业提供突破地理界限的机会。…

苹果宣布9月26日发布全新macOS Sonoma系统 新增不少实用功能

苹果公司在召开的特别活动中&#xff0c;宣布将于 9 月 26 日正式发布 macOS Sonoma&#xff0c;用户可以打开“设置”->“通用”->“软件更新”路径进行更新&#xff0c;新版本主要增强空间函数、为 AirPods 测试自适应音频、个性化音量和对话感知等新功能。 macOS Sono…

1.0零基础尝试DCM通讯(c-store)

前言 本项目是对医院放疗及相关设备的互通互联。对dcm文件及数据协议是本项目的基础。 今天在项目组成员支持下,对dcm通讯进行了初步的尝试,有人之路,这个过程可以说是非常愉快,于是乎准备将这个愉快的过程记录,方便自己查阅和后来人。 c-store 本次的安装和测试使用的…

乔哈里视窗:助力项目团队高效沟通

项目研发通常涉及多个团队成员、不同的职能部门和利益相关者&#xff0c;如果干系人间缺乏沟通&#xff0c;缺乏对项目目标、需求的共识和理解&#xff0c;团队成员间容易产生隔阂和矛盾&#xff0c;无法有效协调和管理&#xff0c;导致项目无法按时交付、质量下降、成本增加等…

视频怎么压缩?把视频压缩的小一点这样做

视频压缩在我们的生活和工作中有着广泛的应用需求&#xff0c;是一种减少视频文件大小的方法&#xff0c;可以给我们带来以下几个方面的作用&#xff1a; 1、减少存储空间占用&#xff1a;视频压缩可以显著减少视频的大小&#xff0c;从而腾出更多的存储空间&#xff0c;对于手…