JetPack之LiveData粘性原因分析及hook解决

目录

  • 前言
  • 一、LiveData粘性原因分析
    • 1.1 发送消息流程
    • 1.2 监听消息流程
    • 1.3 根因分析
  • 二、hook解决


前言

在 Android 中,LiveData 的默认行为是粘性的,即 LiveData 在设置数据后,即使观察者订阅时已经有数据存在,观察者仍会立即收到这个数据。

在上一篇文章中JetPack之LiveData最后的案例我们看到了livedata的粘性事件,一般情况下,我们观察者先订阅,等到消息发生改变时,接收到消息,使用observe做一些更新UI等操作,但是粘性事件会导致我们会接收到订阅之前的数据,这在某些场景下并不是我们想要的。

一、LiveData粘性原因分析

1.1 发送消息流程

MutableLiveData的两个发送消息流程setValue、postValue。
setValue 只能在主线程使用,postValue可以在任何线程使用,它被调用时,其实也是通过handler切换到了主线程,再调用 的setValue

    protected void postValue(T value) {
        boolean postTask;
        synchronized (mDataLock) {
            postTask = mPendingData == NOT_SET;
            mPendingData = value;
        }
        if (!postTask) {
            return;
        }
        ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
    }
    
    private final Runnable mPostValueRunnable = new Runnable() {
        @SuppressWarnings("unchecked")
        @Override
        public void run() {
            Object newValue;
            synchronized (mDataLock) {
                newValue = mPendingData;
                mPendingData = NOT_SET;
            }
            setValue((T) newValue);
        }
    };

setValue 首先声明自己要在主线程中运行,然后 mVersion++;,最后调用dispatchingValue分发消息

    protected void setValue(T value) {
        assertMainThread("setValue");
        mVersion++;
        mData = value;
        dispatchingValue(null);
    }

dispatchingValue方法主要是对参数中的观察者进行了判空以及遍历,最后对每个遍历的对象调用了considerNotify方法

    void dispatchingValue(@Nullable ObserverWrapper initiator) {
        if (mDispatchingValue) {
            mDispatchInvalidated = true;
            return;
        }
        mDispatchingValue = true;
        do {
            mDispatchInvalidated = false;
            if (initiator != null) {
                considerNotify(initiator);
                initiator = null;
            } else {
                for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                        mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                    considerNotify(iterator.next().getValue());
                    if (mDispatchInvalidated) {
                        break;
                    }
                }
            }
        } while (mDispatchInvalidated);
        mDispatchingValue = false;
    }

considerNotify首先判断观察者是否存活,如果观察者不处于活动状态,则直接返回,不通知观察者。

  • 检查观察者是否应该处于活动状态。如果观察者不应该处于活动状态,则调用 activeStateChanged(false) 方法通知观察者状态已更改,并返回,不通知观察者。
  • 检查观察者上一次接收到的版本号是否大于或等于 LiveData 的当前版本号。如果是,则表示观察者已经接收过最新的数据,无需再次通知观察者。
  • 如果不是最新版本号,将 LiveData 的当前版本号赋值给观察者的上一次版本号,表示观察者已经接收到最新的数据。
  • 调用观察者的 onChanged 方法,将 LiveData 中存储的数据 mData 传递给观察者进行处理。
    private void considerNotify(ObserverWrapper observer) {
        if (!observer.mActive) {
            return;
        }
        // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
        //
        // we still first check observer.active to keep it as the entrance for events. So even if
        // the observer moved to an active state, if we've not received that event, we better not
        // notify for a more predictable notification order.
        if (!observer.shouldBeActive()) {
            observer.activeStateChanged(false);
            return;
        }
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion;
        observer.mObserver.onChanged((T) mData);
    }

发送消息流程基本解读完毕,读到这里,有几个核心点:

1.mLastVersion:上一个版本号
2.mVersion 当前版本号
3.如果当前版本号不是最新版本号,那么版本号会被覆盖,然后回调观察者的onChanged方法,即我们更新UI等操作的地方

接下来看监听流程

1.2 监听消息流程

从监听的observe方法入手

  • assertMainThread(“observe”);: 检查当前线程是否为主线程,如果不是主线程则抛出异常。这是为了确保 observe 方法在主线程中调用,因为 LiveData 的观察者通常在主线程中更新 UI。
  • if (owner.getLifecycle().getCurrentState() == DESTROYED) { return; }: 检查生命周期所有者的当前状态是否为 DESTROYED(已销毁),如果是,则直接返回,不执行后续操作。
  • LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);: 创建一个 LifecycleBoundObserver 对象,将生命周期所有者和观察者传入。
  • ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);: 将观察者和对应的 LifecycleBoundObserver 对象放入 mObservers Map 中,如果之前已经存在相同的观察者则返回已存在的 ObserverWrapper 对象。
  • if (existing != null && !existing.isAttachedTo(owner)) { throw new IllegalArgumentException(“Cannot add the same observer” + " with different lifecycles"); }: 如果之前已经存在相同的观察者,但是观察者与不同的生命周期所有者绑定,则抛出异常,因为相同的观察者不能绑定到不同的生命周期所有者。
  • if (existing != null) { return; }: 如果之前已经存在相同的观察者且与相同的生命周期所有者绑定,则直接返回,不执行后续操作。
  • owner.getLifecycle().addObserver(wrapper);: 将 LifecycleBoundObserver 对象添加到生命周期所有者的 Lifecycle 中,这样当生命周期所有者的状态发生变化时,会通知绑定的观察者。
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        assertMainThread("observe");
        if (owner.getLifecycle().getCurrentState() == DESTROYED) {
            // ignore
            return;
        }
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        owner.getLifecycle().addObserver(wrapper);
    }

从 LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);入手,看一下内部做了什么
LifecycleBoundObserver 继承了 ObserverWrapper ,实现了 LifecycleEventObserver 接口。

 class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
        @NonNull
        final LifecycleOwner mOwner;

        LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
            super(observer);
            mOwner = owner;
        }
        .......

LifecycleEventObserver 当 lifecycle 状态改变的时候会感应到,并进行回调onStateChanged方法

public fun interface LifecycleEventObserver : LifecycleObserver {
    /**
     * Called when a state transition event happens.
     *
     * @param source The source of the event
     * @param event The event
     */
    public fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event)
}

当onStateChanged的状态改变传递到LifecycleBoundObserver时,会调用LifecycleBoundObserver的onStateChanged方法

  class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
        @NonNull
        final LifecycleOwner mOwner;

        LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
            super(observer);
            mOwner = owner;
        }

        @Override
        boolean shouldBeActive() {
            return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
        }

        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
            if (currentState == DESTROYED) {
                removeObserver(mObserver);
                return;
            }
            Lifecycle.State prevState = null;
            while (prevState != currentState) {
                prevState = currentState;
                activeStateChanged(shouldBeActive());
                currentState = mOwner.getLifecycle().getCurrentState();
            }
        }

最终回调到ObserverWrapper 的activeStateChanged方法,观察一下ObserverWrapper类结构,原来之前的mLastVersion是在这里定义的,默认值为-1,回到activeStateChanged方法,可以看到最终也会回到 dispatchingValue方法,只是负责分发当前观察者(this),不像发送消息流程分发到全部的观察者。

public abstract class LiveData<T> {
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Object mDataLock = new Object();
    static final int START_VERSION = -1;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 private abstract class ObserverWrapper {
        final Observer<? super T> mObserver;
        boolean mActive;
        int mLastVersion = START_VERSION;

        ObserverWrapper(Observer<? super T> observer) {
            mObserver = observer;
        }

        abstract boolean shouldBeActive();

        boolean isAttachedTo(LifecycleOwner owner) {
            return false;
        }

        void detachObserver() {
        }

        void activeStateChanged(boolean newActive) {
            if (newActive == mActive) {
                return;
            }
            // immediately set active state, so we'd never dispatch anything to inactive
            // owner
            mActive = newActive;
            changeActiveCounter(mActive ? 1 : -1);
            if (mActive) {
                dispatchingValue(this);
            }
        }
    }

1.3 根因分析

mLastVersion 的默认初始值是-1,mVersion 的默认初始值也是-1,当我们先执行发送的时候,进行了自增,mVersion 就变成了0,当我们执行observe 进行监听的时候,observer.mLastVersion >= mVersion 这个条件就不成立了,因为此时mLastVersion 是-1,小于 mVersion 了。
发送和监听都会调用dispatchingValue方法,但mVersion只要发送就会在setValue方法中++,而mLastVersion永远只能在setValue方法后的considerNotify方法中被置为mVersion的值。

    public LiveData() {
        mData = NOT_SET;
        mVersion = START_VERSION;
    }
    public abstract class LiveData<T> {
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Object mDataLock = new Object();
    static final int START_VERSION = -1;

二、hook解决

在安卓开发中,“Hook” 是指通过修改系统或应用程序的行为,来实现某种特定的功能或者改变程序的默认行为。常见的 Hook 技术包括方法 Hook、类 Hook、系统 Hook 等。以下是对安卓 Hook 的详细解释:

方法 Hook
方法 Hook 是指在程序运行时替换或者修改某个方法的实现逻辑,以达到特定的目的。
通过方法 Hook,可以拦截系统或第三方库的方法调用,修改方法的参数或返回值,实现功能增强或者数据篡改等操作。
常见的方法 Hook 框架有 Xposed、Dexposed、Frida 等。
类 Hook
类 Hook 是指在程序运行时替换或者修改某个类的实现逻辑,以达到特定的目的。
通过类 Hook,可以修改类的属性、方法行为,实现功能增强或者数据篡改等操作。
类 Hook 通常需要使用字节码操作技术,如 ASM、Javassist 等。
系统 Hook
系统 Hook 是指修改系统层的行为,如修改系统服务、系统调用等,以实现对系统行为的控制。
通过系统 Hook,可以实现系统级别的功能增强、权限管理、安全加固等操作。
系统 Hook 需要对系统底层进行深入了解,通常需要 root 权限才能实现。

Hook 的应用场景:

功能增强:通过 Hook 修改系统或应用程序的行为,实现功能增强或定制化功能。
数据篡改:通过 Hook 修改数据传递或处理逻辑,实现数据篡改或数据劫持。
安全加固:通过 Hook 检测恶意行为、加固系统安全,防止恶意软件的攻击。
调试分析:通过 Hook 获取程序运行时的信息,进行调试分析或性能优化。

我们使用hook反射的方式,在每次Observe方法中,将mLastVersion赋值为mVersion,这样下次就不会调用到onChanged方法中,去除了粘性!
核心代码

        private void hook(Observer<? super T> observer) {
            try {
                Field mObserversField = LiveData.class.getDeclaredField("mObservers");
                mObserversField.setAccessible(true);
                Object mObserversObject = mObserversField.get(this);

                Class<?> mObserversClass = mObserversObject.getClass();
                Method get = mObserversClass.getDeclaredMethod("get", Object.class);
                get.setAccessible(true);
                Object invokeEntry = get.invoke(mObserversObject, observer);

                Object observerWrapper = null;
                if (invokeEntry != null && invokeEntry instanceof Map.Entry) {
                    observerWrapper = ((Map.Entry) invokeEntry).getValue();
                }
                if (observerWrapper == null) {
                    throw new NullPointerException("observerWrapper is null");
                }
                Log.d("Henry","属性是什么"+ observerWrapper.getClass());
                Class<?> superClass = observerWrapper.getClass().getSuperclass();
                Field mLastVersion = superClass.getDeclaredField("mLastVersion");
                mLastVersion.setAccessible(true);

                Field mVersion = LiveData.class.getDeclaredField("mVersion");
                mVersion.setAccessible(true);

                Object mVersionValue = mVersion.get(this);
                mLastVersion.set(observerWrapper, mVersionValue);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

示例:采取反射,先发消息后订阅,不会接受到旧消息。
OkLiveDataBusJava.java

public class OkLiveDataBusJava {
    //存放订阅者
    private static final Map<String, BusMutableLiveData<?>> bus = new HashMap<>();

    public synchronized static <T> BusMutableLiveData<T> with(String key, Class<T> type, boolean ishook) {
        if (!bus.containsKey(key)) {
            bus.put(key, new BusMutableLiveData<>(ishook));
        }
        return (BusMutableLiveData<T>) bus.get(key);
    }

    public static class BusMutableLiveData<T> extends MutableLiveData<T> {
        private boolean ishook;
        private BusMutableLiveData(boolean ishook) {
            this.ishook = ishook;
        }

        @Override
        public void observe(LifecycleOwner owner, Observer<? super T> observer) {
            super.observe(owner, observer);
            if (ishook) {
                hook(observer);
                Log.d("Henry", " 启用hook");
            } else {
                Log.d("Henry", " 不启用hook");
            }
        }

        private void hook(Observer<? super T> observer) {
            try {
                Field mObserversField = LiveData.class.getDeclaredField("mObservers");
                mObserversField.setAccessible(true);
                Object mObserversObject = mObserversField.get(this);

                Class<?> mObserversClass = mObserversObject.getClass();
                Method get = mObserversClass.getDeclaredMethod("get", Object.class);
                get.setAccessible(true);
                Object invokeEntry = get.invoke(mObserversObject, observer);

                Object observerWrapper = null;
                if (invokeEntry != null && invokeEntry instanceof Map.Entry) {
                    observerWrapper = ((Map.Entry) invokeEntry).getValue();
                }
                if (observerWrapper == null) {
                    throw new NullPointerException("observerWrapper is null");
                }
                Log.d("Henry","属性是什么"+ observerWrapper.getClass());
                Class<?> superClass = observerWrapper.getClass().getSuperclass();
                Field mLastVersion = superClass.getDeclaredField("mLastVersion");
                mLastVersion.setAccessible(true);

                Field mVersion = LiveData.class.getDeclaredField("mVersion");
                mVersion.setAccessible(true);

                Object mVersionValue = mVersion.get(this);
                mLastVersion.set(observerWrapper, mVersionValue);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

OkLiveDataBusActivity

public class OkLiveDataBusActivity extends AppCompatActivity {

    Button button;

    @SuppressLint("MissingInflatedId")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ok_live_data_bus);
        button = findViewById(R.id.OKlivedata_jump);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(OkLiveDataBusActivity.this,
                        OkLiveDataBusSecondActivity.class));
            }
        });

        OkLiveDataBusJava.with("data", String.class, true).postValue("old 数据-----------");

    }
}

OkLiveDataBusSecondActivity

public class OkLiveDataBusSecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ok_live_data_bus_second);
        OkLiveDataBusJava.with("data", String.class, true).observe(this,
                new Observer<String>() {
                    @Override
                    public void onChanged(String s) {
                        Toast.makeText(OkLiveDataBusSecondActivity.this,
                                "获取数据" + s, Toast.LENGTH_SHORT).show();
                    }
                });
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                OkLiveDataBusJava.with("data", String.class, true).postValue("new 数据-----------");
            }
        }).start();
    }
}

测试一下:
在这里插入图片描述
来张美图犒劳一下
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/475549.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

通过人工智能驱动的交互提升客户体验

用AI创造无限可能&#xff1a;打造极致客户体验的秘诀 在当今竞争激烈的市场中&#xff0c;客户体验至关重要。 企业正在迅速采用人工智能驱动的交互来彻底改变与客户的互动。 人工智能技术不仅简化了运营&#xff0c;还带来了以前无法达到的个性化和效率水平。 对于寻求满足客…

权限管理系统-0.5.0

六、审批管理模块 审批管理模块包括审批类型和审批模板&#xff0c;审批类型如&#xff1a;出勤、人事、财务等&#xff0c;审批模板如&#xff1a;加班、请假等具体业务。 6.1 引入依赖 在项目中引入activiti7的相关依赖&#xff1a; <!--引入activiti的springboot启动器…

【Java Web基础】一些网页设计基础(二)

文章目录 1. Bootstrap导航栏设计1.1 代码copy与删减效果1.2 居中属性与底色设置1.3 占不满问题分析1.4 字体颜色、字体大小、字体间距设置1.5 修改超链接hover颜色&#xff0c;网站首页字体颜色 1. Bootstrap导航栏设计 1.1 代码copy与删减效果 今天设计导航栏&#xff0c;直…

round函数使用后,小数点前的0不见了

ROUND函数用于将数字四舍五入到指定的小数位数。 其基本语法为ROUND(number, num_digits)&#xff0c;其中number是要进行四舍五入的数字&#xff0c;num_digits是保留的小数位数。如果num_digits大于0&#xff0c;则四舍五入到指定的小数位&#xff1b;如果num_digits等于0&a…

【算法】 LRU Cache

目录 一、什么是LRU Cache 二、LRU Cache的实现 三、 LRU算法的运用场景 一、什么是LRU Cache LRU是Least Recently Used的缩写&#xff0c;意思是最近最少使用&#xff0c;它是一种Cache替换算法。 什么是 Cache&#xff1f;狭义的Cache指的是位于CPU和主存间的快速RAM&am…

vue2源码学习01配置rollup打包环境

1.下载rollup相关依赖 npm i rollup rollup-plugin-babel babel/core babel/preset-env --save-dev 2.新建rollup.config.js配置打包选项 //rollup可以导出一个对象&#xff0c;作为打包的配置文件 import babel from rollup-plugin-babel export default {input: ./src/ind…

CI/CD脚本简介,YAML介绍,Editor解析

说明&#xff1a; 此篇文章纯概念&#xff0c;没有实际操作&#xff0c;实际操作请蹲下一篇&#xff01; CI/CD理解 这段代码是用于配置GitLab CI/CD&#xff08;Continuous Integration/Continuous Deployment&#xff09;的YAML语法。GitLab CI/CD是一种自动化软件&#xff0…

【MySQL】对数据库的操作以及数据库备份相关操作

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习计网、mysql和算法 ✈️专栏&#xff1a;MySQL学习 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac…

罗技G29游戏方向盘试玩拆解,带震动力反馈

1.正好有时间记录下 自己的爱好 一千多的罗技G29游戏方向盘试玩拆解&#xff0c;带震动力反馈&#xff0c;值这个价吗_哔哩哔哩_bilibili 一千多的罗技G29游戏方向盘试玩拆解&#xff0c;带震动力反馈&#xff0c;值这个价吗_哔哩哔哩_bilibili 2.拆解 3.2个大电机 4.主控芯…

上榜|美创入选《2024年网络与信息安全行业全景图》32个细分领域

近日&#xff0c;深圳市网络与信息安全行业协会正式发布《2024年网络与信息安全行业全景图》&#xff08;以下简称“全景图”&#xff09;&#xff0c;定位展现我国网络与信息安全行业整体生态及细分领域代表性厂商。 美创科技凭借硬核实力&#xff0c;成功入选数据安全、安全服…

外卖项目:使用AOP切面,完成公共字段自动填充(断点调试详细讲解)

文章目录 一、问题描述二、实现思路三、实现步骤四、断点实操五、代码演示 一、问题描述 我们已经完成了后台系统的员工管理功能和菜品分类功能的开发&#xff0c;在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段&#xff0c;在编辑员工或者编…

西井科技与安通控股签署战略合作协议 共创大物流全新生态

2024年3月21日&#xff0c;西井科技与安通控股在“上海硅巷”新象限空间正式签署战略合作框架协议。双方基于此前在集装箱物流的成功实践与资源优势&#xff0c;积极拓展在AI数字化产品、新能源自动驾驶解决方案和多场景应用&#xff0c;以及绿色物流链等领域的深度探索、强强联…

Pudgy Penguins交易量一路攀升 多次创下历史新高

日前&#xff0c;一个名为胖企鹅&#xff08;Pudgy Penguins&#xff09; NFT 项目交易量持续攀升&#xff0c;一度在3月9日成为NFT市场的“销冠”。事实上&#xff0c;从2023年下半年开始&#xff0c;Pudgy Penguins的地板价就在不断上升&#xff0c;进入2024年更是多次创下历…

练习题+题解:链表+dp

目录 1.链表指定区间反转题目描述输入格式:输出格式:输入样例:输出样例:题目分析代码实现 2.hxj和他的甜品盲盒I输入格式:输入样例:输出样例:样例解释 输入样例:输出样例:样例解释 题目分析Java代码实现 3.First 50 Prime Numbers输入格式输出格式输入样例输出样例题目解析代码…

MySQL之索引与事务

一 索引的概念 一种帮助系统查找信息的数据 数据库索引 是一个排序的列表&#xff0c;存储着索引值和这个值所对应的物理地址无须对整个表进行扫描&#xff0c;通过物理地 址就可以找到所需数据是表中一列或者若干列值排序的方法 需要额外的磁盘空间 索引的作用 1 数据库…

【干货】Java开发者快速上手.NET指南

前言 前几天有小伙伴在技术群里发了一个微软官方出的&#xff1a;适用于Java开发人员的.NET快速入门免费电子书&#xff0c;今天大姚来分享一下Java开发者想要快速上手.NET有哪些教程和优质资料。 微软适用于Java开发人员的.NET快速入门指南 下载阅读地址&#xff1a;适用于 …

惟客数据CTO 钱勇:数据资产运营创新和实践

​企业如何做好数据资产运营&#xff0c;有效挖掘和利用数据资产&#xff1f; 近日&#xff0c;在由华东江苏大数据交易中心主办的“第四届数字经济科技大会”上&#xff0c;WakeData惟客数据CTO、星光数智CEO 钱勇 给出了自己的观点。 在演讲环节&#xff0c;钱勇以《数据资…

用tp6写的简单的eml的登录和curd

项目地址&#xff1a; 企业管理eml: 这是一个简单的eml (gitee.com) 1.登录和主页显示 1.1 登录功能逻辑图 1.2 控制器 app/controller/index.php php think make:validate LoginValidate <?php namespace app\controller;use app\BaseController; use app\model\User; …

IDEA设置全局配置

1、 IDEA设置全局配置 在IDEA中&#xff0c;选择 File -> Close Project 关闭项目。然后选择Customize -> All settings 进行全局配置&#xff0c;即所有项目公共的配置。 配置文件编码 配置控制台编码 配置maven 配置文件模板 配置文件模板作者和时间信息如下&#xff…

德勤:《亚太地区半导体行业展望》

2024年2月22日&#xff0c;德勤联合全球半导体联盟&#xff08;GSA&#xff09;对亚洲半导体产业链相关企业展开调研&#xff0c;邀请数位亚太地区主要半导体企业领导人&#xff0c;共同探讨半导体企业在当前环境下应如何通过数字技术曲线的领先优势保持业务竞争力和盈利能力&a…