首页
网站首页
公司简介
资讯中心
推荐内容
返回顶部
触摸事件派发机制源码分析,Android资源动态加载思路
发布时间:2020-03-02 02:42
浏览次数:

在很多Android应用上,都有资源动态加载的功能,比如更换主题皮肤,替换聊天界面背景图片等。

本文出自 “阿敏其人” 简书博客,转载或引用请注明出处。

  • Android 6.0 & API Level 23
  • Github: Nvsleep
  • 邮箱: lizhenqiao@126.com

目前各大技术论坛出现各式各样的缓存方案,很多ORM框架,如GreenDao,但真正在项目中使用并不需要那么多功能,很多功能都是冗余的,这样增加了apk的大小,而且集成起来也比较费劲,让人很费解。

图片 1微信更换聊天窗口背景

小二,来我上两张图。

做一个轻量的缓存方案

以微信为例,当用户选择模板时,会先从网络上下载相应的图片资源,然后再替换为聊天界面的背景图片。我们知道,应用中的资源文件,包括图片,xml文件等,都是在编译的时候打包好的,那怎样才能动态加载资源呢?

来吧,我们先来看一下原版的微信发送位置嗯,是发送位置,为什么不带发送实时位置,缺个另外一个真机。嗯,买一个16年出的google亲儿子,嗯,信仰充值先想想就好!!!

  • Activity顶层窗口接受屏幕触摸事件的准备以及对输入事件到来时候的预处理;
  • ViewGroup的事件派发机制dispatchTouchEvent()分析;
  • View自身的事件派发机制dispatchTouchEvent()分析;
  • View自身onTouchEvent()方法分析;

目前网络请求拿到的数据主流是json数据,然后通过Gson、json、jackjson等框架解析出来的可用数据基本上会映射到一个java对象上,这样对java对象的缓存已成主流,有时会是一个java对象的列表。

其实有一个比较简单的思路,将需要替换的资源文件打包在一个apk文件中,动态下发到本地,然后通过重新构造Resources对象访问apk中的资源,进行本地的动态替换。主要有以下几个步骤:

图片 2微信原版发送位置.gif

一个Activity有一个PhoneWindow窗口,对应一个顶层ViewParent的实现类ViewRootImpl,窗口接受屏幕事件的准备工作是在ViewRootImpl.setView()中进行的

接下来笔者就说说如何实现一个轻量的对象缓存方案,既能缓存对象,又能缓存对象列表。

Android应用中的资源是通过AssetManager来管理的,其中addAssetPath方法可以指定资源加载路径。

接下来,再来看一下自己的程序gif

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized  { if (mView == null) { mView = view; ..... // Schedule the first layout -before- adding to the window // manager, to make sure we do the relayout before receiving // any other events from the system. // 这里先去向主线程发个消息稍后就去发起视图树的测量,布局,绘制显示工作,这样下面的操作完成后,视图窗口显示出来就可以马上接受各种输入事件了 requestLayout(); // 一般没有特别设置该窗口不能接受输入事件设置,这里if==true if ((mWindowAttributes.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { // 初始化设置一个当前窗口可以联通接受系统输入事件的的通道 mInputChannel = new InputChannel(); } try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); // 建立当前视图窗口与系统WindowManagerService服务的关联,并传入刚才创建的mInputChannel // 会在WindowManagerService服务进程为该APP窗口生成两个InputQueue,其中一个会调用InputQueue.transferTo()返回到当前APP进程窗口;另外一个保留在WindowManagerService为当前APP窗口创建的WindowState中 res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel); } catch (RemoteException e) { mAdded = false; mView = null; mAttachInfo.mRootView = null; mInputChannel = null; mFallbackEventHandler.setView; unscheduleTraversals(); setAccessibilityFocus(null, null); throw new RuntimeException("Adding window failed", e); } finally { if  { attrs.restore(); } } ..... // 很明显,view是PhoneWindow的内部类DecorView对象,而DecorView extends FrameLayout implements RootViewSurfaceTaker // 所以这里if==ture if (view instanceof RootViewSurfaceTaker) { // 创建InputQueue的create和destroy的通知对象,这里DecroView.willYouTakeTheInputQueue()一般为null mInputQueueCallback = ((RootViewSurfaceTaker)view).willYouTakeTheInputQueue(); } // 从上面知道,一般没有特别设置该窗口不能接受输入事件设置,这里mInputChannel!=null已经生成 if (mInputChannel != null) { // 一般情况DecroView.willYouTakeTheInputQueue()为null,所以这里if==false if (mInputQueueCallback != null) { mInputQueue = new InputQueue(); mInputQueueCallback.onInputQueueCreated(mInputQueue); } // 创建一个与当前窗口已经生成的InputChannel相关的接受输入事件的处理对象 mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper; } view.assignParent; mAddedTouchMode = (res & WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE) != 0; mAppVisible = (res & WindowManagerGlobal.ADD_FLAG_APP_VISIBLE) != 0; if (mAccessibilityManager.isEnabled { mAccessibilityInteractionConnectionManager.ensureConnection(); } if (view.getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } // Set up the input pipeline. // 这里设置当前各种不同类别输入事件到来时候按对应类型依次分别调用的处理对象 CharSequence counterSuffix = attrs.getTitle(); mSyntheticInputStage = new SyntheticInputStage(); InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage); InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage, "aq:native-post-ime:" + counterSuffix); InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage); InputStage imeStage = new ImeInputStage(earlyPostImeStage, "aq:ime:" + counterSuffix); InputStage viewPreImeStage = new ViewPreImeInputStage; InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage, "aq:native-pre-ime:" + counterSuffix); mFirstInputStage = nativePreImeStage; mFirstPostImeInputStage = earlyPostImeStage; mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix; } }}

保存缓存是通过将对象序列化后再base64Encode处理转成字符串然后通过SharedPreferences保存到以对象类名命名的key上。代码如下:

/** * Add an additional set of assets to the asset manager. This can be * either a directory or ZIP file. Not for use by applications. Returns * the cookie of the added asset, or 0 on failure. * {@hide} */ public final int addAssetPath(String path) { synchronized  { int res = addAssetPathNative; makeStringBlocks(mStringBlocks); return res; } }

图片 3发送位置.gif

从上面看出,在窗口初始化即ViewRootImpl.setView()中,会建立当前视图窗口体系与系WindowManagerService服务的关联,并在系统WindowManagerService服务为该窗口生成两个InputChannel输入事件通道,一个转移到当前顶层ViewParent即ViewRootImpl中,并在ViewRootImpl生成一个与输入事件通道关联的事件处理WindowInputEventReceiver内部类对象mInputEventReceiver:

 /** * Save the data to the local cache. * @author leibing * @createTime 2016/08/26 * @lastModify 2016/08/26 * @param context 上下文 * @param data 数据源 * @return */ public void save(final Context context, final T data){ ThreadManager.getInstance().getNewCachedThreadPool().execute(new Runnable() { @Override public void run() { synchronized (SpLocalCache.class) { final SharedPreferences spLc = context.getSharedPreferences( cacheName, Context.MODE_PRIVATE ); String strData = base64Encode; if (strData != null) spLc.edit() .putString(KEY_DATA, base64Encode .commit(); SharePreferenceUtil.getInstance.setSpLocalCache(cacheName); } } }); }

很显然这是个隐藏的API,所以需要通过反射来调用。

嗯哼,看完啦。

final class WindowInputEventReceiver extends InputEventReceiver { public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); } // 重写了父类onInputEvent,调用enqueueInputEvent实际处理native层返回的InputEvent输入事件 @Override public void onInputEvent(InputEvent event) { enqueueInputEvent(event, this, 0, true); } @Override public void onBatchedInputEventPending() { if (mUnbufferedInputDispatch) { super.onBatchedInputEventPending(); } else { scheduleConsumeBatchedInput(); } } @Override public void dispose() { unscheduleConsumeBatchedInput(); super.dispose(); }}// 看下WindowInputEventReceiver父类public abstract class InputEventReceiver { .... // Called from native code. @SuppressWarnings // 当输入事件到来时该方法由native层代码发起调用 private void dispatchInputEvent(int seq, InputEvent event) { mSeqMap.put(event.getSequenceNumber; onInputEvent; } /** * Called when an input event is received. * The recipient should process the input event and then call {@link #finishInputEvent} * to indicate whether the event was handled. No new input events will be received * until {@link #finishInputEvent} is called. * * @param event The input event that was received. */ public void onInputEvent(InputEvent event) { finishInputEvent(event, false); } ....}

读取缓存是通过从以对象类名命名key值上拿到字符串数据,经过base64Decode处理再反序列化后拿到对象数据,所以在写需要缓存的对象类时,记得一定要写serialVersionUID,这是为了反序列化,代码如下:

private AssetManager createAssetManager(String skinFilePath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, skinFilePath); return assetManager; } catch (Exception e) { e.printStackTrace(); return null; }}

private Resources createResources(Context context, AssetManager assetManager) { Resources superRes = context.getResources(); Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration; return resources;} 

从原版微信的gif当中,我们看到,大概可以分为这么几个行为1、进入页面,产生两个标记点,周围地点列表。两个标记点 一个是固定不变的当前位置的定位圆形标记,一个是可移动的红色标记。并且,会根据当前经纬度改变位置。地址列表 就是根据当前刚进入页面的经纬度搜索的出来的 附近poi 2、当我们点击周围点的列表,列表更新圆形绿色切换,这点没什么好说,需要注意的是地图会动态改变位置``可移动的地图上的红色标记处于地图的中心点位置3、当我们手动拖动地图,这时候两点注意当手势松开的时候,可移动红色标记移动并且居中``周围poi列表信息更新,而且第一条信息是我们当前屏幕的中心点所获取的地址,这个地址 逆地里编码 得到的,即经纬度转地址4、点击搜索条关键字,按关键字进行搜索展示列表第一个poi信息作为默认选择item``如果我们不按下item,直接返回,那么之前页面的信息不变,地址照旧``如果我们按下item,那么该item作为之前页面的poi地址列表第一条item,并且搜索附近的poi信息。

至此可以看到,一个屏幕输入事件返回处理在当前视图窗口WindowInputEventReceiver内部类的onInputEvent(InputEvent event)中,随即调用了外部类ViewRootImpl.enqueueInputEvent(event, this, 0, true),需要注意的是这里的参数flags==0

 /** * Read the data from the local cache * @author leibing * @createTime 2016/08/26 * @lastModify 2016/08/26 * @param context 上下文 * @param localCacheCallBack 回调 * @return */ public void read(final Context context, final LocalCacheCallBack localCacheCallBack){ ThreadManager.getInstance().getNewCachedThreadPool().execute(new Runnable() { @Override public void run() { synchronized (SpLocalCache.class) { SharedPreferences spLc = context.getSharedPreferences( cacheName, Context.MODE_PRIVATE ); final String strData = spLc.getString(KEY_DATA, null); if (StringUtil.isNotEmpty { final Object obj = base64Decode; mHandler.post(new Runnable() { @Override public void run() { if (localCacheCallBack != null) localCacheCallBack.readCacheComplete; } }); } } } }); }

有了Resource对象,就可以访问指定路径的资源文件,进行动态替换,示例如下:

嗯,大概这么就是介么个样子,页面简单,写成文字还是看起来巴拉巴拉一大堆的。我们的demo大概也就是围绕着上面几点展开的。

void enqueueInputEvent(InputEvent event, InputEventReceiver receiver, int flags, boolean processImmediately) { adjustInputEventForCompatibility; QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags); // Always enqueue the input event in order, regardless of its time stamp. // We do this because the application or the IME may inject key events // in response to touch events and we want to ensure that the injected keys // are processed in the order they were received and we cannot trust that // the time stamp of injected events are monotonic. QueuedInputEvent last = mPendingInputEventTail; if (last == null) { mPendingInputEventHead = q; mPendingInputEventTail = q; } else { last.mNext = q; mPendingInputEventTail = q; } mPendingInputEventCount += 1; Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName, mPendingInputEventCount); if (processImmediately) { doProcessInputEvents(); } else { scheduleProcessInputEvents(); }}

缓存对象列表其实很简单,只需要写一个固定的列表缓存对象类,里面包含一个内容为泛型的列表即可,代码如下:

public class SkinManager { private Resources mResources; /** * 获取APK资源 * @param context 上下文 * @param apkPath APK路径 */ public void loadSkinRes(Context context, String skinFilePath) { if (TextUtils.isEmpty(skinFilePath)) { return ; } try { AssetManager assetManager = createAssetManager(skinFilePath); mResources = createResources(context, assetManager); } catch (Exception e) { e.printStackTrace(); } } private AssetManager createAssetManager(String skinFilePath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, skinFilePath); return assetManager; } catch (Exception e) { e.printStackTrace(); return null; } } private Resources createResources(Context context, AssetManager assetManager) { Resources superRes = context.getResources(); Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration; return resources; } public Resources getSkinResource() { return mResources; }}
  • 当前微信版本:6.3.25
  • android高德地图版本:

    图片 4高德地图版本.png

先是获取一个指向当前事件的输入事件队列QueuedInputEvent对象,然后根据情况赋值ViewRootImpl成员变量mPendingInputEventHead或者追加到mPendingInputEventTail的mNext尾部,随即调用doProcessInputEvents()

/** * @className: ListCache * @classDescription: 列表缓存(用于缓存列表数据,减弱对sqlite的依赖) * @author: leibing * @createTime: 2016/08/26 */public class ListCache<T> implements Serializable{ // 序列化UID 当需要反序列化的时候,此UID必须要. private static final long serialVersionUID = -3276096981990292013L; // 对象列表(用于存储需要缓存下来的列表) private ArrayList<T> objList; public ArrayList<T> getObjList() { return objList; } public void setObjList(ArrayList<T> objList) { this.objList = objList; }}

在进入Activity的时候进行检查,如果有资源apk文件,则通过新的Resources对象进行资源获取。

16年9月份官网给的。

void doProcessInputEvents() { // Deliver all pending input events in the queue. while (mPendingInputEventHead != null) { QueuedInputEvent q = mPendingInputEventHead; mPendingInputEventHead = q.mNext; if (mPendingInputEventHead == null) { mPendingInputEventTail = null; } q.mNext = null; mPendingInputEventCount -= 1; Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName, mPendingInputEventCount); long eventTime = q.mEvent.getEventTimeNano(); long oldestEventTime = eventTime; if (q.mEvent instanceof MotionEvent) { MotionEvent me = (MotionEvent)q.mEvent; if (me.getHistorySize { oldestEventTime = me.getHistoricalEventTimeNano; } } mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime); deliverInputEvent; } // We are done processing all input events that we can process right now // so we can clear the pending flag immediately. if (mProcessInputEventsScheduled) { mProcessInputEventsScheduled = false; mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS); }}

童鞋们,是不是很简单?

public class MainActivity extends Activity { private Context mContext; private ImageView mBgView; private SkinManager mSkinManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mBgView = (ImageView) findViewById; mContext = this; mSkinManager = new SkinManager(); checkNewSkin(); } private void checkNewSkin() { String skinDir = "/mnt/sdcard/skin"; File file = new File; File[] skinFile = file.listFiles(); if (skinFile == null || skinFile.length == 0) { return ; } mSkinManager.loadSkinRes(mContext, skinFile[0].getAbsolutePath; if (mSkinManager.getSkinResource() != null) { mBgView.setBackgroundDrawable(mSkinManager.getSkinResource().getDrawable(R.mipmap.skin)); } }}
  • 测试机器:菊花荣耀7,Android6.0
  • IDE: AS

只要mPendingInputEventHead!=null即当前待处理事件队列还有事件需要去被处理掉,就一直循环调用deliverInputEvent()

本项目已在github开源,地址:CustomCache

这里是非常简单的处理,将编译好的资源apk文件push到本地sd卡直接加载,正常情况下应该是从网络下载,根据不同的模板名称进行资源的动态替换。

嗯,交代完毕。

private void deliverInputEvent(QueuedInputEvent q) { Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent", q.mEvent.getSequenceNumber; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0); } InputStage stage; if (q.shouldSendToSynthesizer { stage = mSyntheticInputStage; } else { stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage; } if (stage != null) { stage.deliver; } else { finishInputEvent; }}

如有疑问,请联系。

我们这里涉及到三个Activity,分别是JwActivity 得到定位,暂且叫做 A页面ShareLocActivity 发送位置页面 暂且叫做 B页面SeaechTextAddressActivity 搜索页面 暂且叫做 C页面

这里分别有两个判断,q.shouldSendToSynthesizer()和q.shouldSkipIme(),上面WindowInputEventReceiver拿到native返回的一个输入事件对象时候,调用的ViewRootImpl.enqueueInputEvent(event, this, 0, true),标记了输入事件的QueuedInputEvent对象至此为止falg==0,所以,这里一般情况调用在setView()中生成的NativePreImeInputStage mFirstInputStage对象接着去处理.从setView()方法中可知,一共生成了7个InputStage的子类对象依次接龙按事件类型对应去处理,入口是NativePreImeInputStage该子类对象,NativePreImeInputStage的顶层父类当然也是InputStage:

  • QQ:872721111
  • email:leibing1989@126.com

之前弄得工具类,客官您将就着用

abstract class InputStage { private final InputStage mNext; protected static final int FORWARD = 0; protected static final int FINISH_HANDLED = 1; protected static final int FINISH_NOT_HANDLED = 2; /** * Creates an input stage. * @param next The next stage to which events should be forwarded. */ public InputStage(InputStage next) { mNext = next; } /** * Delivers an event to be processed. */ public final void deliver(QueuedInputEvent q) { if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) { forward; } else if (shouldDropInputEvent { finish; } else { apply(q, onProcess; } } /** * Marks the the input event as finished then forwards it to the next stage. */ protected void finish(QueuedInputEvent q, boolean handled) { q.mFlags |= QueuedInputEvent.FLAG_FINISHED; if  { q.mFlags |= QueuedInputEvent.FLAG_FINISHED_HANDLED; } forward; } /** * Forwards the event to the next stage. */ protected void forward(QueuedInputEvent q) { onDeliverToNext; } /** * Applies a result code from {@link #onProcess} to the specified event. */ protected void apply(QueuedInputEvent q, int result) { if (result == FORWARD) { forward; } else if (result == FINISH_HANDLED) { finish; } else if (result == FINISH_NOT_HANDLED) { finish; } else { throw new IllegalArgumentException("Invalid result: " + result); } } /** * Called when an event is ready to be processed. * @return A result code indicating how the event was handled. */ protected int onProcess(QueuedInputEvent q) { return FORWARD; } /** * Called when an event is being delivered to the next stage. */ protected void onDeliverToNext(QueuedInputEvent q) { if (DEBUG_INPUT_STAGES) { Log.v(TAG, "Done with " + getClass().getSimpleName() + ". " + q); } if (mNext != null) { mNext.deliver; } else { finishInputEvent; } } ....}
/** * User: LJM * Date&Time: 2016-08-17 & 22:36 * Describe: 获取经纬度工具类 * * 需要权限 * <!--用于进行网络定位--> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission> <!--用于访问GPS定位--> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission> <!--获取运营商信息,用于支持提供运营商信息相关的接口--> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission> <!--用于访问wifi网络信息,wifi信息会用于进行网络定位--> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission> <!--这个权限用于获取wifi的获取权限,wifi信息会用来进行网络定位--> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"></uses-permission> <!--用于访问网络,网络定位需要上网--> <uses-permission android:name="android.permission.INTERNET"></uses-permission> <!--用于读取手机当前的状态--> <uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission> <!--写入扩展存储,向扩展卡写入数据,用于写入缓存定位数据--> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission> 需要在application 配置的mate-data 和sevice <service android:name="com.amap.api.location.APSService" > </service> <meta-data android:name="com.amap.api.v2.apikey" android:value="60f458d237f0494627e196293d49db7e"/> 另外,还需要一个key xxx.jks * */public class AMapLocUtils implements AMapLocationListener { private AMapLocationClient locationClient = null; // 定位 private AMapLocationClientOption locationOption = null; // 定位设置 @Override public void onLocationChanged(AMapLocation aMapLocation) { mLonLatListener.getLonLat(aMapLocation); locationClient.stopLocation(); locationClient.onDestroy(); locationClient = null; locationOption = null; } private LonLatListener mLonLatListener; public void getLonLat(Context context, LonLatListener lonLatListener){ locationClient = new AMapLocationClient; locationOption = new AMapLocationClientOption(); locationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);// 设置定位模式为高精度模式 locationClient.setLocationListener;// 设置定位监听 locationOption.setOnceLocation; // 单次定位 locationOption.setNeedAddress;//逆地理编码 mLonLatListener = lonLatListener;//接口 locationClient.setLocationOption(locationOption);// 设置定位参数 locationClient.startLocation(); // 启动定位 } public interface LonLatListener{ void getLonLat(AMapLocation aMapLocation); }}
  • 从构造就可以看出,每个生成的InputStage对象都会有一个成员变量mNext指向下一个处理事件的InputStage对象;
  • deliver()方法先判断该事件对象是否已经处理完成或者需要抛弃掉,都不满足则调用onProcess()处理该事件对象,处理完成后返回处理结果给apply()方法后续工作,根据onProcess()返回处理结果是否把事件传递给其mNext指向的下一个InputStage去处理;
  • 当然具体处理是在子类的onProcess()中实现的了
友情链接: 网站地图
Copyright © 2015-2019 http://www.nflfreepicks.net. 新葡萄京娱乐场网址有限公司 版权所有