Android查缺补漏(IPC篇)

Stella981
• 阅读 563

本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html

进程间通讯篇系列文章目录:

在上一篇博文中介绍了一种轻量级的跨进程通讯方案-Messenger,Messenger实现起来非常简单,其底层原理也是AIDL,更像是一个简易版的AIDL,但简单的东西往往也有其局限性,Messenger的主要作用是传递消息,它无法实现RPC功能也就是无法让我们在客户端本地就能调用远程的方法,而且Messenger是以串行的方式处理,无法同时处理多个请求,只能一个一个的处理。而AIDL就可以很好弥补Messenger的不足,虽然实现起来相对复杂一些,但它功能强大,无疑是跨进程通讯的首选方案。接下来我们先看看AIDL是什么,都可以传递哪些数据,并且本文会用一个小例子来直观的体会AIDL的实现过程。

读完本文你将深入掌握以下几个知识点:

  • AIDL是什么?
  • AIDL传递的类型。
  • 怎么创建AIDL。
  • AIDL文件中的定向tag:in、out、inout的区别。
  • 如何在AIDL中添加权限校验。

一、AIDL是什么?

AIDL全称Android Interface Definition Language,即Android接口定义语言。AIDL是Android中可以实现跨进程通讯的一种方案,通过AIDL可以实现RPC方式,所谓RPC是指远程过程调用(Remote Procedure Call),可以简单的理解为就像在本地一样方便的调动远程的方法。在Android的跨进程通讯的方案中,只有AIDL可以实现RPC方式。

二、AIDL文件支持哪些数据类型:

  • 基本数据类型:int、long、char、boolean、double等
  • String
  • CharSequence
  • ArrayList:里面每个元素也需要被AIDL支持
  • HashMap:里面的每个Key和Value也都需要被AIDL支持
  • Parcelable:所有实现了此接口的对象
  • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用

三、创建AIDL

接下类用一个小例子来说明AIDL的创建过程及用法,尽管在同一个APP内依然可以指定两个进程,但为了更能凸显“跨进程”这一点,还是决定将此示例借助于两个APP来实现,毕竟在开发中真实的需求也是发生在两个APP中。

在实现AIDL的过程中服务端APP和客户端APP中要包含结构完全相同的AIDL接口文件,包括AIDL接口所在的包名及包路径要完全一样,否则就会报错,这是因为客户端需要反序列化服务端中所有和AIDL相关的类,如果类的完整路径不一致就无法反序列化成功。

小技巧:为了更加方便的创建AIDL文件,我们可以新建一个lib工程,让客户端APP和服务端APP同时依赖这个lib,这样只需要在这个lib工程中添加AIDL文件就可以了!

简要说明一下将要实现的小例子的需求:是一个通讯录,在服务端维护一个List用来存放联系人信息,客户端可以通过RPC方式来添加联系人、获取联系列表等功能。

1、新建一个承载AIDL文件的lib(在本示例中姑且叫做libaidl)

  • 创建一个Android Library类型的Module,为了与普通的java代码作区分,在main文件夹下为AIDL文件新建一个专门的文件夹,新建工程的结构如下:

Android查缺补漏(IPC篇)

  • 然后添加AIDL接口文件:

首先新建一个Contact类,通过上面的介绍我们知道,普通的java类是不能在AIDL中使用的,必须要实现Parcelable接口,并在AIDL文件中声明:

Contact.java

/**
 * Created by liuwei on 18/2/8.
 */
public class Contact implements Parcelable {
    private int phoneNumber;
    private String name;
    private String address;

    public Contact(int phoneNumber, String name, String address) {
        this.phoneNumber = phoneNumber;
        this.name = name;
        this.address = address;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(phoneNumber);
        dest.writeString(name);
        dest.writeString(address);
    }

    private final static Creator<Contact> CREATOR = new Creator<Contact>() {
        @Override
        public Contact createFromParcel(Parcel source) {
            return new Contact(source);
        }

        @Override
        public Contact[] newArray(int size) {
            return new Contact[size];
        }
    };


    public Contact(Parcel parcel) {
        phoneNumber = parcel.readInt();
        name = parcel.readString();
        address = parcel.readString();
    }
    
    @Override
    public String toString() {
        return "Contact{" +
                "phoneNumber=" + phoneNumber +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

声明Contact类

Contact.aidl

package cn.codingblock.libaidl.contacts;
parcelable Contact;

创建AIDL接口文件,声明需要暴露给客户端的方法。

IContactsManager.aidl

package cn.codingblock.libaidl.contacts;

import cn.codingblock.libaidl.contacts.Contact;

interface IContactsManager {
    int getPhoneNumber(in String name);
    String getName(int phoneNumeber);
    Contact getContact(int phoneNumber);
    List<Contact> getContactList();
    boolean addContact(in Contact contact);
}

注:在AIDL接口文件中如果引用到了某个类,即使与这个类的AIDL声明在同一个包中也使用import导入此类。

aidl文件最终的结构如下:

Android查缺补漏(IPC篇)

  • 在本次的示例中我们的客户端APP是ipcclient工程,服务端APP是ipc工程,记得在两个工程中添加libaidl的依赖(添加依赖的方法比较简单,就不多说了),服务端工程、客户端工程、lib工程的结构如下:

Android查缺补漏(IPC篇)

小问题:AIDl文件中in、out、inout的区别?

  • in、out、inout称为AIDL接口方法参数的定向tag,代表着数据的流向。
  • in:服务端收到对象后对此对象做任何修改都不会同步给客户端。
  • out:无论客户端传过去的对象有没有提前设置值,在Binder传输过程中都会new一个空对象传递给服务端,服务端接收到的对象后对此对象所做的修改都会同步给客户端。
  • inout:服务端接受对象后,无论是客户端还是服务端对此对象所做的修改都会两端同步。
  • 基本类型的参数只能是in。

对此问题感兴趣的同学可以查看AIDL所生成的Stub源码。

2、服务端实现(在ipc工程中)

  • 创建一个Service,用于响应客户端的绑定请求,我们将这个Service名为为ContactManagerService。
  • 接着创建一个类,让这个类继承AIDL接口中的Stub类,并实现其抽象方法。在Service中返回这个新建这个类的对象。

详细实现如下:ContactManagerService.java

/**
 * Created by liuwei on 18/2/8.
 */
public class ContactManagerService extends Service {

    private final static String TAG = ContactManagerService.class.getSimpleName();

    private CopyOnWriteArrayList<Contact> contacts = new CopyOnWriteArrayList<>();

    @Override
    public void onCreate() {
        super.onCreate();
        contacts.add(new Contact(110, "报警电话", "派出所"));
        contacts.add(new Contact(119, "火警电话", "消防局"));
        contacts.add(new Contact(112, "故障电话", "保障局"));
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new ContactManagerBinder();
    }

    private class ContactManagerBinder extends IContactsManager.Stub{

        /**
         * 根据号码返回手机号
         * @param name
         * @return
         * @throws RemoteException
         */
        @Override
        public int getPhoneNumber(String name) throws RemoteException {
            if (!TextUtils.isEmpty(name)) {
                for (Contact contact:contacts) {
                    if (contact.name.equals(name)){
                        return contact.phoneNumber;
                    }
                }
            }
            return 0;
        }

        /**
         * 根据号码返回名称
         * @param phoneNumber
         * @return
         * @throws RemoteException
         */
        @Override
        public String getName(int phoneNumber) throws RemoteException {
            for (Contact contact:contacts) {
                if (contact.phoneNumber == phoneNumber){
                    return contact.name;
                }
            }
            return null;
        }

        /**
         * 根据号码返回联系人对象
         * @param phoneNumber
         * @return
         * @throws RemoteException
         */
        @Override
        public Contact getContact(int phoneNumber) throws RemoteException {
            for (Contact contact:contacts) {
                if (contact.phoneNumber == phoneNumber) {
                    return contact;
                }
            }
            return null;
        }

        /**
         * 获取联系人集合
         * @return
         * @throws RemoteException
         */
        @Override
        public List<Contact> getContactList() throws RemoteException {
            return contacts;
        }

        /**
         * 添加联系人
         * @param contact
         * @return
         * @throws RemoteException
         */
        @Override
        public boolean addContact(Contact contact) throws RemoteException {
            if (contact != null) {
                return contacts.add(contact);
            }
            return false;
        }
    }
}
  • 最后在清单文件中将此Service添加配置,并将export属性设为true以供外界调用:

上面代码很简单,值得一提的是AIDL的方法都是在服务端的Binder线程池中执行的,如果有多个客户端同时请求,就会有多个线程来操作这些方法,本次示例将存放联系人的集合采用了CopyOnWriteArrayList实现,由于CopyOnWriteArrayList本身是线程安全的,所以在此我们不需要做额外的同步处理。

==从上文我们知道,在List中AIDL只支持ArrayList的传输,那么在此处为什么可以使用CopyOnWriteArrayList呢?==

这是因为AIDL支持的是List,之所以说AIDL只支持传递ArrayList ,是因为它在传递其他List类型时就会自动将其他类型在传递之前转换成ArrayList然后再返回给服务端,也就是说无论你在服务端使用其他的任何list的子类型,在客户端接收到的类型都是ArrayList。

所以本次示例中虽然服务端返回的事CopyOnWriteArrayList,但是在Binder中会按照List的规范去读取它并最终形成一个新的ArrayList返回给客户端,类似的还有ConcurrentHashMap对应于HashMap。(其实不光CopyOnWriteArrayList,还有LinkedList等其他的List子类型也都是可以的。)

3、客户端实现(在ipcclient工程中)

  • 在客户中绑定服务端的Service,绑定成功后就可以在ServiceConnection中的onServiceConnected方法中将返回的Binder对象转换成AIDL接口所属的类型。

首先向Intent指定Component,需要传入两个参数,一个是远程Service所在工程包名,另一个是远程Service的全量限定名,然后使用bindService绑定远程Service:

Intent intent = new Intent();
intent.setComponent(new ComponentName("cn.codingblock.ipc", "cn.codingblock.ipc.aidl.contact.ContactManagerService"));
bindService(intent, serviceConnection, BIND_AUTO_CREATE);

在serviceConnection中获取返回的Binder并使用IContactsManager.Stub.asInterface()方法将Binder对象转换成IContactsManager类型。

private ServiceConnection serviceConnection = new ServiceConnection(){
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mIContactsManager = IContactsManager.Stub.asInterface(service);
        Log.i(TAG, "onServiceConnected: mIContactsManager=" + mIContactsManager);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        mIContactsManager = null;
        Log.i(TAG, "onServiceDisconnected: ");
    }
};
  • 拿到Binder对象后就可以调用在AIDL文件中声明的方法了,来看一下完整的代码:

    /**

    • Created by liuwei on 18/2/8.

    */ public class ContactMangerActivity extends AppCompatActivity { private static final String TAG = ContactMangerActivity.class.getSimpleName();

    private IContactsManager mIContactsManager;
    private EditText et_contact_name;
    private EditText et_contact_phone_number;
    private EditText et_contact_address;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact_manger);
    
        ViewUtils.findAndOnClick(this, R.id.btn_add_contact, mOnClickListener);
        ViewUtils.findAndOnClick(this, R.id.btn_get_phone_number, mOnClickListener);
        ViewUtils.findAndOnClick(this, R.id.btn_get_name, mOnClickListener);
        ViewUtils.findAndOnClick(this, R.id.btn_get_contact, mOnClickListener);
        ViewUtils.findAndOnClick(this, R.id.btn_get_list, mOnClickListener);
    
        et_contact_name = ViewUtils.find(this, R.id.et_contact_name);
        et_contact_phone_number = ViewUtils.find(this, R.id.et_contact_phone_number);
        et_contact_address = ViewUtils.find(this, R.id.et_contact_address);
    
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("cn.codingblock.ipc", "cn.codingblock.ipc.aidl.contact.ContactManagerService"));
        bindService(intent, serviceConnection, BIND_AUTO_CREATE);
        
    }
    
    private ServiceConnection serviceConnection = new ServiceConnection(){
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mIContactsManager = IContactsManager.Stub.asInterface(service);
            Log.i(TAG, "onServiceConnected: mIContactsManager=" + mIContactsManager);
        }
    
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mIContactsManager = null;
            Log.i(TAG, "onServiceDisconnected: ");
        }
    };
    
    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_add_contact:
    
                    Contact contact = new Contact(getEtContactPhoneNumber(), getEtContactName(), getEtContactAddress());
                    try {
                        mIContactsManager.addContact(contact);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
    
                    break;
    
                case R.id.btn_get_phone_number:
                    String name = getEtContactName();
    
                    try {
                        Log.i(TAG, "onClick: " + name + "的电话:" + mIContactsManager.getPhoneNumber(name));
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
    
                    break;
    
                case R.id.btn_get_name:
    
                    int number = getEtContactPhoneNumber();
    
                    try {
                        Log.i(TAG, "onClick: " + number + " 对应的名称:" + mIContactsManager.getName(number));
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
    
                    break;
    
                case R.id.btn_get_contact:
    
                    int number1 = getEtContactPhoneNumber();
    
                    try {
                        Contact contact1 = mIContactsManager.getContact(number1);
    
                        System.out.println(contact1);
    
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
    
                    break;
    
                case R.id.btn_get_list:
    
                    try {
                        List<Contact> contacts = mIContactsManager.getContactList();
    
                        System.out.println(contacts);
    
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
    
                    break;
            }
        }
    };
    
    private String getEtContactName() {
        String str = et_contact_name.getText().toString();
        if (TextUtils.isEmpty(str)) {
            Toast.makeText(this, "请输入联系人名称", Toast.LENGTH_SHORT).show();
            return null;
        }
        return str;
    }
    
    private int getEtContactPhoneNumber() {
        String str = et_contact_phone_number.getText().toString();
        if (TextUtils.isEmpty(str)) {
            Toast.makeText(this, "请输入联系人电话", Toast.LENGTH_SHORT).show();
            return 0;
        }
        return Integer.valueOf(str);
    }
    
    private String getEtContactAddress() {
        String str = et_contact_address.getText().toString();
        if (TextUtils.isEmpty(str)) {
            Toast.makeText(this, "请输入联系人地址", Toast.LENGTH_SHORT).show();
            return null;
        }
        return str;
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(serviceConnection);
    }
    

    }

布局文件也就几个EditText和Button比较简单,这里就不贴出来了,接下来运行测试一下。

四、运行测试

两端都运行后,客户端界面如下图:

Android查缺补漏(IPC篇)

查看ipcclient工程的log如下,发现已经成功绑定了远程的Service:

.../cn.codingblock.ipcclient I/ContactMangerActivity: onServiceConnected: mIContactsManager=cn.codingblock.libaidl.contacts.IContactsManager$Stub$Proxy@6b60cb6

此时查看ipc工程的log如下:

.../cn.codingblock.ipc I/ContactManagerService: onCreate: ContactManagerService started...
.../cn.codingblock.ipc I/System.out: 现有的联系人:[Contact{phoneNumber=110, name='报警电话', address='派出所'}, Contact{phoneNumber=119, name='火警电话', address='消防局'}, Contact{phoneNumber=112, name='故障电话', address='保障局'}]

通过上面两个log说明客户端和服务端已经链接成功了,接下类测试一下各按钮远程方法,在号码输入框中输入110,依次点击获取联系人名称按钮和获取联系人信息按钮,log如下:

.../cn.codingblock.ipcclient I/ContactMangerActivity: onClick: 110 对应的名称:报警电话
.../cn.codingblock.ipcclient I/System.out: Contact{phoneNumber=110, name='报警电话', address='派出所'}

接着在三个输入框里面分别输入David,111,david`s home,然后点击添加联系人信息将联系人添加到远程列表里面,在点击获取联系人列表,log如下:

.../cn.codingblock.ipcclient I/System.out: [Contact{phoneNumber=110, name='报警电话', address='派出所'}, Contact{phoneNumber=119, name='火警电话', address='消防局'}, Contact{phoneNumber=112, name='故障电话', address='保障局'}, Contact{phoneNumber=111, name='David', address='david`s home'}]

可以看到david的信息已经成功添加进来了。

五、如何为AIDL添加权限验证

其实在正式的开发工作中,我们不希望任何客户端都能绑定我们的服务端,因为这会存在极大安全隐患,所以当客户端想我们发来绑定请求是我们需要做权限校验,符合我们权限要求的客户端才可以与我们的服务端建立链接。

添加权限校验可能会有很多方法,没有对错之分,在实际开发中适合就好,接下来我们介绍一种相对来说比较方便的权限验证的方案:

  • 还是用上面的示例来说明,首先在服务端工程也就是ipc工程的清单文件中声明所需的权限,如下:

  • 然后在ContactManagerService的onBinde方法中进行权限验证,验证不通过就直接返回null。

    @Nullable @Override public IBinder onBind(Intent intent) { if (checkCallingOrSelfPermission("cn.codingblock.permission.ACCESS_CONTACT_MANAGER") == PackageManager.PERMISSION_DENIED) { Log.i(TAG, "onBind: 权限校验失败,拒绝绑定..."); return null; } Log.i(TAG, "onBind: 权限校验成功!"); return new ContactManagerBinder(); }

客户端先不做修改,运行测试一下,此时在客户端已经无法获取服务端的Binder对象,在客户端点击按钮操作时可以看到报空指针异常了:

/cn.codingblock.ipcclient E/AndroidRuntime: FATAL EXCEPTION: main
        Process: cn.codingblock.ipcclient, PID: 4726
        java.lang.NullPointerException: Attempt to invoke interface method 'java.util.List cn.codingblock.libaidl.contacts.IContactsManager.getContactList()' on a null object reference
            at cn.codingblock.ipcclient.aidl.ContactMangerActivity$2.onClick(ContactMangerActivity.java:127)
            at android.view.View.performClick(View.java:6256)
            at android.view.View$PerformClick.run(View.java:24701)
            at android.os.Handler.handleCallback(Handler.java:789)
            at android.os.Handler.dispatchMessage(Handler.java:98)
            at android.os.Looper.loop(Looper.java:164)
            at android.app.ActivityThread.main(ActivityThread.java:6541)
            at java.lang.reflect.Method.invoke(Native Method)
            at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
  • 接下来我们在客户端上加上链接服务端所需的权限:

注意:要在客户端和服务端两个工程中都加入以上声明权限和定义权限的代码。

经反复测试发现:服务端工程中声明权限和定义权限的代码缺一不可,而客户端工程中如果只加入声明权限的代码,那么如果在安装时,客户端APP先于服务端APP安装,客户端就会由于找不到定义权限而无法成功获取权限!

所以为了保险起见,将两端都同时加入定义权限的代码和声明权限的代码,当然本示例中最好的方法是直接统一加入libaidl工程中,一次加入,两端可用!

六、小结

虽然AIDL在创建的时候步骤比较繁琐,但其功能十分强大。最后概括一下AIDL的创建步骤:

在服务端:

  • 创建一个AIDL接口文件(如果用到了其他的类,要将类序列化,并在AIDL文件中声明)
  • 再创建Service用于响应客户端的绑定请求。
  • 接着创建一个类,让这个类继承AIDL接口中的Stub类,并实现其抽象方法。在Service的onBind方法中返回这个新建这个类的对象。

接着在客户端:

  • 在客户中绑定服务端的Service,绑定成功后就可以在ServiceConnection中的onServiceConnected方法中将返回的Binder对象转换成AIDL接口所属的类型。
  • 拿到Binder对象后就可以调用在AIDL文件中声明的方法了。

最后想说的是,本系列文章为博主对Android知识进行再次梳理,查缺补漏的学习过程,一方面是对自己遗忘的东西加以复习重新掌握,另一方面相信在重新学习的过程中定会有巨大的新收获,如果你也有跟我同样的想法,不妨关注我一起学习,互相探讨,共同进步!

参考文献:

  • 《Android开发艺术探索》
  • 《socket_百度百科》

源码地址:本系列文章所对应的全部源码已同步至github,感兴趣的同学可以下载查看,结合代码看文章会更好。源码传送门

本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
PhoneGap设置Icon
参考:http://cordova.apache.org/docs/en/latest/config\_ref/images.html通过config.xml中的<icon标签来设置Icon<iconsrc"res/ios/icon.png"platform"ios"width"57"height"57"densi
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这