Android 中 startService()启动service的过程分析一文中讲到了startService()启动service的过程.

我们知道启动service有两种方式:startService()和bindService().两者也可以结合使用.

接下来就通过源码来分析一下bindService()的过程.

现在假设应用程序有一个activity和一个service,然后在activity中通过bindeService()启动service,清单文件中service的process属性没有设置.也就是activity和service在一个进程中.bindService()方法的参数与startService()方法参数不同,bindService()方法参数多了一个重要的参数.

bindService(intent, ServiceConnection, Context.BIND_AUTO_CREATE);

<pre name="code" class="java">

  
private ServiceConnection mConnection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName arg0, IBinder service) {// TODO Auto-generated method stub...}@Overridepublic void onServiceDisconnected(ComponentName arg0) {...}};
通过 android应用程序使用Binder实现进程间通信 , 使用aidl工具快速在应用层实现binder进程间通信 ,这两篇文章我们知道,bindService()这种方式启动service,可以实现通过Binder实现进程间通信.

好了,进入主题

bindService()这个方法是Activty的父类ContextWrapper中的.

frameworks/base/core/Java/android/content/ContextWrapper.java

@Override    public boolean bindService(Intent service, ServiceConnection conn,            int flags) {        return mBase.bindService(service, conn, flags);    }

这里mBase变量是ContextImpl类型,是在创建activity的时候,new 一个 ContextImpl对象,赋值给activity的.

第一步:frameworks/base/core/java/android/app/ContextImpl.java

  @Override    public boolean bindService(Intent service, ServiceConnection conn,            int flags) {    ....        return bindService(service, conn, flags, UserHandle.getUserId(Process.myUid()));    }    /** @hide */    @Override    public boolean bindService(Intent service, ServiceConnection conn, int flags, int userHandle) {        IServiceConnection sd;        if (conn == null) {         ...        }        if (mPackageInfo != null) {            sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(),                    mMainThread.getHandler(), flags);        } else {           ....        }        try {            IBinder token = getActivityToken();//        ....            int res = ActivityManagerNative.getDefault().bindService(                mMainThread.getApplicationThread(), getActivityToken(),                service, service.resolveTypeIfNeeded(getContentResolver()),                sd, flags, userHandle);            if (res < 0) {             ....            }            return res != 0;        } catch (RemoteException e) {            ...        }    }

这里的mMainThread是一个ActivityThread实例,通过它的getHandler函数可以获得一个Handler对象,有了这个Handler对象后,就可以把消息分发到ActivityThread所在的线程消息队列中去了,后面我们将会看到这个用法,现在我们暂时不关注,只要知道这里从ActivityThread处获得了一个Handler并且保存在下面要介绍的ServiceDispatcher中去就可以了。

我们先看一下ActivityThread.getHandler的实现,然后再回到这里的bindService函数来

第二步: 在frameworks/base/core/java/android/app/ActivityThread.java中

 final H mH = new H();
final Handler getHandler() {        return mH;    }
这里返回的Handler是在ActivityThread类内部从Handler类继承下来的一个H类实例变量。

回到第一步中的ContextImpl.bindService函数中,获得了这个Handler对象后,就调用mPackageInfo.getServiceDispatcher函数来获得一个IServiceConnection接口,这里的mPackageInfo的类型是LoadedApk,我们来看看它的getServiceDispatcher函数的实现,然后再回到ContextImpl.bindService函数来。




第三步:getServiceDispatcher()

frameworks/base/core/java/android/app/LoadedApk.java

public final IServiceConnection getServiceDispatcher(ServiceConnection c,            Context context, Handler handler, int flags) {        synchronized (mServices) {            LoadedApk.ServiceDispatcher sd = null;            HashMap<ServiceConnection, LoadedApk.ServiceDispatcher> map = mServices.get(context);            if (map != null) {                sd = map.get(c);            }            if (sd == null) {                sd = new ServiceDispatcher(c, context, handler, flags);                if (map == null) {                    map = new HashMap<ServiceConnection, LoadedApk.ServiceDispatcher>();                    mServices.put(context, map);                }                map.put(c, sd);            } else {                sd.validate(context, handler);            }            return sd.getIServiceConnection();        }    }

先看一下关系图:


看一下ServiceDispatcher类,是LoadedApk内部类

static final class ServiceDispatcher {        private final ServiceDispatcher.InnerConnection mIServiceConnection;        private final ServiceConnection mConnection;      ....        private static class InnerConnection extends IServiceConnection.Stub {            final WeakReference<LoadedApk.ServiceDispatcher> mDispatcher;            InnerConnection(LoadedApk.ServiceDispatcher sd) {                mDispatcher = new WeakReference<LoadedApk.ServiceDispatcher>(sd);            }            public void connected(ComponentName name, IBinder service) throws RemoteException {                LoadedApk.ServiceDispatcher sd = mDispatcher.get();                if (sd != null) {                    sd.connected(name, service);                }            }        }        private final HashMap<ComponentName, ServiceDispatcher.ConnectionInfo> mActiveConnections            = new HashMap<ComponentName, ServiceDispatcher.ConnectionInfo>();        ServiceDispatcher(ServiceConnection conn,                Context context, Handler activityThread, int flags) {            mIServiceConnection = new InnerConnection(this);            mConnection = conn;            mContext = context;            mActivityThread = activityThread;            mLocation = new ServiceConnectionLeaked(null);            mLocation.fillInStackTrace();            mFlags = flags;        }        void validate(Context context, Handler activityThread) {            if (mContext != context) {        ....            }            if (mActivityThread != activityThread) {             .....            }        }      .....        IServiceConnection getIServiceConnection() {            return mIServiceConnection;        }    .....
在getServiceDispatcher函数中,传进来的参数context是一个调用bindService的activity实例,先以它为Key值在mServices中查看一下,是不是已经存在相应的ServiceDispatcher实例,如果有了,就不用创建了,直接取出来。在我们这个情景中,需要创建一个新的ServiceDispatcher。在创建新的ServiceDispatcher实例的过程中,将上面传下来ServiceConnection参数c和Hanlder参数保存在了ServiceDispatcher实例的内部,并且创建了一个InnerConnection对象,这是一个Binder对象,一会是要传递给ActivityManagerService的,ActivityManagerServic后续就是要通过这个Binder对象和ServiceConnection通信的。

函数getServiceDispatcher最后就是返回了一个InnerConnection对象给ContextImpl.bindService函数。回到ContextImpl.bindService函数中,它接着就要调用ActivityManagerService的远程接口来进一步处理了。

回到第一步

int res = ActivityManagerNative.getDefault().bindService(                mMainThread.getApplicationThread(), getActivityToken(),                service, service.resolveTypeIfNeeded(getContentResolver()),                sd, flags, userHandle);

参数getActivityToken()是与activity相关的一个Binder类型,每个activity都有一个关联的Binder对象

参考分析点击android桌面app图标启动应用程序的过程这篇文章.

service.resolveTypeIfNeeded(getContentResolver())获取这个intent的MIME类型,这里假设没有设置MIME类型 即AndroidManifest.xml没有设置Service的MIME类型,所以这里返回null.

第四步:bindService()

frameworks/base/core/java/android/app/ActivityManagerNative.java

public int bindService(IApplicationThread caller, IBinder token,            Intent service, String resolvedType, IServiceConnection connection,            int flags, int userId) throws RemoteException {        Parcel data = Parcel.obtain();        Parcel reply = Parcel.obtain();        data.writeInterfaceToken(IActivityManager.descriptor);        data.writeStrongBinder(caller != null ? caller.asBinder() : null);        data.writeStrongBinder(token);        service.writeToParcel(data, 0);        data.writeString(resolvedType);        data.writeStrongBinder(connection.asBinder());        data.writeInt(flags);        data.writeInt(userId);        mRemote.transact(BIND_SERVICE_TRANSACTION, data, reply, 0);        reply.readException();        int res = reply.readInt();        data.recycle();        reply.recycle();        return res;    }

经过Binder驱动

 case BIND_SERVICE_TRANSACTION: {            data.enforceInterface(IActivityManager.descriptor);            IBinder b = data.readStrongBinder();            IApplicationThread app = ApplicationThreadNative.asInterface(b);            IBinder token = data.readStrongBinder();            Intent service = Intent.CREATOR.createFromParcel(data);            String resolvedType = data.readString();            b = data.readStrongBinder();            int fl = data.readInt();            int userId = data.readInt();            IServiceConnection conn = IServiceConnection.Stub.asInterface(b);            int res = bindService(app, token, service, resolvedType, conn, fl, userId);            reply.writeNoException();            reply.writeInt(res);            return true;        }

上面就调用到了 ActivityManagerService中.

第五步:

frameworks/base/services/java/com/android/server/am/ActivityManagerService.java.

 public int bindService(IApplicationThread caller, IBinder token,            Intent service, String resolvedType,            IServiceConnection connection, int flags, int userId) {     ....        synchronized(this) {            return mServices.bindServiceLocked(caller, token, service, resolvedType,                    connection, flags, userId);        }    }

第六步: bindServiceLocked ()

frameworks/base/services/java/com/android/server/am/ActiveServices.java

int bindServiceLocked(IApplicationThread caller, IBinder token,            Intent service, String resolvedType,            IServiceConnection connection, int flags, int userId) {      .....        final ProcessRecord callerApp = mAm.getRecordForAppLocked(caller);        if (callerApp == null) {         ....        }        ActivityRecord activity = null;        if (token != null) {            activity = mAm.mMainStack.isInStackLocked(token);            if (activity == null) {            ....            }        }        int clientLabel = 0;        PendingIntent clientIntent = null;    .....        ServiceLookupResult res =            retrieveServiceLocked(service, resolvedType,                    Binder.getCallingPid(), Binder.getCallingUid(), userId, true);        if (res == null) {            return 0;        }        if (res.record == null) {            return -1;        }        ServiceRecord s = res.record;  ...            AppBindRecord b = s.retrieveAppBindingLocked(service, callerApp);            ConnectionRecord c = new ConnectionRecord(b, activity,                    connection, flags, clientLabel, clientIntent);            IBinder binder = connection.asBinder();            ArrayList<ConnectionRecord> clist = s.connections.get(binder);            if (clist == null) {                clist = new ArrayList<ConnectionRecord>();                s.connections.put(binder, clist);            }            clist.add(c);            b.connections.add(c);            if (activity != null) {                if (activity.connections == null) {                    activity.connections = new HashSet<ConnectionRecord>();                }                activity.connections.add(c);            }            b.client.connections.add(c);          ....            clist = mServiceConnections.get(binder);            if (clist == null) {                clist = new ArrayList<ConnectionRecord>();                mServiceConnections.put(binder, clist);            }            clist.add(c);            if ((flags&Context.BIND_AUTO_CREATE) != 0) {                s.lastActivity = SystemClock.uptimeMillis();                if (bringUpServiceLocked(s, service.getFlags(), false) != null) {                    return 0;                }            }  .....    }
函数首先根据传进来的参数token是启动service的activity在ActivityManagerService里面的一个令牌,通过这个令牌就可以将这个activity对应的的ActivityRecord取回来。

函数首先通过retrieveServiceLocked来解析service这个Intent,就是解析我们在AndroidManifest.xml定义的Service标签的intent-filter相关内容,然后将解析结果放在res.record中.

接下来,就是把传进来的参数connection封装成一个ConnectionRecord对象。注意,这里的参数connection是一个Binder对象,它的类型是LoadedApk.ServiceDispatcher.InnerConnection,是在第三步中创建的,后续ActivityManagerService就是要通过它与ServiceConnection通信,因此,这里要把这个ConnectionRecord变量c保存下来,它保在在好几个地方,都是为了后面要用时方便地取回来的,这里就不仔细去研究了,只要知道ActivityManagerService要使用它时就可以方便地把它取出来就可以了。

最后,传进来的参数flags的位Context.BIND_AUTO_CREATE为1,因此,这里会调用bringUpServiceLocked函数进一步处理。

第七步:bringUpServiceLocked();

frameworks/base/services/java/com/android/server/am/ActiveServices.java

private final String bringUpServiceLocked(ServiceRecord r,            int intentFlags, boolean whileRestarting) {     ....        final boolean isolated = (r.serviceInfo.flags&ServiceInfo.FLAG_ISOLATED_PROCESS) != 0;        final String procName = r.processName;        ProcessRecord app;        if (!isolated) {            app = mAm.getProcessRecordLocked(procName, r.appInfo.uid);            if (DEBUG_MU)                Slog.v(TAG_MU, "bringUpServiceLocked: appInfo.uid=" + r.appInfo.uid + " app=" + app);            if (app != null && app.thread != null) {                try {                    app.addPackage(r.appInfo.packageName);                    realStartServiceLocked(r, app);                    return null;                } catch (RemoteException e) {                    Slog.w(TAG, "Exception when starting service " + r.shortName, e);                }                // If a dead object exception was thrown -- fall through to                // restart the application.            }        } else {         ...        }....    }
这里service所在的进程已经启动起来了,所以接下来直接在这个进程中启动service.

第八步:realStartServiceLocked();

frameworks/base/services/java/com/android/server/am/ActiveServices.java

private final void realStartServiceLocked(ServiceRecord r,            ProcessRecord app) throws RemoteException {     ...        r.app = app;        r.restartTime = r.lastActivity = SystemClock.uptimeMillis();        app.services.add(r);        bumpServiceExecutingLocked(r, "create");        mAm.updateLruProcessLocked(app, true);        boolean created = false;        try {        ....            app.thread.scheduleCreateService(r, r.serviceInfo,                    mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo));            r.postNotification();            created = true;        } finally {    ....        }        requestServiceBindingsLocked(r);.....        sendServiceArgsLocked(r, true);    }

这里相继调用三个函数

scheduleCreateService()

requestServiceBindingsLocked()

sendServiceArgsLocked()

这里①就不分析了,和Android 中 startService()启动service的过程分析的第七步至第十步一样的.首先创建service,即service的onCreate()方法会被调用.

好了,我们看②

第九步:requestServiceBindingsLocked();

frameworks/base/services/java/com/android/server/am/ActiveServices.java

 private final void requestServiceBindingsLocked(ServiceRecord r) {        Iterator<IntentBindRecord> bindings = r.bindings.values().iterator();        while (bindings.hasNext()) {            IntentBindRecord i = bindings.next();            if (!requestServiceBindingLocked(r, i, false)) {                break;            }        }    }
private final boolean requestServiceBindingLocked(ServiceRecord r,            IntentBindRecord i, boolean rebind) {     ....        if ((!i.requested || rebind) && i.apps.size() > 0) {            try {                bumpServiceExecutingLocked(r, "bind");                r.app.thread.scheduleBindService(r, i.intent.getIntent(), rebind);                if (!rebind) {                    i.requested = true;                }                i.hasBound = true;                i.doRebind = false;            } catch (RemoteException e) {           ....            }        }        return true;    }
这里的参数r就是我们在前面的 第六步 中创建的ServiceRecord了,它代表创建的service。函数requestServiceBindingsLocked调用了requestServiceBindingLocked函数来处理绑定服务的操作,而函数requestServiceBindingLocked又调用了app.thread.scheduleBindService函数执行操作,前面我们已经介绍过app.thread,它是一个Binder对象的远程接口,类型是ApplicationThreadProxy。

第十 步:scheduleBindService();

frameworks/base/core/java/android/app/ApplicationThreadNative.java文件中,这里又是Binder进程间通信了.

 public final void scheduleBindService(IBinder token, Intent intent, boolean rebind)            throws RemoteException {        Parcel data = Parcel.obtain();        data.writeInterfaceToken(IApplicationThread.descriptor);        data.writeStrongBinder(token);        intent.writeToParcel(data, 0);        data.writeInt(rebind ? 1 : 0);        mRemote.transact(SCHEDULE_BIND_SERVICE_TRANSACTION, data, null,                IBinder.FLAG_ONEWAY);        data.recycle();    }
经过Binder驱动

 case SCHEDULE_BIND_SERVICE_TRANSACTION: {            data.enforceInterface(IApplicationThread.descriptor);            IBinder token = data.readStrongBinder();            Intent intent = Intent.CREATOR.createFromParcel(data);            boolean rebind = data.readInt() != 0;            scheduleBindService(token, intent, rebind);            return true;        }

这样就调用到了应用程序中了

第十一步:scheduleBindService();

在frameworks/base/core/java/android/app/ActivityThread.java中

 public final void scheduleBindService(IBinder token, Intent intent,                boolean rebind) {            BindServiceData s = new BindServiceData();            s.token = token;            s.intent = intent;            s.rebind = rebind;            if (DEBUG_SERVICE)                Slog.v(TAG, "scheduleBindService token=" + token + " intent=" + intent + " uid="                        + Binder.getCallingUid() + " pid=" + Binder.getCallingPid());            queueOrSendMessage(H.BIND_SERVICE, s);        }

第十二步: queueOrSendMessage();

在frameworks/base/core/java/android/app/ActivityThread.java中

 private void queueOrSendMessage(int what, Object obj) {        queueOrSendMessage(what, obj, 0, 0);    }    private void queueOrSendMessage(int what, Object obj, int arg1, int arg2) {        synchronized (this) {            if (DEBUG_MESSAGES) Slog.v(                TAG, "SCHEDULE " + what + " " + mH.codeToString(what)                + ": " + arg1 + " / " + obj);            Message msg = Message.obtain();            msg.what = what;            msg.obj = obj;            msg.arg1 = arg1;            msg.arg2 = arg2;            mH.sendMessage(msg);        }    }

第十三步: handleBindService();

在frameworks/base/core/java/android/app/ActivityThread.java中

 case BIND_SERVICE:                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind");                    handleBindService((BindServiceData)msg.obj);                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);                    break;
private void handleBindService(BindServiceData data) {        Service s = mServices.get(data.token);       ....        if (s != null) {            try {                data.intent.setExtrasClassLoader(s.getClassLoader());                try {                    if (!data.rebind) {                        IBinder binder = s.onBind(data.intent);//                        ActivityManagerNative.getDefault().publishService(                                data.token, data.intent, binder);                    } else {                     ....                    }                    ensureJitEnabled();                } catch (RemoteException ex) {                }            } catch (Exception e) {            ....            }        }    }

s.onRebind(data.intent);就会回到service的onBind()方法了,这个方法大家应该熟悉.我们会在这个回调里新建一个binder对象,然后返回给 ActivityManagerService.

第十四步:publishService();

这里通过Binder进程通信,调用到ActivityManagerService中去,参数中有上一步创建的Binder对象.

中间通信步骤就省略了,和第四步类似.

第十五步:publishService();

frameworks/base/services/java/com/android/server/am/ActivityManagerService.java.

 public void publishService(IBinder token, Intent intent, IBinder service) {       ...            mServices.publishServiceLocked((ServiceRecord)token, intent, service);        ....    }
第十六步: publishServiceLocked();
frameworks/base/services/java/com/android/server/am/ ActiveServices.java

void publishServiceLocked(ServiceRecord r, Intent intent, IBinder service) {       ...        try {          ....            if (r != null) {                Intent.FilterComparison filter                        = new Intent.FilterComparison(intent);                IntentBindRecord b = r.bindings.get(filter);                if (b != null && !b.received) {                    b.binder = service;                    b.requested = true;                    b.received = true;                    if (r.connections.size() > 0) {                        Iterator<ArrayList<ConnectionRecord>> it                                = r.connections.values().iterator();                        while (it.hasNext()) {                            ArrayList<ConnectionRecord> clist = it.next();                            for (int i=0; i<clist.size(); i++) {                                ConnectionRecord c = clist.get(i);                                if (!filter.equals(c.binding.intent.intent)) {                                  ....                                    continue;                                }                               ....                                try {                                    c.conn.connected(r.name, service);                                } catch (Exception e) {                                   ....                                }                            }                        }                    }                }             ...            }        } finally {         ...        }    }

第六步 中,我们曾经把一个ConnectionRecord放在ServiceRecord.connections列表中,因此,这里可以从r.connections中将这个ConnectionRecord取出来,每一个ConnectionRecord里面都有一个成员变量conn,它的类型是IServiceConnection,是一个Binder对象的远程接口,这个Binder对象又是什么呢?这就是我们在第三步中创建的LoadedApk.ServiceDispatcher.InnerConnection对象了。因此,这里执行c.conn.connected函数后就会进入到LoadedApk.ServiceDispatcher.InnerConnection.connected函数中去了。参数service就是onBind()方法创建的Binder对象

第十七步:InnerConnection.connected();

frameworks/base/core/java/android/app/LoadedApk.java

 static final class ServiceDispatcher {        private final ServiceDispatcher.InnerConnection mIServiceConnection;        private final ServiceConnection mConnection;    .....        private static class InnerConnection extends IServiceConnection.Stub {            final WeakReference<LoadedApk.ServiceDispatcher> mDispatcher;            InnerConnection(LoadedApk.ServiceDispatcher sd) {                mDispatcher = new WeakReference<LoadedApk.ServiceDispatcher>(sd);            }            public void connected(ComponentName name, IBinder service) throws RemoteException {                LoadedApk.ServiceDispatcher sd = mDispatcher.get();                if (sd != null) {                    sd.connected(name, service);                }            }        }
这里它将操作转发给ServiceDispatcher.connected函数。

第十七步:ServiceDispatcher.connected();

frameworks/base/core/java/android/app/LoadedApk.java

 public void connected(ComponentName name, IBinder service) {            if (mActivityThread != null) {                mActivityThread.post(new RunConnection(name, service, 0));            } else {              ...            }        }
我们在前面 第三步 中说到,这里的mActivityThread是一个Handler实例,它是通过ActivityThread.getHandler函数得到的,因此,调用它的post函数后,就会把一个消息放到ActivityThread的消息队列中去了。

第十八步:H.post;

由于H类继承于Handler类,因此,这里实际执行的Handler.post函数,这个函数定义在frameworks/base/core/java/android/os/Handler.java文件,调用了这个函数之后,这个消息就真正地进入到ActivityThread的消息队列去了,与sendMessage把消息放在消息队列不一样的地方是,post方式发送的消息不是由这个Handler的handleMessage函数来处理的,而是由post的参数Runnable的run函数来处理的。这里传给post的参数是一个RunConnection类型的参数,它继承了Runnable类,因此,最终会调用RunConnection.run函数来处理这个消息。

第十九步:RunConnection.run

frameworks/base/core/java/android/app/LoadedApk.java

 private final class RunConnection implements Runnable {            RunConnection(ComponentName name, IBinder service, int command) {                mName = name;                mService = service;                mCommand = command;            }            public void run() {                if (mCommand == 0) {                    doConnected(mName, mService);                } else if (mCommand == 1) {                    doDeath(mName, mService);                }            }            final ComponentName mName;            final IBinder mService;            final int mCommand;        }
这里的mCommand值为0,于是就执行ServiceDispatcher.doConnected函数来进一步操作了。


第二十步:ServiceDispatcher.doConnected

frameworks/base/core/java/android/app/LoadedApk.java

 public void doConnected(ComponentName name, IBinder service) {           ....            // If there is a new service, it is now connected.            if (service != null) {                mConnection.onServiceConnected(name, service);            }        }
mConnection.onServiceConnected(name, service);这个回调大家应该也很熟悉吧,这里就把service这个Binder对象传到activity中来了.

好了,到这一步,service 已经onCreate()和onBind()了,同时,绑定服务时,服务返回的Binder对象也接收到了,

然后回到第八步.③也不分析了,Android 中 startService()启动service的过程分析第十一步第十三步一样的.即service的onStart()方法和onStartCommand()会被调用.

上面就是bindService()的过程了.
























更多相关文章

  1. 一只不务正业的程序猿玩出个Processing交互库
  2. LogUtils:一个强大的Android日志管理器,支持对象、List、Map、数组
  3. Android进程-zygote进程
  4. AsyncTask的坑,Handler,Looper与MessageQueue
  5. 最详细的Android(安卓)Bitmap回收机制(从2.3到7.0,8.0)
  6. Android中图片的异步加载
  7. Android(安卓)OpenGL库加载过程源码分析
  8. Android(安卓)Property动画——ObjectAnimator使用
  9. android sqlite query用法和参数含义

随机推荐

  1. android 邮件乱码问题
  2. Android(安卓)截图实现(2)
  3. 2014年 Android 学习计划
  4. 安装 Mono for Android Visual Studio 20
  5. 安装Android 时 SDK AVD MANAGER时更新报
  6. [Android]Awind.inc系列android软件破解
  7. Android 问题汇总
  8. 自动换行
  9. Android 代码改变图片颜色android:tint="
  10. Android中让View匀速旋转