关闭

Android aidl Binder框架浅析

标签: AndroidaidlBinderonTrasact
33828人阅读 评论(47) 收藏 举报
分类:
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38461079 ,本文出自【张鸿洋的博客】

1、概述

Binder能干什么?Binder可以提供系统中任何程序都可以访问的全局服务。这个功能当然是任何系统都应该提供的,下面我们简单看一下Android的Binder的框架

Android Binder框架分为服务器接口、Binder驱动、以及客户端接口;简单想一下,需要提供一个全局服务,那么全局服务那端即是服务器接口,任何程序即客户端接口,它们之间通过一个Binder驱动访问。

服务器端接口:实际上是Binder类的对象,该对象一旦创建,内部则会启动一个隐藏线程,会接收Binder驱动发送的消息,收到消息后,会执行Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务器端代码。

Binder驱动:该对象也为Binder类的实例,客户端通过该对象访问远程服务。

客户端接口:获得Binder驱动,调用其transact()发送消息至服务器

如果大家对上述不了解,没关系,下面会通过例子来更好的说明,实践是检验真理的唯一标准嘛

2、AIDL的使用

如果对Android比较熟悉,那么一定使用过AIDL,如果你还不了解,那么也没关系,下面会使用一个例子展示AIDL的用法。

我们使用AIDL实现一个跨进程的加减法调用

1、服务端

新建一个项目,创建一个包名:com.zhy.calc.aidl,在包内创建一个ICalcAIDL文件:

package com.zhy.calc.aidl;
interface ICalcAIDL
{
	int add(int x , int y);
	int min(int x , int y );
}

注意,文件名为ICalcAIDL.aidl

然后在项目的gen目录下会生成一个ICalcAIDL.java文件,暂时不贴这个文件的代码了,后面会详细说明

然后我们在项目中新建一个Service,代码如下:

package com.example.zhy_binder;

import com.zhy.calc.aidl.ICalcAIDL;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class CalcService extends Service
{
	private static final String TAG = "server";

	public void onCreate()
	{
		Log.e(TAG, "onCreate");
	}

	public IBinder onBind(Intent t)
	{
		Log.e(TAG, "onBind");
		return mBinder;
	}

	public void onDestroy()
	{
		Log.e(TAG, "onDestroy");
		super.onDestroy();
	}

	public boolean onUnbind(Intent intent)
	{
		Log.e(TAG, "onUnbind");
		return super.onUnbind(intent);
	}

	public void onRebind(Intent intent)
	{
		Log.e(TAG, "onRebind");
		super.onRebind(intent);
	}

	private final ICalcAIDL.Stub mBinder = new ICalcAIDL.Stub()
	{

		@Override
		public int add(int x, int y) throws RemoteException
		{
			return x + y;
		}

		@Override
		public int min(int x, int y) throws RemoteException
		{
			return x - y;
		}

	};

}
在此Service中,使用生成的ICalcAIDL创建了一个mBinder的对象,并在Service的onBind方法中返回

最后记得在AndroidManifest中注册

 <service android:name="com.example.zhy_binder.CalcService" >
            <intent-filter>
                <action android:name="com.zhy.aidl.calc" />

                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </service>

这里我们指定了一个name,因为我们一会会在别的应用程序中通过Intent来查找此Service;这个不需要Activity,所以我也就没写Activity,安装完成也看不到安装图标,悄悄在后台运行着。

到此,服务端编写完毕。下面开始编写客户端

2、客户端

客户端的代码比较简单,创建一个布局,里面包含4个按钮,分别为绑定服务,解除绑定,调用加法,调用减法

布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="bindService"
        android:text="BindService" />

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="unbindService"
        android:text="UnbindService" />

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="addInvoked"
        android:text="12+12" />

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="minInvoked"
        android:text="50-12" />

</LinearLayout>

主Activity

package com.example.zhy_binder_client;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import com.zhy.calc.aidl.ICalcAIDL;

public class MainActivity extends Activity
{
	private ICalcAIDL mCalcAidl;

	private ServiceConnection mServiceConn = new ServiceConnection()
	{
		@Override
		public void onServiceDisconnected(ComponentName name)
		{
			Log.e("client", "onServiceDisconnected");
			mCalcAidl = null;
		}

		@Override
		public void onServiceConnected(ComponentName name, IBinder service)
		{
			Log.e("client", "onServiceConnected");
			mCalcAidl = ICalcAIDL.Stub.asInterface(service);
		}
	};

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

	}
	
	/**
	 * 点击BindService按钮时调用
	 * @param view
	 */
	public void bindService(View view)
	{
		Intent intent = new Intent();
		intent.setAction("com.zhy.aidl.calc");
		bindService(intent, mServiceConn, Context.BIND_AUTO_CREATE);
	}
	/**
	 * 点击unBindService按钮时调用
	 * @param view
	 */
	public void unbindService(View view)
	{
		unbindService(mServiceConn);
	}
	/**
	 * 点击12+12按钮时调用
	 * @param view
	 */
	public void addInvoked(View view) throws Exception
	{

		if (mCalcAidl != null)
		{
			int addRes = mCalcAidl.add(12, 12);
			Toast.makeText(this, addRes + "", Toast.LENGTH_SHORT).show();
		} else
		{
			Toast.makeText(this, "服务器被异常杀死,请重新绑定服务端", Toast.LENGTH_SHORT)
					.show();

		}

	}
	/**
	 * 点击50-12按钮时调用
	 * @param view
	 */
	public void minInvoked(View view) throws Exception
	{

		if (mCalcAidl != null)
		{
			int addRes = mCalcAidl.min(58, 12);
			Toast.makeText(this, addRes + "", Toast.LENGTH_SHORT).show();
		} else
		{
			Toast.makeText(this, "服务端未绑定或被异常杀死,请重新绑定服务端", Toast.LENGTH_SHORT)
					.show();

		}

	}

}

很标准的绑定服务的代码。

直接看运行结果:

我们首先点击BindService按钮,查看log

08-09 22:56:38.959: E/server(29692): onCreate
08-09 22:56:38.959: E/server(29692): onBind
08-09 22:56:38.959: E/client(29477): onServiceConnected
可以看到,点击BindService之后,服务端执行了onCreate和onBind的方法,并且客户端执行了onServiceConnected方法,标明服务器与客户端已经联通

然后点击12+12,50-12可以成功的调用服务端的代码并返回正确的结果

下面我们再点击unBindService

08-09 22:59:25.567: E/server(29692): onUnbind
08-09 22:59:25.567: E/server(29692): onDestroy
由于我们当前只有一个客户端绑定了此Service,所以Service调用了onUnbind和onDestory

然后我们继续点击12+12,50-12,通过上图可以看到,依然可以正确执行,也就是说即使onUnbind被调用,连接也是不会断开的,那么什么时候会端口呢?

即当服务端被异常终止的时候,比如我们现在在手机的正在执行的程序中找到该服务:

点击停止,此时查看log

08-09 23:04:21.433: E/client(30146): onServiceDisconnected
可以看到调用了onServiceDisconnected方法,此时连接被断开,现在点击12+12,50-12的按钮,则会弹出Toast服务端断开的提示。

说了这么多,似乎和Binder框架没什么关系,下面我们来具体看一看AIDL为什么做了些什么。

3、分析AIDL生成的代码


1、服务端

先看服务端的代码,可以看到我们服务端提供的服务是由

private final ICalcAIDL.Stub mBinder = new ICalcAIDL.Stub()
	{

		@Override
		public int add(int x, int y) throws RemoteException
		{
			return x + y;
		}

		@Override
		public int min(int x, int y) throws RemoteException
		{
			return x - y;
		}

	};

ICalcAILD.Stub来执行的,让我们来看看Stub这个类的声明:

public static abstract class Stub extends android.os.Binder implements com.zhy.calc.aidl.ICalcAIDL

清楚的看到这个类是Binder的子类,是不是符合我们文章开通所说的服务端其实是一个Binder类的实例

接下来看它的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_add:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
int _arg1;
_arg1 = data.readInt();
int _result = this.add(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
case TRANSACTION_min:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
int _arg1;
_arg1 = data.readInt();
int _result = this.min(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}

文章开头也说到服务端的Binder实例会根据客户端依靠Binder驱动发来的消息,执行onTransact方法,然后由其参数决定执行服务端的代码。

可以看到onTransact有四个参数

code , data ,replay , flags

code 是一个整形的唯一标识,用于区分执行哪个方法,客户端会传递此参数,告诉服务端执行哪个方法

data客户端传递过来的参数

replay服务器返回回去的值

flags标明是否有返回值,0为有(双向),1为没有(单向)

我们仔细看case TRANSACTION_min中的代码

data.enforceInterface(DESCRIPTOR);与客户端的writeInterfaceToken对用,标识远程服务的名称

int _arg0;
_arg0 = data.readInt();
int _arg1;
_arg1 = data.readInt();

接下来分别读取了客户端传入的两个参数

int _result = this.min(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);

然后执行this.min,即我们实现的min方法;返回result由reply写回。

add同理,可以看到服务端通过AIDL生成Stub的类,封装了服务端本来需要写的代码。

2、客户端

客户端主要通过ServiceConnected与服务端连接

private ServiceConnection mServiceConn = new ServiceConnection()
	{
		@Override
		public void onServiceDisconnected(ComponentName name)
		{
			Log.e("client", "onServiceDisconnected");
			mCalcAidl = null;
		}

		@Override
		public void onServiceConnected(ComponentName name, IBinder service)
		{
			Log.e("client", "onServiceConnected");
			mCalcAidl = ICalcAIDL.Stub.asInterface(service);
		}
	};

如果你比较敏锐,应该会猜到这个onServiceConnected中的IBinder实例,其实就是我们文章开通所说的Binder驱动,也是一个Binder实例

在ICalcAIDL.Stub.asInterface中最终调用了:

return new com.zhy.calc.aidl.ICalcAIDL.Stub.Proxy(obj);

这个Proxy实例传入了我们的Binder驱动,并且封装了我们调用服务端的代码,文章开头说,客户端会通过Binder驱动的transact()方法调用服务端代码

直接看Proxy中的add方法

@Override public int add(int x, int y) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(x);
_data.writeInt(y);
mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}

首先声明两个Parcel对象,一个用于传递数据,一个用户接收返回的数据

_data.writeInterfaceToken(DESCRIPTOR);与服务器端的enforceInterfac对应

_data.writeInt(x);
_data.writeInt(y);写入需要传递的参数

mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);

终于看到了我们的transact方法,第一个对应服务端的code,_data,_repay分别对应服务端的data,reply,0表示是双向的

_reply.readException();
_result = _reply.readInt();

最后读出我们服务端返回的数据,然后return。可以看到和服务端的onTransact基本是一行一行对应的。

到此,我们已经通过AIDL生成的代码解释了Android Binder框架的工作原理。Service的作用其实就是为我们创建Binder驱动,即服务端与客户端连接的桥梁。

AIDL其实通过我们写的aidl文件,帮助我们生成了一个接口,一个Stub类用于服务端,一个Proxy类用于客户端调用。那么我们是否可以不通过写AIDL来实现远程的通信呢?下面向大家展示如何完全不依赖AIDL来实现客户端与服务端的通信。

4、不依赖AIDL实现程序间通讯


1、服务端代码

我们新建一个CalcPlusService.java用于实现两个数的乘和除

package com.example.zhy_binder;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Log;

public class CalcPlusService extends Service
{
	private static final String DESCRIPTOR = "CalcPlusService";
	private static final String TAG = "CalcPlusService";

	public void onCreate()
	{
		Log.e(TAG, "onCreate");
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId)
	{
		Log.e(TAG, "onStartCommand");
		return super.onStartCommand(intent, flags, startId);
	}

	public IBinder onBind(Intent t)
	{
		Log.e(TAG, "onBind");
		return mBinder;
	}

	public void onDestroy()
	{
		Log.e(TAG, "onDestroy");
		super.onDestroy();
	}

	public boolean onUnbind(Intent intent)
	{
		Log.e(TAG, "onUnbind");
		return super.onUnbind(intent);
	}

	public void onRebind(Intent intent)
	{
		Log.e(TAG, "onRebind");
		super.onRebind(intent);
	}

	private MyBinder mBinder = new MyBinder();

	private class MyBinder extends Binder
	{
		@Override
		protected boolean onTransact(int code, Parcel data, Parcel reply,
				int flags) throws RemoteException
		{
			switch (code)
			{
			case 0x110:
			{
				data.enforceInterface(DESCRIPTOR);
				int _arg0;
				_arg0 = data.readInt();
				int _arg1;
				_arg1 = data.readInt();
				int _result = _arg0 * _arg1;
				reply.writeNoException();
				reply.writeInt(_result);
				return true;
			}
			case 0x111:
			{
				data.enforceInterface(DESCRIPTOR);
				int _arg0;
				_arg0 = data.readInt();
				int _arg1;
				_arg1 = data.readInt();
				int _result = _arg0 / _arg1;
				reply.writeNoException();
				reply.writeInt(_result);
				return true;
			}
			}
			return super.onTransact(code, data, reply, flags);
		}

	};

}

我们自己实现服务端,所以我们自定义了一个Binder子类,然后复写了其onTransact方法,我们指定服务的标识为CalcPlusService,然后0x110为乘,0x111为除;

记得在AndroidMenifest中注册

 <service android:name="com.example.zhy_binder.CalcPlusService" >
            <intent-filter>
                <action android:name="com.zhy.aidl.calcplus" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </service>

服务端代码结束。

2、客户端代码

单独新建了一个项目,代码和上例很类似

首先布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="bindService"
        android:text="BindService" />

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="unbindService"
        android:text="UnbindService" />

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="mulInvoked"
        android:text="50*12" />
    
    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="divInvoked"
        android:text="36/12" />

</LinearLayout>

可以看到加入了乘和除

然后是Activity的代码

package com.example.zhy_binder_client03;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends Activity
{

	private IBinder mPlusBinder;
	private ServiceConnection mServiceConnPlus = new ServiceConnection()
	{
		@Override
		public void onServiceDisconnected(ComponentName name)
		{
			Log.e("client", "mServiceConnPlus onServiceDisconnected");
		}

		@Override
		public void onServiceConnected(ComponentName name, IBinder service)
		{

			Log.e("client", " mServiceConnPlus onServiceConnected");
			mPlusBinder = service;
		}
	};

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

	}

	public void bindService(View view)
	{
		Intent intentPlus = new Intent();
		intentPlus.setAction("com.zhy.aidl.calcplus");
		boolean plus = bindService(intentPlus, mServiceConnPlus,
				Context.BIND_AUTO_CREATE);
		Log.e("plus", plus + "");
	}

	public void unbindService(View view)
	{
		unbindService(mServiceConnPlus);
	}

	public void mulInvoked(View view)
	{

		if (mPlusBinder == null)
		{
			Toast.makeText(this, "未连接服务端或服务端被异常杀死", Toast.LENGTH_SHORT).show();
		} else
		{
			android.os.Parcel _data = android.os.Parcel.obtain();
			android.os.Parcel _reply = android.os.Parcel.obtain();
			int _result;
			try
			{
				_data.writeInterfaceToken("CalcPlusService");
				_data.writeInt(50);
				_data.writeInt(12);
				mPlusBinder.transact(0x110, _data, _reply, 0);
				_reply.readException();
				_result = _reply.readInt();
				Toast.makeText(this, _result + "", Toast.LENGTH_SHORT).show();

			} catch (RemoteException e)
			{
				e.printStackTrace();
			} finally
			{
				_reply.recycle();
				_data.recycle();
			}
		}

	}
	
	public void divInvoked(View view)
	{

		if (mPlusBinder == null)
		{
			Toast.makeText(this, "未连接服务端或服务端被异常杀死", Toast.LENGTH_SHORT).show();
		} else
		{
			android.os.Parcel _data = android.os.Parcel.obtain();
			android.os.Parcel _reply = android.os.Parcel.obtain();
			int _result;
			try
			{
				_data.writeInterfaceToken("CalcPlusService");
				_data.writeInt(36);
				_data.writeInt(12);
				mPlusBinder.transact(0x111, _data, _reply, 0);
				_reply.readException();
				_result = _reply.readInt();
				Toast.makeText(this, _result + "", Toast.LENGTH_SHORT).show();

			} catch (RemoteException e)
			{
				e.printStackTrace();
			} finally
			{
				_reply.recycle();
				_data.recycle();
			}
		}

	}
}

为了明了,我直接在mulInvoked里面写了代码,和服务端都没有抽象出一个接口。首先绑定服务时,通过onServiceConnected得到Binder驱动即mPlusBinder;

然后准备数据,调用transact方法,通过code指定执行服务端哪个方法,代码和上面的分析一致。

下面看运行结果:


是不是很好的实现了我们两个应用程序间的通讯,并没有使用aidl文件,也从侧面分析了我们上述分析是正确的。


好了,就到这里,相信大家看完这篇博文,对aidl和Binder的理解也会更加深刻。


测试代码点击下载

代码先安装server端的代码,然后再安装client端的。。。











42
5
查看评论
发表评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场

AIDL与Binder的区别

Binder是一个远程对象的基础类,核心部分是远程调用机制,这部分是由IBinder定义的。 它是对IBinder类的实现,其中IBinder类提供了这样一个类的标准的本地化实现方式。 大...
  • sunny_girl_11
  • sunny_girl_11
  • 2015-11-17 11:18
  • 4026

AIDL与Binder与Messenger的使用区别!

a.只有当你需要来自不同应用的客户端通过IPC(进程间通信)通信来访问你的服务时,并且想在服务里处理多线程的业务,这时就需要使用AIDL。 b.如果你不需要同时对几个应用进程IPC操作,你最好通...
  • luojiusan520
  • luojiusan520
  • 2016-03-10 00:05
  • 1667

Android基础——初学者必知的AIDL在应用层上的Binder机制

初学者必知的AIDL在应用层上的Binder机制 事先说明: 本人也是个初学者,所以本文是从初学者的角度入手,如果有不妥的地方请留言教导我,谢谢。 本篇文章主要针对讲解AIDL的使用和AIDL在...
  • qq_30379689
  • qq_30379689
  • 2016-08-20 14:11
  • 4094

binder与aidl机制区别

binder是一个远程对象的基础类,核心部分是远程调用机制,这部分是由IBinder定义的。它是对IBinder类的实现,其中IBinder类提供这样一个类的标准的本地化实现方式。 大多数开发者不会去...
  • a2758963
  • a2758963
  • 2015-02-07 18:35
  • 1819

Android 基于Message的进程间通信 Messenger完全解析

转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/46858663; 本文出自:【张鸿洋的博客】 一、概述说到Andro...
  • lmj623565791
  • lmj623565791
  • 2015-07-23 09:58
  • 58282

Android中的Service:Binder,Messenger,AIDL(2)

前言前面一篇博文介绍了关于Service的一些基本知识,包括service是什么,怎么创建一个service,创建了一个service之后如何启动它等等。在这一篇博文里有一些需要前一篇铺垫的东西,建议...
  • luoyanglizi
  • luoyanglizi
  • 2016-06-06 11:02
  • 18574

Android 面试精华题目总结

下面的题目都是楼主在android交流群大家面试时遇到的,如果大家有好的题目或者好的见解欢迎分享,楼主将长期维护此帖。 1、请解释下在单线程模型中Message,Handler,Message Q...
  • lmj623565791
  • lmj623565791
  • 2014-04-18 14:39
  • 178909

android aidl 进程间通信需要注意msg的大小(android.os.TransactionTooLargeException)

1.bus工程实现通过service实现aidl实体类 2.actor工程通过发起bindservice,根据action去启动远程(跨进程的)bus上的aidl。 那么问题来了,我们知道,...
  • Buaaroid
  • Buaaroid
  • 2015-08-18 16:36
  • 2071

Android:学习AIDL,这一篇文章就够了(上)

前言在决定用这个标题之前甚是忐忑,主要是担心自己对AIDL的理解不够深入,到时候大家看了之后说——你这是什么玩意儿,就这么点东西就敢说够了?简直是坐井观天不知所谓——那样就很尴尬了。不过又转念一想,我...
  • luoyanglizi
  • luoyanglizi
  • 2016-07-21 10:58
  • 65880

Android AIDL使用详解

1.什么是aidl:aidl是 Android Interface definition language的缩写,一看就明白,它是一种android内部进程通信接口的描述语言,通过它我们可以定义进程间...
  • stonecao
  • stonecao
  • 2011-05-16 17:56
  • 416818
    个人资料
    • 访问:18019971次
    • 积分:51486
    • 等级:
    • 排名:第66名
    • 原创:206篇
    • 转载:0篇
    • 译文:6篇
    • 评论:15663条
    我的微信公众号
    联系方式


    QQ群:
    • 55032675
    • 423372824
    • 497438697
    • 请勿重复加群,Thx
    博客专栏
    统计