replugin宿主与插件通信小结

近来replugin开发中遇到宿主和插件间需要通信的情形,思来只有进程间通信(IPC)才是比较好的宿主与插件的通信方式。而Android进程间通信主要有2种方式:Messenger和AIDL。

AIDL(Android Interface Definition Language)是Android接口定义语言。AIDL设计用于实现Android进程间通信。Android中每个进程都拥有自己的Dalvik虚拟机(4.4之后是ART虚拟机),以及独立的内存区域,通过AIDL制定的规则,使不同进程可以进行数据交流。

宿主与插件间通信又可细分为,插件主动向宿主发起通信以及宿主主动向插件发起通信2种情况。为了模拟这2中可能的情况,同时对加深对Messenger和AIDL的理解。在宿主和插件中分别使用这两种方式,宿主作为服务端使用Messenger通信,以及插件作为服务端使用AIDL进行通信,分别查看2种方式的使用效果。

一、使用Messenger

1.1 Messenger服务端

1)服务端定义一个Service

<service android:name=".PluginComService"    android:enabled="true"    android:exported="true">  
<intent-filter>        
<action android:name="com.hk.daijun.messengerclient"></action>        
<category android:name="android.intent.category.DEFAULT" /> 
</intent-filter>

 在清单文件中配置Service,exported要配置为true,否则别的进程不能绑定该Service,在intent-filter中设置action。

2) 创建Messenger和Handler实例

创建Messenger和Handler实例,通过Handler来处理消息。

private static class MessengerHandler extends Handler {    
@Override    public void handleMessage(Message msg) {        Log.e(TAG, "handleMessage: what="+msg.what); 
  switch (msg.what) {            case 1001:                
  Log.e(TAG, "handleMessage: 服务端收到消息"+msg.getData().getString("msg"));                
  Log.e(TAG, "handleMessage: 服务端收到消息"+msg.getData().getString("msgJson"));                
  String msgJson = msg.getData().getString("msgJson");               
  Gson gson = new Gson();                
  PushMsgContent pushMsgContent = gson.fromJson(msgJson,PushMsgContent.class);         
  if (pushMsgContent != null) {                    
    Log.e(TAG, "handleMessage: PushMsgContent="+pushMsgContent.sendTime+"  "+pushMsgContent.msgContent.alarmTime); 
  }                
  Messenger mesgerReply = msg.replyTo;                
  Message msgReply = Message.obtain();                
  Bundle bundle = new Bundle();                
  bundle.putString("service_reply","hello,服务端已收到信息,想做什么");                
  msgReply.what = 2001;                
  msgReply.setData(bundle); 
  try {                    
    mesgerReply.send(msgReply);                
  } catch (RemoteException e) {                    
    Log.e(TAG, "handleMessage: 消息发给客户端,发送失败");                   
   e.printStackTrace();                
}                
break; 
}        
super.handleMessage(msg);    
}}
private Messenger messenger = new Messenger(new MessengerHandler());

 

消息传递时可以考虑使用字符串,然后服务端和客户端都用Gson进行解析,因为宿主是第三方开发,插件是自己开发,省去频繁变更接口的麻烦。

3)重写onBind方法

在Service的onBind方法中,用Messenger创建一个IBinder,Service将IBinder在onBind方法中返回给客户端。

1.2 Messenger客户端

1)创建ServiceConnection,绑定服务

客户端创建ServiceConnection实例,绑定服务端第1步中的Service,action要与服务端app中AndroidManifest中的一致,包名(服务端app包名,即AndroidManifest中查看到的包名)也要一致。

private void connectToService(){        
  Intent intent = new Intent();        
  intent.setAction("com.hk.daijun.messengerclient");  
  intent.setPackage("com.hk.daijun.replugintest");//        
  bindService(intent,serviceConnection,BIND_AUTO_CREATE);  
  RePlugin.getHostContext().bindService(intent,serviceConnection,BIND_AUTO_CREATE);    
}

此处需要主要要用宿主的上下文来绑定服务,如果直接用bindService来启动服务,能启动服务,但解绑时会报错服务未注册。

2)实例化Messenger

服务绑定后,在ServiceConnection中的onServiceConnected方法中,用IBinder来实例化Messenger(引用服务端的Handler),用此Messenger来向服务端发送消息。

3)接收服务端回复

若客户端还想从服务端那边收到回复,则需要再定义一个Messenger,messengerAccept来,在给服务端发送消息时,在Message的replyTo字段中赋值为messengerAccept。

public void sendMessageToPlugin(){    
    Log.e(TAG, "sendMessageToHost: ");    
    Message msg = Message.obtain();    
    msg.what = 1001;    
    msg.arg1 = 1001;    
    msg.replyTo = mesgerAccept;    
    Bundle bundle = new Bundle();    
    bundle.putString("msg","我要与宿主通信");          
    bundle.putString("msgJson",getJsonString());      
    msg.setData(bundle);    
    try {        
        mesgerSend.send(msg);    
    } catch (RemoteException e) {        
        e.printStackTrace();        
        Log.e(TAG, "sendMessageToHost:消息发送异常"+e.getMessage());    
    }
}

 接收服务端的回复,创建的Messenger实例mesgerAccept处理服务端的回复。

private void initAcceptMessenger() {    
mesgerAccept = new Messenger(new Handler(){  
    @Override        
    public void handleMessage(Message msg) {            
        switch (msg.what) {                
        case 2001:                    
        Log.e(TAG, "handleMessage: 收到服务端的回复");                    
        Log.e(TAG, "handleMessage: 
        service_reply="+msg.getData().getString("service_reply"));                    
        break;            
      }            
      super.handleMessage(msg);        
    }    
  });
}

4) 实现效果

首先在宿主app中进入插件,然后在插件中绑定宿主Service,然后发送消息。

 

 

二、使用AIDL

AIDL是一个缩写,全称是Android Interface Definition Language,要使用AIDL,必须创建一个定义编程接口的.aidl文件。AIDL文件可以分为两种。一种是用来定义parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类型的。一类是用来定义方法接口,以供系统使用来完成跨进程通信的。

AIDL支持的数据类型,包括Java中八种基本数据类型:byte、short、int、long、double、boolean、char,以及String、List、Map,集合里边的所有元素也必须是AIDL支持的数据类型(包括支持的Java数据类型以及自定义的数据类型)。List可以使用泛型、Map不支持泛型。

自定义数据类型,必须实现parcelable接口,还要注意定向tag。定向tag包括三种:in、out、inout。AIDL中的定向 tag 表示了在跨进程通信中数据的流向,其中 in 表示数据只能由客户端流向服务端, out 表示数据只能由服务端流向客户端,而 inout 则表示数据可在服务端与客户端之间双向流通。一般通常in就够了。

2.1 客户端(宿主)

1)新建自定义数据类型java bean

其中writeToParcel方法即表示支持定向tag为in,而如果要支持为 out 或者 inout 的定向 tag 的话,还需要实现 readFromParcel()方法,此方法不在Parcelable接口中,需要自己手写。

可以看到自定义的数据类型要实现parcelable序列化接口。因为进程是系统进行资源分配和调度的基本单位,不同的进程有不同的内存区域,java中传递对象一般是传递对象在内存堆上的引用,而由于不同进程间不能访问各自的内存区域,所以传递的引用对方进程是访问不到的。所以要将传递的数据进行序列化。在客户端对这个对象进行序列化的操作,将其中包含的数据转化为序列化流,然后将这个序列化流传输到服务端的内存中去,再在服务端对这个数据流进行反序列化的操作,还原其中包含的数据,从而实现进程间传递数据的目的。

2)增加aidl文件目录

在project视图下,在main目录下新建aidl目录,然后新建package,包名与AlarmMessage的包名路径一致。

3)添加aidl文件

 

添加后,aidl文件没在bean目录下,aidl的包名要和对应的javabean包名一致,手动移动aidl文件到bean包名下,然后修改包名,声明序列化类AlarmMessage。

 

 

4)增加aidl接口文件

增加aidl接口文件内容,这个文件里面主要内容是进程间通信的接口。注意要导入自定义数据类型的包。AIDL默认支持一些数据类型,在使用这些数据类型的时候是不需要导包的,但是除了这些类型之外的数据类型,在使用之前必须导包,就算目标文件与当前正在编写的 .aidl 文件在同一个包下。

配置完成后,将Module plugin1进行build,就会生成AlarmMessageMgr.java文件。

 

2.2 服务端(插件)

以上都是插件工程中的配置,现在要在宿主工程中也进行相应配置。配置过程也是几乎一样,需要将java文件和aidl文件都从插件工程中复制到宿主工程中,并且包路径要一样。

在将aidl文件和java bean复制到宿主工程中后。在插件中新建Service类,此次通信以插件作为服务端,而宿主app作为客户端,宿主app主动发起通信。插件中新建Service,其中AlarmMessage.Stub就是aidl文件写完后,对插件工程进行重新编译后产生java文件,在onBind中返回给客户端,客户端据此来进行通信。

public class PluginAidlService extends Service {
    private static final String TAG = "PluginAidlService";
    private List<AlarmMessage> mMsgList = new ArrayList<>();
    private final AlarmMessageMgr.Stub mAlarmMgr = new AlarmMessageMgr.Stub() {
        @Override
        public List<AlarmMessage> getAlarmMessages() throws RemoteException {
            return mMsgList;
        }
        @Override
        public void addAlarmMessage(AlarmMessage msg) throws RemoteException {
            Log.e(TAG, "plugin service addAlarmMessage: ");
            synchronized (this) {
                if (mMsgList == null) {
                    mMsgList = new ArrayList<>();
                }
                if (msg == null) {
                    Log.e(TAG, "addAlarmMessage: msg to add is null");
                    msg = new AlarmMessage();
                    msg.alarmId = 0;
                    msg.alarmTime = "1997-10-7 12:00:00";
                    msg.alarmTitle = "空报警";
                }
                mMsgList.add(msg);
            }
            int size = mMsgList.size();
            for (int i=0;i<size;i++) {
                AlarmMessage alarmMessage = mMsgList.get(i);
                Log.e(TAG, "aidl plugin1 : "+String.format("alarmTitle=%s alarmTime=%s",alarmMessage.alarmTitle,alarmMessage.alarmTime));
            }
        }
    };

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e(TAG, "onBind: ");
        return mAlarmMgr;
    }

 而客户端即宿主app通过绑定插件中的PluginAidlService服务来进行通信,需要注意的是,由于这里涉及到宿主app启动插件中的四大组件(Activity、Service、BroadcastReceiver、ContentProvider),所以不能直接使用原生的api来启动插件中的Service,需要调用Replugin中的api来启动。

    private void connectToService(){
        Log.e(TAG, "connectToService: ");
        Intent intent1 = RePlugin.createIntent("plugin1","com.hk.daijun.plugin1.PluginAidlService");
        intent1.setAction("com.hk.daijun.replugintest");
        intent1.setPackage("com.hk.daijun.plugin1");
        try {
            PluginServiceClient.bindService(PluginCommunicateActivity.this, intent1, serviceConnection, BIND_AUTO_CREATE);
        } catch (PluginClientHelper.ShouldCallSystem e) {
            Log.e(TAG, "connectToService: "+e.getMessage());
        }
    }

同样,通过bindService()来启动插件中的服务,取消绑定也需要用Replugin的api来进行Service解绑。

PluginServiceClient.unbindService(PluginCommunicateActivity.this,serviceConnection);

 客户端在Service连接建立后,调用AlarmMessageMgr.Stub.asInterface(service)将服务端返回的Binder对象转换成AIDL接口类型的对象,进而可以调用aidl定义的接口来进行调用服务端的远程方法。

private void initServiceConnection(){
    serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.e(TAG, "aidlhost onServiceConnected: ");
            mAlarmMgr = AlarmMessageMgr.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(TAG, "aidlhost onServiceDisconnected: ");
        }
    };
}

 插件重新build后,宿主以内置模式接入插件,需要将宿主工程后先clean下,清除build目录,再重新编译安装,否则宿主不先clean的话,插件的修改可能并不生效!

实现效果如下,在宿主中发送报警信息,插件中收到信息后,将所有报警信息打印出来。宿主app中主要是2个按钮,一个绑定插件服务,一个发送消息。

 

还可以模拟插件Service处理完一些逻辑后,如进行网络请求,获取第三方系统账户信息等操作,执行成功后再启动插件中的Activity,需要注意的是,在Service中启动Activity,必须设置FLAG_ACTIVITY_NEW_TASK标签,但FLAG_ACTIVITY_NEW_TASK标签必须配合taskAffinity属性使用,如果不设置taskAffinity属性值,将不会生成新task。所以不设置taskAffinity就不会生成新的task。

三、 AIDL原理

掠下aidl实现进程间通信的主要原理。AIDL的底层是Binder,其本质就是为我们提供了一种快速实现Binder的工具,通过定义AIDL,android studio编译后生成了AlarmMessageMgr.java文件,而原理的关键也就是这个文件,这个文件其实也可以通过手动编写。

public static com.hk.daijun.plugin1.AlarmMessageMgr asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.hk.daijun.plugin1.AlarmMessageMgr))) {
return ((com.hk.daijun.plugin1.AlarmMessageMgr)iin);
}
return new com.hk.daijun.plugin1.AlarmMessageMgr.Stub.Proxy(obj);
}

 asInteface方法用于将服务器的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于统一进程,那么返回服务器的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。

然后看下addAlarmMessage(AlarmMessage msg)方法的调用过程,转到方法的实现。

@Override public void addAlarmMessage(com.hk.daijun.plugin1.bean.AlarmMessage msg) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((msg!=null)) {
_data.writeInt(1);
msg.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addAlarmMessage, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}

 这个方法由客户端调用,运行在客户端,输入的参数msg定向tag是in,代表数据只能由客户端流向服务端。参数信息写入_data中,然后调用transact方法发起RPC(远程过程调用),mRemote就是Binder对象在binder驱动层对应的引用。transact方法调启后,客户端当前线程被挂起,服务端的onTransact方法会被调用。

@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getAlarmMessages:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.hk.daijun.plugin1.bean.AlarmMessage> _result = this.getAlarmMessages();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addAlarmMessage:
{
data.enforceInterface(DESCRIPTOR);
com.hk.daijun.plugin1.bean.AlarmMessage _arg0;
if ((0!=data.readInt())) {
_arg0 = com.hk.daijun.plugin1.bean.AlarmMessage.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addAlarmMessage(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}

 onTransact方法中4个参数根据参数code,来确定调用哪个方法,data代表输入参数,reply代表返回值。客户端调用服务端的远程方法跟调用本地方法差不多,客户端发起的请求要挂起直到服务端进程处理完,所以远程方法如果是比较耗时的,客户端最好不要在UI线程中发起这个远程方法调用,而应该放到子线程中进行。而服务端的Binder方法是运行在Binder的线程池中。

四、总结

Messenger是执行进程间通信(IPC)最简单的方式,服务端Messenger通过Handler将客户端的请求放到消息循环中排队,然后逐个取出进行处理,而客户端要接收结果需要2个Messenger,一个用于发送消息,一个用于接收消息。其底层原理也是Binder,客户端得到服务端的Binder对象在binder驱动层对应的mRemote引用,然后给服务器发消息,实现跨进程通信。这种方式缺点很明显,服务端以串行方式处理客户端的消息,消息处理结果反馈并不及时,不适合服务端、客户端经常通信的场景。而用AIDL可以并发处理客户端的远程调用,远程方法调用就像本地调用一样,立即执行后就有结果返回。但Messenger的特点是简单,不需要想AIDL一样,需要定义aidl文件,还需要将aidl文件拷贝到服务端和客户端。由于引入了Replugin,使用Messenger、AIDL启动宿主/插件的Service,启动方式有所区别,使用时需要注意。AIDL的底层是Binder,其本质只是为我们提供了一种快速实现Binder的工具。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ChatGPT团队中,3个清华学霸,1个北大学霸,共9位华人

众所周知&#xff0c;美国硅谷其实有着众多的华人&#xff0c;哪怕是芯片领域&#xff0c;华为也有着一席之地&#xff0c;比如AMD 的 CEO 苏姿丰、Nvidia 的 CEO 黄仁勋 都是华人。 还有更多的美国著名的科技企业中&#xff0c;都有着华人的身影&#xff0c;这些华人&#xff…

Java入坑之类的派生与继承

一、继承 1.1继承的概念 Java中的继承&#xff1a;子类就是享有父类的属性和方法&#xff0c;并且还存在一定的属性和方法的扩展。 Subclass&#xff0c;从另一个类派生出的类&#xff0c;称为子类(派生类&#xff0c;扩展类等) Superclass&#xff0c;派生子类的类&#xff…

3.5 函数的极值与最大值和最小值

学习目标&#xff1a; 我要学习函数的极值、最大值和最小值&#xff0c;我会采取以下几个步骤&#xff1a; 理解基本概念&#xff1a;首先&#xff0c;我会理解函数的极值、最大值和最小值的概念。例如&#xff0c;我会学习函数在特定区间内的最高点和最低点&#xff0c;并且理…

( “树” 之 DFS) 104. 二叉树的最大深度 ——【Leetcode每日一题】

104. 二叉树的最大深度 给定一个二叉树&#xff0c;找出其最大深度。 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 说明: 叶子节点是指没有子节点的节点。 示例&#xff1a; 给定二叉树 [3,9,20,null,null,15,7]&#xff0c; 返回它的最大深度 3 。 思路&am…

激光和相机的标定

一、手动标定 代码工程&#xff1a;GitHub - Livox-SDK/livox_camera_lidar_calibration: Calibrate the extrinsic parameters between Livox LiDAR and camera 这是Livox提供的手动校准Livox雷达和相机之间外参的方法&#xff0c;并在Mid-40&#xff0c;Horizon和Tele-15上进…

ReactNative入门

React基本用法&#xff1a; react与js不同的点在于 react使用的是虚拟DOM js是真实DOM 作用&#xff1a;当有新的数据填充 可以复用之前的&#xff0c;而js需要整体重新渲染 创建虚拟DOM还可以使用jsx语法直接声明&#xff1a; 注意要用babel标签将jsx转化为js 但是建议采用j…

图解并用 C 语言实现非比较排序(计数排序、桶排序和基数排序)

目录 一、计数排序 二、桶排序 三、基数排序 一、计数排序 算法步骤&#xff1a; 找出待排序数组 arr 中的最小值和最大值&#xff08;分别用 min 和 max 表示&#xff09;。 创建一个长度为 max - min 1、元素初始值全为 0 的计数器数组 count。 扫描一遍原始数组&…

2023 年嵌入式世界的3 大趋势分析

目录 大家好&#xff0c;本文讲解了嵌入式发展的3个大趋势&#xff0c;分享给大家。 趋势#1 – Visual Studio Code Integration 趋势#2 –支持“现代”软件流程 趋势 #3 – 在设计中利用 AI 和 ML 结论 大家好&#xff0c;本文讲解了嵌入式发展的3个大趋势&#xff0c;分享…

Python圈的普罗米修斯——一套近乎完善的监控系统

文章目录前言一、怎么采集监控数据&#xff1f;二、采集的数据结构与指标类型2.1 数据结构2.2 指标类型2.3 实例概念2.4.数据可视化2.5.应用前景总结前言 普罗米修斯(Prometheus)是一个SoundCloud公司开源的监控系统。当年&#xff0c;由于SoundCloud公司生产了太多的服务&…

网络安全实战之植入后门程序

在 VMware 上建立两个虚拟机&#xff1a;win7 和 kali。 Kali&#xff1a;它是 Linux 发行版的操作系统&#xff0c;它拥有超过 300 个渗透测试工具&#xff0c;就不用自己再去找安装包&#xff0c;去安装到我们自己的电脑上了&#xff0c;毕竟自己从网上找到&#xff0c;也不…

如何把数据库中的数据显示到页面

主要内容&#xff1a;使用JDBC访问数据库中数据&#xff08;Java Web数据可视化案例&#xff09; 文章目录前期准备&#xff1a;案例&#xff1a;第一步&#xff1a;创建数据库及数据第二步&#xff1a;编写实体类第三步&#xff1a;编写Dao类第四步&#xff1a;编写Servlet代码…

springboot集成hadoop3.2.4HDFS

前言 记录springboot集成hadoop3.2.4版本&#xff0c;并且调用HDFS的相关接口&#xff0c;这里就不展示springboot工程的建立了&#xff0c;这个你们自己去建工程很多教程。 一、springboot配置文件修改 1.1 pom文件修改 <!-- hadoop依赖 --><dependency><gro…

Stable Diffusion - API和微服务开发

Stable Diffusion 是一种尖端的开源工具&#xff0c;用于从文本生成图像。 Stable Diffusion Web UI 通过 API 和交互式 UI 打开了许多这些功能。 我们将首先介绍如何使用此 API&#xff0c;然后设置一个示例&#xff0c;将其用作隐私保护微服务以从图像中删除人物。 推荐&…

一种轻量的“虚拟机”——Windows 沙盒模式

Windows 沙盒模式Windows沙盒的好处操作步骤Windows沙盒的好处 相比虚拟机和第三方的沙盒软件&#xff0c;Windows Sandbox启用后仅占用100MB硬盘空间&#xff0c;还能与物理机安全地共享部分内存空间。简单来说就是易用、免费、不卡机&#xff01; 由于要保证沙盒内的数据不…

(九)【软件设计师】计算机系统-浮点数习题

文章目录一、2009年下半年第3、4题二、2011年上半年第5题三、2012年下半年第3题四、2015年上半年第1题五、2015年下半年第3题六、2016年下半年第3题七、2018年上半年第1题八、2020年下半年第3题知识点回顾 &#xff08;八&#xff09;【软件设计师】计算机系统—浮点数一、2009…

Android13 PMS是如何启动的?

作者&#xff1a;Arthas0v0 平常使用安卓实际就是在使用各种app&#xff0c;而下载的app实际是一个apk文件。这个apk文件的安装就交给了PackageManagerService来实现。PackageManagerService的启动也是在SystemServer中。这个过程比较长需要长一点的时间来理。 SystemServer.s…

ORACLE EBS 系统架构与应用实践(一)

一、从ERP到EBS 从上世纪70年代晚期的物料需求计划MRP&#xff08;Material Requirements Planning&#xff09;到80年代的MRP II&#xff0c;再到90年代的企业资源计划ERP&#xff08;Enterprise Resource Planning&#xff09;&#xff0c;企业管理软件&#xff08;或曰应用…

u盘里的文件被自动删除了怎么办?五种数据恢复方案

u盘是我们日常生活中常常用到的一种便携式存储设备&#xff0c;可以帮助我们存储和携带大量的文件信息。但是&#xff0c;使用过程中难免会遇到一些问题&#xff0c;例如u盘会自己删除文件的情况&#xff0c;如果你遇到了这种情况&#xff0c;该怎样找回u盘自己删除的文件呢&am…

AI 芯片的简要发展历史

随着人工智能领域不断取得突破性进展。作为实现人工智能技术的重要基石&#xff0c;AI芯片拥有巨大的产业价值和战略地位。作为人工智能产业链的关键环节和硬件基础&#xff0c;AI芯片有着极高的技术研发和创新的壁垒。从芯片发展的趋势来看&#xff0c;现在仍处于AI芯片发展的…

FFMPEG: [ API ] >打开/关闭一个输入文件

它们是成对出现的. ffmpeg 把输入文件--转换成--->AVFormatContext实例 ◆ avformat_open_input() int avformat_open_input(AVFormatContext ** ps,const char * url,ff_const59 AVInputFormat * fmt,AVDictionary ** options )Open an input stream and read the header.…
最新文章