当前位置: 首页 > news >正文

android_viewtracker 原理

一、说明

我们业务中大部分场景是用 RecyclerView 实现的列表,而 View 的曝光通常是直接写在 adapter 的 onBindViewHolder 中,这样就会导致 item 还没显示出来的时候就会触发曝光。最近业务提出需要实现根据 View 显示在屏幕上面积大于 80% 才算曝光。网上搜索后看到阿里天猫使用了这个库 android_viewtracker,也大概看了一下其实现原理,在此记录一下。

仓库链接:android_viewtracker

二、原理

初始化

private void initTracker() {/*** SDK的初始化** @param mContext              全局的application* @param mTrackerOpen          是否开启无痕点击埋点* @param mTrackerExposureOpen  是否开启无痕曝光埋点* @param printLog              是否输出调试log*/TrackerManager.getInstance().init(mApplication, true, true, true);TrackerManager.getInstance().setCommit(new IDataCommit() {@Overridepublic void commitClickEvent(HashMap<String, Object> commonInfo, String viewName, HashMap<String, Object> viewData) {Log.e("aa", ">>>>>>>>>>>>> commitExposureEvent " + viewName);}@Overridepublic void commitExposureEvent(HashMap<String, Object> commonInfo, String viewName, HashMap<String, Object> viewData, long exposureData, HashMap<String, Object> exposureIndex) {Log.e("aa", ">>>>>>>>>>>>> commitExposureEvent " + viewName + " viewData = " + viewData + " exposureData = " + exposureData + " exposureIndex = " + exposureIndex);}});JSONObject exposureConfig = new JSONObject();try {exposureConfig.put("timeThreshold", 500); // 曝光时间阈值exposureConfig.put("dimThreshold", 0.8); // 曝光比例exposureConfig.put("masterSwitch", true); // 是否打开无痕点击事件上报exposureConfig.put("batchOpen", false); // 是否打开批量上报,即页面离开时,所有view上报一次曝光总时长exposureConfig.put("exposureSampling", 100); // 曝光采样率} catch (Exception e) {// 如果发生put操作异常,走默认值}Intent intent = new Intent(ConfigReceiver.ACTION_CONFIG_CHANGED);intent.putExtra(ConfigReceiver.VIEWTRACKER_EXPOSURE_CONFIG_KEY, exposureConfig.toString());mApplication.sendBroadcast(intent);}

走进初始化代码

    /*** initiate viewtracker SDK** @param application         global application context* @param trackerOpen         whether or not track click event* @param trackerExposureOpen whether or not track exposure event* @param logOpen             whether or not print the log*/public void init(Application application, boolean trackerOpen, boolean trackerExposureOpen, boolean logOpen) {GlobalsContext.mApplication = application;GlobalsContext.trackerOpen = trackerOpen;GlobalsContext.trackerExposureOpen = trackerExposureOpen;GlobalsContext.logOpen = logOpen;if (GlobalsContext.trackerOpen || GlobalsContext.trackerExposureOpen) {mActivityLifecycle = new ActivityLifecycleForTracker();application.registerActivityLifecycleCallbacks(mActivityLifecycle);}}

这儿做了 2 件事:

  1. 把一些配置信息传入给 SDK 保存起来。
  2. 注册了 Activity 生命周期监听,并由 ActivityLifecycleForTracker 管理。

生命周期监听

private class ActivityLifecycleForTracker implements Application.ActivityLifecycleCallbacks {@Overridepublic void onActivityCreated(Activity activity, Bundle bundle) {}@Overridepublic void onActivityStarted(Activity activity) {}@Overridepublic void onActivityResumed(Activity activity) {TrackerLog.d("onActivityResumed activity " + activity.toString());attachTrackerFrameLayout(activity);}@Overridepublic void onActivityPaused(Activity activity) {if (GlobalsContext.trackerExposureOpen) {TrackerLog.d("onActivityPaused activity " + activity.toString());if (GlobalsContext.batchOpen) {batchReport();}}}@Overridepublic void onActivityStopped(Activity activity) {}@Overridepublic void onActivityDestroyed(Activity activity) {TrackerLog.d("onActivityDestroyed activity " + activity.toString());detachTrackerFrameLayout(activity);}@Overridepublic void onActivitySaveInstanceState(Activity activity, Bundle bundle) {}}

这儿分别监听了 Activity 的 3 个生命周期,并做了一些事,根据方法名称我们猜测:

  1. onResume:关联 TrackerFrameLayout。
  2. onPause:批量上报。
  3. onDestroy:分离 TrackerFrameLayout。

关联 TrackerFrameLayout

public void attachTrackerFrameLayout(Activity activity) {// this is a problem: several activity exist in the TabActivityif (activity == null || activity instanceof TabActivity) {return;}// exist android.R.id.content not found crashtry {ViewGroup container = (ViewGroup) activity.findViewById(android.R.id.content);if (container == null) {return;}if (container.getChildCount() > 0) {View root = container.getChildAt(0);if (root instanceof TrackerFrameLayout) {TrackerLog.d("no attachTrackerFrameLayout " + activity.toString());} else {TrackerFrameLayout trackerFrameLayout = new TrackerFrameLayout(activity);while (container.getChildCount() > 0) {View view = container.getChildAt(0);container.removeViewAt(0);trackerFrameLayout.addView(view, view.getLayoutParams());}container.addView(trackerFrameLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));}}} catch (Exception e) {TrackerLog.e(e.toString());}}

这儿做了几个事:

  1. 创建 TrackerFrameLayout。
  2. 将 Activity 下 id 为 android.R.id.content 的 View 中的子 View 移除。
  3. 将所有移除的子 View 添加到 TrackerFrameLayout。

总得来说就是中间插了一个 TrackerFrameLayout。

批量上报

    /*** commit the data for exposure event in batch*/private void batchReport() {long time = System.currentTimeMillis();Handler handler = ExposureManager.getInstance().getExposureHandler();Message message = handler.obtainMessage();message.what = ExposureManager.BATCH_COMMIT_EXPOSURE;handler.sendMessage(message);TrackerLog.v("batch report exposure views " + (System.currentTimeMillis() - time) + "ms");}
private ExposureManager() {HandlerThread exposureThread = new HandlerThread("ViewTracker_exposure");exposureThread.start();exposureHandler = new Handler(exposureThread.getLooper(), new Handler.Callback() {@Overridepublic boolean handleMessage(Message msg) {switch (msg.what) {...case BATCH_COMMIT_EXPOSURE:for (CommitLog commitLog : commitLogs.values()) {// the exposure times inside pagecommitLog.argsInfo.put("exposureTimes", String.valueOf(commitLog.exposureTimes));// Scene 3 (switch back and forth when press Home button) is excluded.TrackerUtil.commitExtendEvent(commitLog.pageName, 2201, commitLog.viewName, null, String.valueOf(commitLog.totalDuration), commitLog.argsInfo);TrackerLog.v("onActivityPaused batch commit " + "pageName=" + commitLog.pageName + ",viewName=" + commitLog.viewName+ ",totalDuration=" + commitLog.totalDuration + ",args=" + commitLog.argsInfo.toString());}// clear after committed.commitLogs.clear();break;default:break;}return false;}});}

可以看到这儿只是对之前积累的数据做了一次批量上报。

分离 TrackerFrameLayout

private void detachTrackerFrameLayout(Activity activity) {if (activity == null || activity instanceof TabActivity) {return;}try {ViewGroup container = (ViewGroup) activity.findViewById(android.R.id.content);if (container == null) {return;}if (container.getChildAt(0) instanceof TrackerFrameLayout) {container.removeViewAt(0);}} catch (Exception e) {TrackerLog.e(e.toString());}}

这个比较简单,就是将 TrackerFrameLayout 给移除掉。

TrackerFrameLayout 解析

TrackerFrameLayout 虽然代码量不大,但却是最核心的一个类,所有的监听都是通过它来的,我们着重看一下这块儿。

/*** the parent layout of content view inside Activity* Created by mengliu on 16/6/14.*/
public class TrackerFrameLayout extends FrameLayout implements GestureDetector.OnGestureListener {/*** Custom threshold is used to determine whether it is a click event,* When the user moves more than 20 pixels in screen, it is considered as the scrolling event instead of a click.*/private static final float CLICK_LIMIT = 20;/*** the X Position*/private float mOriX;/*** the Y Position*/private float mOriY;private GestureDetector mGestureDetector;private ReuseLayoutHook mReuseLayoutHook;/*** common info attached with the view inside page*/public HashMap<String, Object> commonInfo = new HashMap<String, Object>();/*** all the visible views inside page, key is viewName*/private Map<String, ExposureModel> lastVisibleViewMap = new ArrayMap<String, ExposureModel>();private long lastOnLayoutSystemTimeMillis = 0;private int focusChangeCount = 0;public TrackerFrameLayout(Context context) {super(context);this.mGestureDetector = new GestureDetector(context, this);this.mReuseLayoutHook = new ReuseLayoutHook(this, commonInfo);// after the onActivityResumedCommonHelper.addCommonArgsInfo(this);}public TrackerFrameLayout(Context context, AttributeSet attrs) {super(context, attrs);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {mGestureDetector.onTouchEvent(ev);if (getContext() != null && getContext() instanceof Activity) {// trigger the click eventClickManager.getInstance().eventAspect((Activity) getContext(), ev, commonInfo);}switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:mOriX = ev.getX();mOriY = ev.getY();break;case MotionEvent.ACTION_MOVE:if ((Math.abs(ev.getX() - mOriX) > CLICK_LIMIT) || (Math.abs(ev.getY() - mOriY) > CLICK_LIMIT)) {// Scene 1: Scroll beginninglong time = System.currentTimeMillis();TrackerLog.v("dispatchTouchEvent triggerViewCalculate begin ");ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, this, commonInfo, lastVisibleViewMap);TrackerLog.v("dispatchTouchEvent triggerViewCalculate end costTime=" + (System.currentTimeMillis() - time));} else {TrackerLog.d("dispatchTouchEvent ACTION_MOVE but not in click limit");}break;case MotionEvent.ACTION_UP:break;}return super.dispatchTouchEvent(ev);}public Map<String, ExposureModel> getLastVisibleViewMap() {return lastVisibleViewMap;}/*** all the state change of view trigger the exposure event** @param changed* @param left* @param top* @param right* @param bottom*/@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {TrackerLog.v("onLayout traverseViewTree begin");// duplicate message in 1slong time = System.currentTimeMillis();if (time - lastOnLayoutSystemTimeMillis > 1000) {lastOnLayoutSystemTimeMillis = time;CommonHelper.addCommonArgsInfo(this);TrackerLog.v("onLayout addCommonArgsInfo");ExposureManager.getInstance().traverseViewTree(this, mReuseLayoutHook);}
//        ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, this, commonInfo, lastVisibleViewMap);TrackerLog.v("onLayout traverseViewTree end costTime=" + (System.currentTimeMillis() - time));super.onLayout(changed, left, top, right, bottom);}@Overridepublic boolean onDown(MotionEvent motionEvent) {TrackerLog.v("onDown");return false;}@Overridepublic void onShowPress(MotionEvent motionEvent) {TrackerLog.v("onShowPress");}@Overridepublic boolean onSingleTapUp(MotionEvent motionEvent) {TrackerLog.v("onSingleTapUp");return false;}@Overridepublic void onLongPress(MotionEvent motionEvent) {TrackerLog.v("onLongPress");}@Overridepublic boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {return false;}/*** Scene 2: Scroll ending** @param motionEvent* @param motionEvent1* @param v* @param v1* @return*/@Overridepublic boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {long time = System.currentTimeMillis();TrackerLog.v("onFling triggerViewCalculate begin");this.postDelayed(new Runnable() {@Overridepublic void run() {ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, TrackerFrameLayout.this, commonInfo, lastVisibleViewMap);}}, 1000);TrackerLog.v("onFling triggerViewCalculate end costTime=" + (System.currentTimeMillis() - time));return false;}/*** the state change of window trigger the exposure event.* Scene 3: switch back and forth when press Home button.* Scene 4: enter into the next page* Scene 5: window replace** @param hasFocus*/@Overridepublic void dispatchWindowFocusChanged(boolean hasFocus) {TrackerLog.v("dispatchWindowFocusChanged triggerViewCalculate begin");long ts = System.currentTimeMillis();
//        if (hasFocus && focusChangeCount > 0) {
//            Clog.d("setupExposeInit_window_focus_change::" + focusChangeCount);
//            ExposureManager.getInstance().setupExpose(this);
//        }
//        focusChangeCount++;TrackerLog.v("dispatchWindowFocusChanged triggerViewCalculate end costTime=" + (System.currentTimeMillis() - ts));super.dispatchWindowFocusChanged(hasFocus);}@Overrideprotected void dispatchVisibilityChanged(View changedView, int visibility) {// Scene 6: switch page in the TabActivityif (visibility == View.GONE) {TrackerLog.v("dispatchVisibilityChanged triggerViewCalculate begin");long ts = System.currentTimeMillis();ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_WINDOW_CHANGED, this, commonInfo, lastVisibleViewMap);TrackerLog.v("dispatchVisibilityChanged triggerViewCalculate end costTime=" + (System.currentTimeMillis() - ts));} else {TrackerLog.v("trigger dispatchVisibilityChanged, visibility =" + visibility);}super.dispatchVisibilityChanged(changedView, visibility);}/*** 主动触发一次曝光检测* */public void manualTriggerCalculate() {ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, this, commonInfo, lastVisibleViewMap);}
}

我们接着拆开来看一下:

public class TrackerFrameLayout extends FrameLayout implements GestureDetector.OnGestureListener {    private GestureDetector mGestureDetector;   public TrackerFrameLayout(Context context) {super(context);this.mGestureDetector = new GestureDetector(context, this);this.mReuseLayoutHook = new ReuseLayoutHook(this, commonInfo);// after the onActivityResumedCommonHelper.addCommonArgsInfo(this);} @Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {mGestureDetector.onTouchEvent(ev);if (getContext() != null && getContext() instanceof Activity) {// trigger the click eventClickManager.getInstance().eventAspect((Activity) getContext(), ev, commonInfo);}switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:mOriX = ev.getX();mOriY = ev.getY();break;case MotionEvent.ACTION_MOVE:if ((Math.abs(ev.getX() - mOriX) > CLICK_LIMIT) || (Math.abs(ev.getY() - mOriY) > CLICK_LIMIT)) {// Scene 1: Scroll beginninglong time = System.currentTimeMillis();TrackerLog.v("dispatchTouchEvent triggerViewCalculate begin ");ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, this, commonInfo, lastVisibleViewMap);TrackerLog.v("dispatchTouchEvent triggerViewCalculate end costTime=" + (System.currentTimeMillis() - time));} else {TrackerLog.d("dispatchTouchEvent ACTION_MOVE but not in click limit");}break;case MotionEvent.ACTION_UP:break;}return super.dispatchTouchEvent(ev);}
}

触摸事件在到达你的 View 之前,都会先经过 TrackerFrameLayout 分发,在这儿监听并处理了手势事件,它通过手势识别器分析了手势类型,并将手势传回给 TrackerFrameLayout 进行处理。

我们先接着往下看这行代码做了什么:

ClickManager.getInstance().eventAspect((Activity) getContext(), ev, commonInfo);

    /*** find the clicked view, register the View.AccessibilityDelegate, commit data when trigger the click event.** @param activity* @param event*/public void eventAspect(Activity activity, MotionEvent event, HashMap<String, Object> commonInfo) {GlobalsContext.start = System.currentTimeMillis();if (!GlobalsContext.trackerOpen) {return;}if (activity == null) {return;}// sample not hitif (isSampleHit == null) {isSampleHit = CommonHelper.isSamplingHit(GlobalsContext.sampling);}if (!isSampleHit) {TrackerLog.d("click isSampleHit is false");return;}try {if (event.getAction() == MotionEvent.ACTION_DOWN) {handleViewClick(activity, event, commonInfo);}} catch (Throwable th) {TrackerLog.e(th.getMessage());}}

这儿做了 2 个事情:

  1. 判断采样率:通过随机数操作采样率,如果超出采样率则不进行处理。
  2. handleViewClick:处理点击事件。

接着往下看:

private void handleViewClick(Activity activity, MotionEvent event, HashMap<String, Object> commonInfo) {View view = activity.getWindow().getDecorView();View tagView = null;View clickView = getClickView(view, event, tagView);if (clickView != null) {if (mDelegate != null) {mDelegate.setCommonInfo(commonInfo);}clickView.setAccessibilityDelegate(mDelegate);}}

getClickView 是找到点击的 View。

setAccessibilityDelegate 是给 View 设置了代理,这样可以监听到 View 的事件,并做出一些处理。

public class ViewDelegate extends View.AccessibilityDelegate {private HashMap<String, Object> commonInfo = new HashMap<String, Object>();public void setCommonInfo(HashMap<String, Object> commonInfo) {this.commonInfo = commonInfo;}public void sendAccessibilityEvent(View clickView, int eventType) {TrackerLog.d("eventType: " + eventType);if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) {TrackerLog.d("click: " + clickView);DataProcess.processClickParams(commonInfo, clickView);}super.sendAccessibilityEvent(clickView, eventType);}
}

我们可以看到这儿是监听了 View 的点击事件,并做了点击上报。

这儿巧妙的利用了 setAccessibilityDelegate 实现了 View 的点击监听。

我们再回到 dispatchTouchEvent 方法中接着往下看。

case MotionEvent.ACTION_MOVE:if ((Math.abs(ev.getX() - mOriX) > CLICK_LIMIT) || (Math.abs(ev.getY() - mOriY) > CLICK_LIMIT)) {// Scene 1: Scroll beginninglong time = System.currentTimeMillis();TrackerLog.v("dispatchTouchEvent triggerViewCalculate begin ");ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, this, commonInfo, lastVisibleViewMap);TrackerLog.v("dispatchTouchEvent triggerViewCalculate end costTime=" + (System.currentTimeMillis() - time));} else {TrackerLog.d("dispatchTouchEvent ACTION_MOVE but not in click limit");}break;

我们接着进入到 triggerViewCalculate 方法:

    /*** for the exposure event** @param view* @return*/public void triggerViewCalculate(int triggerType, View view, HashMap<String, Object> commonInfo,Map<String, ExposureModel> lastVisibleViewMap) {...Map<String, ExposureModel> currentVisibleViewMap = new ArrayMap<String, ExposureModel>();traverseViewTree(view, lastVisibleViewMap, currentVisibleViewMap);commitExposure(triggerType, commonInfo, lastVisibleViewMap, currentVisibleViewMap);TrackerLog.d("triggerViewCalculate");}

再进入到 traverseViewTress 方法:

    /*** find all the view that can be seen in screen.** @param view*/private void traverseViewTree(View view, Map<String, ExposureModel> lastVisibleViewMap,Map<String, ExposureModel> currentVisibleViewMap) {if (CommonHelper.isViewHasTag(view)) {wrapExposureCurrentView(view, lastVisibleViewMap, currentVisibleViewMap);}if (view instanceof ViewGroup) {ViewGroup group = (ViewGroup) view;int childCount = group.getChildCount();for (int i = 0; i < childCount; i++) {traverseViewTree(group.getChildAt(i), lastVisibleViewMap, currentVisibleViewMap);}}}

这儿主要是遍历所有子 view,找到是否有指定 tag 的 view,如果有的话代表这个 view 可能需要进行曝光埋点。

接着再进入到这个方法 wrapExposureCurrentView:

    private void wrapExposureCurrentView(View view, Map<String, ExposureModel> lastVisibleViewMap,Map<String, ExposureModel> currentVisibleViewMap) {String viewTag = (String) view.getTag(TrackerConstants.VIEW_TAG_UNIQUE_NAME);HashMap<String, Object> params = (HashMap<String, Object>) view.getTag(TrackerConstants.VIEW_TAG_PARAM);boolean isWindowChange = view.hasWindowFocus();boolean exposureValid = checkExposureViewDimension(view);boolean needExposureProcess = isWindowChange && exposureValid;if (!needExposureProcess) {return;}// only add the visible view in screenif (lastVisibleViewMap.containsKey(viewTag)) {ExposureModel model = lastVisibleViewMap.get(viewTag);model.params = params;currentVisibleViewMap.put(viewTag, model);} else if (!currentVisibleViewMap.containsKey(viewTag)) {ExposureModel model = new ExposureModel();model.beginTime = System.currentTimeMillis();model.tag = viewTag;model.params = params;currentVisibleViewMap.put(viewTag, model);}}

这儿首先判断了 view 是否有 view 焦点,接着判断了曝光比例,同时符合这两个条件才会进行曝光处理,否则不处理。

曝光处理逻辑:

  • lastVisibleViewMap 包含指定 tag 的话将曝光数据放入 currentVisibleViewMap。
  • 如果 lastVisibleViewMap 和 currentVisibleViewMap 都不包含指定 tag 的话也将曝光数据放入 currentVisibleViewMap。

将数据放到指定 Map 中是用来处理曝光的。

这儿还有一个关键的方法:checkExposureViewDimension

    /*** check the visible width and height of the view, compared with the its original width and height.** @param view* @return*/private boolean checkExposureViewDimension(View view) {int width = view.getWidth();int height = view.getHeight();Rect GlobalVisibleRect = new Rect();boolean isVisibleRect = view.getGlobalVisibleRect(GlobalVisibleRect);if (isVisibleRect) {int visibleWidth = GlobalVisibleRect.width();int visibleHeight = GlobalVisibleRect.height();if ((visibleWidth * 1.00 / width > GlobalsContext.dimThreshold) && (visibleHeight * 1.00 / height > GlobalsContext.dimThreshold)) {return true;} else {return false;}} else {return false;}}

可以看到这个方法通过获取可视区域大小后对比该 view  的实际大小来判断曝光比例是否达到指定阈值,这样就实现了曝光比例的判断。

接下来我们再回到这个方法中:

    /*** for the exposure event** @param view* @return*/public void triggerViewCalculate(int triggerType, View view, HashMap<String, Object> commonInfo,Map<String, ExposureModel> lastVisibleViewMap) {...Map<String, ExposureModel> currentVisibleViewMap = new ArrayMap<String, ExposureModel>();traverseViewTree(view, lastVisibleViewMap, currentVisibleViewMap);commitExposure(triggerType, commonInfo, lastVisibleViewMap, currentVisibleViewMap);TrackerLog.d("triggerViewCalculate");}

上面我们了解到 traverseViewTree 主要是遍历所有子 view,通过 tag 获取埋点数据并将数据存于 map 中。我们接着再看一下 commitExposure 干了什么。

    private void commitExposure(int triggerType, HashMap<String, Object> commonInfo,Map<String, ExposureModel> lastVisibleViewMap, Map<String, ExposureModel> currentVisibleViewMap) {ExposureInner exposureInner = new ExposureInner();exposureInner.triggerType = triggerType;exposureInner.commonInfo = new HashMap<String, Object>();exposureInner.commonInfo.putAll(commonInfo);exposureInner.lastVisibleViewMap = new HashMap<String, ExposureModel>();for (Map.Entry<String, ExposureModel> entry : lastVisibleViewMap.entrySet()) {exposureInner.lastVisibleViewMap.put(entry.getKey(), (ExposureModel) entry.getValue().clone());}exposureInner.currentVisibleViewMap = new HashMap<String, ExposureModel>();for (Map.Entry<String, ExposureModel> entry : currentVisibleViewMap.entrySet()) {exposureInner.currentVisibleViewMap.put(entry.getKey(), (ExposureModel) entry.getValue().clone());}lastVisibleViewMap.clear();lastVisibleViewMap.putAll(currentVisibleViewMap);// transfer time-consuming operation to new thread.Message message = exposureHandler.obtainMessage();message.what = SINGLE_COMMIT_EXPOSURE;message.obj = exposureInner;exposureHandler.sendMessage(message);}

这儿做了以下几个事:

  • 将 lastMap 和 currentMap 中的数据放到 exposureInner 中,并发送到 handler 去处理曝光。
  • 将 lastMap 中的数据放到 currentMap。
  • 清空 lastMap。

我们接下来再看发到 handler 中做了什么?

    private ExposureManager() {exposureHandler = new Handler(exposureThread.getLooper(), new Handler.Callback() {@Overridepublic boolean handleMessage(Message msg) {switch (msg.what) {case SINGLE_COMMIT_EXPOSURE:ExposureInner exposureInner = (ExposureInner) msg.obj;switch (exposureInner.triggerType) {case TrackerInternalConstants.TRIGGER_VIEW_CHANGED:for (String controlName : exposureInner.lastVisibleViewMap.keySet()) {// If the view is visible in the last trigger timing, but invisible this time, then we commit the view as a exposure event.if (!exposureInner.currentVisibleViewMap.containsKey(controlName)) {ExposureModel model = exposureInner.lastVisibleViewMap.get(controlName);model.endTime = System.currentTimeMillis();reportExposureData(exposureInner.commonInfo, model, controlName);}}break;}break;...}return false;}});}

这儿判断了该 view 在上一次触发了曝光,而这一次没有触发曝光,则直接上报曝光事件。

    private void reportExposureData(HashMap<String, Object> commonInfo, ExposureModel model, String viewTag) {long duration = getExposureViewDuration(model);if (duration > 0) {TrackerLog.v("ExposureView report " + model.toString() + " exposure data " + duration);HashMap<String, Object> indexMap = new HashMap<String, Object>();if (!GlobalsContext.exposureIndex.containsKey(viewTag)) {// commit firstlyGlobalsContext.exposureIndex.put(viewTag, 1);indexMap.put("exposureIndex", 1);} else {int index = GlobalsContext.exposureIndex.get(viewTag);GlobalsContext.exposureIndex.put(viewTag, index + 1);indexMap.put("exposureIndex", index + 1);}DataProcess.commitExposureParams(commonInfo, model.tag, model.params, duration, indexMap);}}

这儿做的几个事:

  • 判断曝光时间 > 0 的话才会上报(如果时间 < 设定时间也为 0)。
  • 记录该 view 的曝光次数。
  • 提交曝光数据。
    public static synchronized void commitExposureParams(HashMap<String, Object> commonInfo, String viewName, HashMap<String, Object> viewData, long exposureData, HashMap<String, Object> exposureIndex) {if (GlobalsContext.logOpen) {TrackerLog.v("commitExposureParams commonInfo=" + commonInfo.toString() + ",viewName=" + viewName + ",viewData=" + viewData + ",exposureData=" + exposureData + ",exposureIndex=" + exposureIndex);}IDataCommit commit = TrackerManager.getInstance().getTrackerCommit();commit.commitExposureEvent(commonInfo, viewName, viewData, exposureData, exposureIndex);}

这儿直接获取的是我们在做 SDK 初始化时设定的回调,曝光信息会将数据回调到我们设定的接口中。

我们上面说到的是用户手动滑动时触发的逻辑,还有 fling 时候也应该监听其曝光。

    /*** Scene 2: Scroll ending** @param motionEvent* @param motionEvent1* @param v* @param v1* @return*/@Overridepublic boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {long time = System.currentTimeMillis();TrackerLog.v("onFling triggerViewCalculate begin");this.postDelayed(new Runnable() {@Overridepublic void run() {ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, TrackerFrameLayout.this, commonInfo, lastVisibleViewMap);}}, 1000);TrackerLog.v("onFling triggerViewCalculate end costTime=" + (System.currentTimeMillis() - time));return false;}

我们看到这儿与滑动基本类似,都是调用了 triggerViewCalculate 去处理曝光逻辑。

还有 onLayout 的时候也进行了曝光判断:

    /*** all the state change of view trigger the exposure event** @param changed* @param left* @param top* @param right* @param bottom*/@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {TrackerLog.v("onLayout traverseViewTree begin");// duplicate message in 1slong time = System.currentTimeMillis();if (time - lastOnLayoutSystemTimeMillis > 1000) {lastOnLayoutSystemTimeMillis = time;CommonHelper.addCommonArgsInfo(this);TrackerLog.v("onLayout addCommonArgsInfo");ExposureManager.getInstance().traverseViewTree(this, mReuseLayoutHook);}//ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, this, commonInfo, lastVisibleViewMap);TrackerLog.v("onLayout traverseViewTree end costTime=" + (System.currentTimeMillis() - time));super.onLayout(changed, left, top, right, bottom);}

同样的在 view 的以下两个生命周期也分别做了曝光判断:

/*** the state change of window trigger the exposure event.* Scene 3: switch back and forth when press Home button.* Scene 4: enter into the next page* Scene 5: window replace** @param hasFocus*/@Overridepublic void dispatchWindowFocusChanged(boolean hasFocus) {TrackerLog.v("dispatchWindowFocusChanged triggerViewCalculate begin");long ts = System.currentTimeMillis();ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_WINDOW_CHANGED, this, commonInfo, lastVisibleViewMap);TrackerLog.v("dispatchWindowFocusChanged triggerViewCalculate end costTime=" + (System.currentTimeMillis() - ts));super.dispatchWindowFocusChanged(hasFocus);}@Overrideprotected void dispatchVisibilityChanged(View changedView, int visibility) {// Scene 6: switch page in the TabActivityif (visibility == View.GONE) {TrackerLog.v("dispatchVisibilityChanged triggerViewCalculate begin");long ts = System.currentTimeMillis();ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_WINDOW_CHANGED, this, commonInfo, lastVisibleViewMap);TrackerLog.v("dispatchVisibilityChanged triggerViewCalculate end costTime=" + (System.currentTimeMillis() - ts));} else {TrackerLog.v("trigger dispatchVisibilityChanged, visibility =" + visibility);}super.dispatchVisibilityChanged(changedView, visibility);}

三、总结

该库通过全局监听 Activity 的生命周期,又动态添加自定义 View,再遍历 view tree 中所有 view 是否有指定 tag 的方式,可以全局检测到 Activity 中 View 的曝光和手势操作,通过无侵入业务代码的方式动态管理了曝光和点击事件。

它分别在以下时机做了曝光监听:

  • onLayout:view 确定大小和位置后,绘制前。
  • 滑动:手势滑动。
  • fling:松手后的惯性滑动。
  • dispatchWindowFocusChanged:窗口焦点变化,比如:前后台切换、页面切换。
  • dispatchVisibilityChanged:view 可见性变化。

值得学习的点:

通过遍历 view 树和设置 view.setAccessibilityDelegate 直接无侵入做到点击事件监听。

这种方式是我之前所没有用过的,值得借鉴。

相关文章:

android_viewtracker 原理

一、说明 我们业务中大部分场景是用 RecyclerView 实现的列表&#xff0c;而 View 的曝光通常是直接写在 adapter 的 onBindViewHolder 中&#xff0c;这样就会导致 item 还没显示出来的时候就会触发曝光。最近业务提出需要实现根据 View 显示在屏幕上面积大于 80% 才算曝光。…...

Object.defineProperty()

**Object.defineProperty()** 方法会直接在一个对象上定义一个新属性&#xff0c;或者修改一个对象的现有属性&#xff0c;并返回此对象。 plain const object1 {}; Object.defineProperty(object1, ‘property1’, { value: 42, writable: false }); object1.property1 77…...

大模型+知识图谱:重塑企业制度标准管理

在数字化转型的浪潮中&#xff0c;制度标准管理领域正迎来一场革命性的变革。借助大模型和知识图谱等前沿人工智能技术&#xff0c;制度标准管理不再仅仅是简单的文档存储和检索&#xff0c;而是演变为一个智能化、高效化、精准化的管理体系。 1.关键技术 我们的制度标准管理…...

ubuntu20系统下conda虚拟环境下安装文件存储位置

在 Conda 虚拟环境中执行 pip install 安装软件后&#xff0c;安装的文件会存储在该虚拟环境专属的 site-packages 目录中。具体路径取决于你激活的 Conda 环境路径。以下是定位步骤&#xff1a; 1. 确认 Conda 虚拟环境的安装路径 查看所有环境&#xff1a; conda info --env…...

深度学习编译器(整理某survey)

一、深度学习框架 TensorFlow PyTorch MXNet ONNX:定义了一个统一的表示&#xff0c;DL models的格式方便不同框架之间的转换模型 二、深度学习硬件 通用硬件&#xff08;CPU、GPU&#xff09;&#xff1a;通过硬件和软件优化支持深度学习工作负载 GPU:通过多核架构实现高…...

Python学习第八天

查看函数参数 操作之前给大家讲一个小技巧&#xff1a;如何查看函数的参数&#xff08;因为python的底层源码是C语言并且不是开放的&#xff0c;也一直困扰着刚学习的我&#xff0c;这个参数叫什么名之类的看doc又总是需要翻译挺麻烦的&#xff09;。 比如我们下面要说到的op…...

SpringBoot为什么默认使用CGLIB?

大家好&#xff0c;我是锋哥。今天分享关于【SpringBoot为什么默认使用CGLIB?】面试题。希望对大家有帮助&#xff1b; SpringBoot为什么默认使用CGLIB? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring Boot 默认使用 CGLIB&#xff08;Code Generation Li…...

【消息队列】数据库的数据管理

1. 数据库的选择 对于当前实现消息队列这样的一个中间件来说&#xff0c;具体要使用哪个数据库&#xff0c;是需要稍作考虑的&#xff0c;如果直接使用 MySQL 数据库也是能实现正常的功能&#xff0c;但是 MySQL 也是一个客户端服务器程序&#xff0c;也就意味着如果想在其他服…...

pytest中pytest.ini文件的使用

pytest.ini 是 pytest 测试框架的配置文件,它允许你自定义 pytest 的行为。通过在 pytest.ini 中设置各种选项,可以改变测试用例的发现规则、输出格式、插件行为等。以下详细介绍 pytest.ini 文件的使用。 1. 文件位置 pytest.ini 文件通常位于项目的根目录下,pytest 在运…...

docker学习笔记(1)从安装docker到使用Portainer部署容器

docker学习笔记第一课 先交代背景 docker宿主机系统&#xff1a;阿里云ubuntu22.04 开发机系统&#xff1a;win11 docker镜像仓库&#xff1a;阿里云&#xff0c;此阿里云与宿主机系统没有关系&#xff0c;是阿里云提供的一个免费的docker仓库 代码托管平台&#xff1a;github&…...

Vue.js侦听器

侦听器​ 基本示例​ 计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。 在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数: …...

【C++学习篇】智能指针

目录 1. 智能指针的使用场景分析 2. RAII和智能指针的设计思路 3. C标准库智能指针的使用 4.shared_ptr和weak_ptr 4.1shared_ptr的循环引用问题 4.2 weak_ptr 1. 智能指针的使用场景分析 下⾯程序中我们可以看到&#xff0c;new了以后&#xff0c;我们也delete了&#xff0c…...

数字电子技术基础(二十四)——TTL门电路的高、低电平的输出特性曲线

目录 1 TTL门电路的特性曲线 1.1 高电平输出特性 1.1.2 高电平输出特性的实验过程 1.1.2 TTL门电路的输出特性的实验结果 1.2 低电平的输出特性 1 TTL门电路的特性曲线 1.1 高电平输出特性 1.1.2 高电平输出特性的实验过程 现在想要测试TTL门电路的输出特性&#xff0c…...

linux进程通信之共享内存

在 Linux 系统中&#xff0c;共享内存&#xff08;Shared Memory&#xff09; 是一种高效的进程间通信&#xff08;IPC&#xff09;方式&#xff0c;允许多个进程直接访问同一块物理内存区域。以下是关于 Linux 共享内存的详细讲解&#xff1a; 一、共享内存的核心特点 高速通信…...

学习第十一天-树

一、树的基础概念 1. 定义 树是一种非线性数据结构&#xff0c;由 n 个有限节点组成层次关系集合。特点&#xff1a; 有且仅有一个根节点其余节点分为若干互不相交的子树节点间通过父子关系连接 2. 关键术语 术语定义节点包含数据和子节点引用的单元根节点树的起始节点&#…...

场景题:10亿QQ用户,如何统计在线人数?

现在卷的环境下&#xff0c;面试除了八股文算法项目外&#xff0c;场景题也是问的越来越多了。一方面是就业市场竞争者较多所带来的必然结果&#xff1b;另一方面是公司对于应聘者的技术要求也越来越高了。 今天继续介绍Java面试常见的场景题&#xff1a;在线人数统计 现在用户…...

学习工具的一天之(burp)

第一呢一定是先下载 【Java环境】&#xff1a;Java Downloads | Oracle 下来是burp的下载 Download Burp Suite Community Edition - PortSwigger 【下载方法二】关注的一个博主 【BurpSuite 安装激活使用详细上手教程 web安全测试工具】https://www.bilibili.com/video/BV…...

归并排序:分治哲学的完美演绎与时空平衡的艺术

引言&#xff1a;跨越世纪的算法明珠 在计算机科学的璀璨星河中&#xff0c;归并排序犹如一颗恒久闪耀的明星。1945年&#xff0c;现代计算机之父冯诺伊曼在EDVAC计算机的研发过程中首次系统性地提出了这一算法&#xff0c;其精妙的分治思想不仅奠定了现代排序算法的理论基础&…...

蓝桥杯4T平台(串口打印电压值)

知识点&#xff1a;串口(单片机发送数据)按键ADC 题目 配置 代码 adc.c uint16_t getadc2(void) {uint16_t adc0;HAL_ADC_Start(&hadc2);adcHAL_ADC_GetValue(&hadc2);return adc; } adc.h uint16_t getadc2(void); main.c #include "lcd.h" #include…...

Stable Diffusion Prompt编写规范详解

Stable Diffusion Prompt编写规范详解 一、语法结构规范 &#xff08;一&#xff09;基础模板框架 [质量强化] [主体特征] [环境氛围] [风格控制] [镜头参数]质量强化&#xff1a;best quality, ultra detailed, 8k resolution‌主体特征&#xff1a;(1girl:1.3), long …...

es6常见知识点

官方文档&#xff1a;[https://es6.ruanyifeng.com/](https://es6.ruanyifeng.com/) 一、Class 1、Class Class只是一个语法糖,其功能用es5也能实现,但是比es5更符合类的期待 定义: constructor代表构造方法,而this指向new 生成的实例 定义类方法时,可以不使用function 注…...

leetcode1 两数之和 哈希表

什么时候使用哈希法&#xff0c;当我们需要查询一个元素是否出现过&#xff0c;或者一个元素是否在集合里的时候&#xff0c;就要第一时间想到哈希法。 242. 有效的字母异位词 (opens new window)这道题目是用数组作为哈希表来解决哈希问题&#xff0c;349. 两个数组的交集 (o…...

Java中lombok的@Data注解【布尔类型】字段定义方式

文章目录 背景第一步、场景复现第二步、分析问题第三步、实现方案总结 背景 在Data注解的bean中添加Boolean字段时&#xff0c;set方法正常&#xff0c;get方法无法获取。 第一步、场景复现 在OrderInfo的实体中&#xff0c;新增布尔类型的字段&#xff1a;支付过【hasPaid】…...

理解数学概念——稠密性(density)

目录 1. 定义 2. 等价定义 3. 直观理解 1. 定义 在拓扑学(topology)和数学相关领域中&#xff0c;对于一个拓扑空间 X 的一个子集 A&#xff0c;若 X的每一个点要么属于A &#xff0c;要么无限“接近”X的某个成员&#xff0c;则称这个子集 A 是稠密的(dense)或称A具有稠密性…...

【Spring AOP】_切点类的切点表达式

目录 1. 根据方法签名匹配编写切点表达式 1.1 具体语法 1.2 通配符表达规范 2. 根据注解匹配编写切点表达式 2.1 实现步骤 2.2 元注解及其常用取值含义 2.3 使用自定义注解 2.3.1 编写自定义注解MyAspect 2.3.2 编写切面类MyAspectDemo 2.3.3 编写测试类及测试方法 在…...

通过多线程获取RV1126的AAC码流

目录 一RV1126多线程获取音频编码AAC码流的流程 1.1AI模块的初始化并使能 1.2AENC模块的初始化 ​​​​​​​1.3绑定AI模块和AENC模块 ​​​​​​​1.4多线程获取每一帧AAC码流 ​​​​​​​1.5每个AAC码流添加ADTSHeader头部 ​​​​​​​1.6写入具体每一帧AAC的…...

HDFS 为什么不适合处理小文件?

目录 一、HDFS 是什么&#xff1f; 1. 核心目标 2. 基本架构 二、HDFS 为什么不适合处理小文件&#xff1f; 1. 元数据管理问题 2. 存储效率低下 3. 访问性能问题 4. 计算框架效率问题 5. 其他限制 一、HDFS 是什么&#xff1f; HDFS&#xff08;Hadoop 分布式文件系统…...

网络空间安全(14)编辑器漏洞

一、概述 网页在线编辑器允许用户在网页上进行文本的编辑&#xff0c;并设置字体样式、段落行间距等&#xff0c;类似于使用Word进行编辑。然而&#xff0c;由于编辑器在处理用户输入、文件上传、权限控制等方面可能存在安全缺陷&#xff0c;因此容易成为攻击者利用的目标。 二…...

SpringMvc与Struts2

一、Spring MVC 1.1 概述 Spring MVC 是 Spring 框架的一部分&#xff0c;是一个基于 MVC 设计模式的轻量级 Web 框架。它提供了灵活的配置和强大的扩展能力&#xff0c;适合构建复杂的 Web 应用程序。 1.2 特点 轻量级&#xff1a;与 Spring 框架无缝集成&#xff0c;依赖…...

Avalonia 打包成deb

参考 https://www.cnblogs.com/Fengyinyong/p/13346642.html 安装工具 dotnet tool install --global dotnet-deb 还原包 dotnet restore -r linux-x64 dotnet deb install 打包&#xff0c;其中/p:SelfContainedtrue是独立运行 dotnet msbuild XXXCore.csproj /t:Creat…...

服务器数据恢复—raid5阵列中硬盘掉线导致上层应用不可用的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 某公司一台服务器&#xff0c;服务器上有一组由8块硬盘组建的raid5磁盘阵列。 磁盘阵列中2块硬盘的指示灯显示异常&#xff0c;其他硬盘指示灯显示正常。上层应用不可用。 服务器数据恢复过程&#xff1a; 1、将服务器中所有硬盘编号…...

除了合并接口,还有哪些优化 Flask API 的方法?

除了合并接口&#xff0c;还有许多其他方法可以优化 Flask API&#xff0c;以下从性能优化、代码结构优化、安全性优化、错误处理优化等方面详细介绍&#xff1a; 性能优化 1. 使用缓存 内存缓存&#xff1a;可以使用 Flask-Caching 扩展来实现内存缓存&#xff0c;减少对数…...

制服小程序的“滑手”:禁用页面左右滑动全攻略

哈哈&#xff0c;看来你已经很聪明地发现了小程序中左右滑动的“顽皮”行为&#xff01;&#x1f604; 没错&#xff0c;我们可以通过设置 disableScroll 属性来“管教”它&#xff0c;同时结合 CSS 样式让页面既禁得住横向“乱跑”&#xff0c;又能顺畅地上下滚动。你的方案已…...

学习日记-250305

阅读论文&#xff1a;Leveraging Pedagogical Theories to Understand Student Learning Process with Graph-based Reasonable Knowledge Tracing ps:代码逻辑最后一点还没理顺&#xff0c;明天继续 4.2 Knowledge Memory & Knowledge Tracing 代码研究&#xff1a; 一般…...

DeepSeek R1模型医疗机构本地化部署评估分析(Discuss V1版上)

为了确保医疗机构在部署和应用DeepSeek R1模型时的成功,可以根据各个步骤设计一套综合的评估和评测体系。该体系将帮助医疗机构在实施过程中持续跟踪效果、识别潜在问题并进行优化调整。以下是对各步骤的详细评估和评测体系设计。 1. 确定模型需求 在医疗机构上线DeepSeek R…...

java 查找连个 集合的交集部分数据

利用了Java 8的Stream API&#xff0c;代码简洁且效率高 import java.util.stream.Collectors; import java.util.List; import java.util.HashSet; import java.util.Set;public class ListIntersection {public static List<Long> findIntersection(List<Long> …...

Hadoop管理页看不到任务的问题

这个yarn分配任务了但是为空 在$HADOOP_HOME/conf/mapred-site.xml 原来的配置文件基础之上添加&#xff1a; <property><name>mapreduce.framework.name</name><value>yarn</value></property> 重启之后就好了...

cmake、CMakeLists.txt、make、ninja

文章目录 一、概念0.cmake官网1.什么是cmake2.为什么使用cmake3.CMakeLists.txt 二、CMakeLists.txt语法&#xff1a;如何编写CMakeLists.txt&#xff0c;语法详解(0)语法基本原则(1)project关键字(2)set关键字(3)message关键字(4)add_executable关键字(5)add_subdirectory关键…...

PHP之Cookie和Session

在你有别的编程语言的基础下&#xff0c;你想学习PHP&#xff0c;可能要了解的一些关于cookie和session的信息。 Cookie 参数信息 setcookie(name,value,expire, path, domain); name : Cookie的名称。 value : Cookie的值。 expire : Cookie的过期时间&#xff0c;可以是一…...

学习记录-用例设计编写

黑马测试视频记录 目录 一、 软件测试流程 二、测试用例编写格式 1、等价类法 2、边界值分析法 3、 判定表法 4、场景法​编辑 5、错误推荐法 一、 软件测试流程 二、测试用例编写格式 1、等价类法 2、边界值分析法 3、 判定表法 4、场景法 5、错误推荐法 时间紧任务重…...

【Docker】容器安全之非root用户运行

【Docker】容器安全之非root用户运行 1. 场景2. 原 Dockerfile 内容3. 整改结果4. 非 root 用户带来的潜在问题4.1 文件夹读写权限异常4.2 验证文件夹权限 1. 场景 最近有个项目要交付&#xff0c;第三方测试对项目源码扫描后发现一个问题&#xff0c;服务的 Dockerfile 都未指…...

CVE-2025-0392:JeeWMS graphReportController.do接口SQL注入漏洞复现

文章目录 CVE-2025-0392:JeeWMS graphReportController.do接口SQL注入漏洞复现0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.构造POC2.复现CVE-2025-0392:JeeWMS graphReportController.do接口SQL注入漏洞复现 0x01 前言 免责声明:请勿利用文章内的相…...

如何使用 Python+Flask+win32print 实现简易网络打印服务1

Python 实现网络打印机&#xff1a;Flask win32print 在工作场景中&#xff0c;我们可能需要一个简单的网页接口&#xff0c;供他人上传文档并自动打印到指定打印机。 本文将演示如何使用 Python Flask win32print 库来实现这一需求。 代码详见&#xff1a;https://github.…...

Ubuntu20.04双系统安装及软件安装(十一):向日葵远程软件

Ubuntu20.04双系统安装及软件安装&#xff08;十一&#xff09;&#xff1a;向日葵远程软件 打开向日葵远程官网&#xff0c;下载图形版本&#xff1a; 在下载目录下打开终端&#xff0c;执行&#xff1a; sudo dpkg -i SunloginClient(按tab键自动补全)出现报错&#xff1a; …...

鸿蒙启动页开发

鸿蒙启动页开发 1.1 更改应用名称和图标 1.更改应用图标 找到moudle.json5文件&#xff0c;找到应用启动的EntryAbility下面的icon,将原来的图标改成自己设置的即可 2.更改应用名称 3.效果展示 2.1 广告页面开发 3.1 详细介绍 3.1.1 启动页面 import { PrivacyDialog } fr…...

认知动力学视角下的生命优化系统:多模态机器学习框架的哲学重构

认知动力学视角下的生命优化系统&#xff1a;多模态机器学习框架的哲学重构 一、信息熵与生命系统的耗散结构 在热力学第二定律框架下&#xff0c;生命系统可视为负熵流的耗散结构&#xff1a; d S d i S d e S dS d_iS d_eS dSdi​Sde​S 其中 d i S d_iS di​S为内部熵…...

【Python编程】高性能Python Web服务部署架构解析

一、FastAPI 与 Uvicorn/Gunicorn 的协同 1. 开发环境&#xff1a;Uvicorn 直接驱动 作用&#xff1a;Uvicorn 作为 ASGI 服务器&#xff0c;原生支持 FastAPI 的异步特性&#xff0c;提供热重载&#xff08;--reload&#xff09;和高效异步请求处理。 启动命令&#xff1a; u…...

仿mudou库one thread oneloop式并发服务器

项目gitee&#xff1a;仿muduo: 仿muduo 一&#xff1a;项目目的 1.1项目简介 通过咱们实现的⾼并发服务器组件&#xff0c;可以简洁快速的完成⼀个⾼性能的服务器搭建。 并且&#xff0c;通过组件内提供的不同应⽤层协议⽀持&#xff0c;也可以快速完成⼀个⾼性能应⽤服务器…...

AI推理模型竞赛:从DeepSeek R1到Claude 3.7的关键进展

摘要 在Reasoning Model首轮竞赛中&#xff0c;从R1到Sonnet 3.7&#xff0c;AI领域取得了显著进展。DeepSeek R1的发布激发了推理模型的竞争。过去一个月内&#xff0c;顶尖AI实验室相继推出了三款最新的SOTA推理模型&#xff1a;OpenAI的o3-mini和deep research&#xff0c;x…...

AORO P9000 PRO三防平板携手RTK高精度定位,电力巡检效率倍增

电网系统覆盖幅员辽阔&#xff0c;每年因设备故障导致的巡检耗时超过百万工日。传统巡检模式受限于定位误差、设备防护不足和作业效率低下三大核心痛点&#xff0c;亟需智能化工具的突破性革新。为了满足这一需求&#xff0c;遨游通讯推出AORO P9000 PRO三防平板&#xff0c;以…...