对Jetpack Compose设计的初步解读与思考

节选自:对Jetpack Compose设计的初步解读与思考


Jetpack Compose前段时间进入了Alpha阶段。经过去年年底至今剧烈的api变化后,现在Compose整体上大致稳定,所以我们也能对于Compose的设计进行初步的解读和评价了。

Compose从整体技术风格上来说是这样一个产物:在语法上激进模仿SwiftUI,编译/运行过程充满Svelte风格,同时也综合了各方包括Android开发组自身对UI框架的思考结果。

使用Compose时,最值得关注的就是Compose的编译器插件。可以这么说,Compose的runtime、api都是依附于编译器插件的,那个巨大而无所不包的编译器插件才是Compose的本体。Compose插件强势的入侵了原版Kotlin的语法,导致包含了Compose的Kotlin基本上可以算作新语言(算成个新虚拟机都不过分)。初次了解的时候确实让我很困惑,因为这与React,Flutter推崇的趋势简直是背道而驰。但是了解了Svelte,SwiftUI之后,Compose显得没有那么突兀了。

Svelte

Svelte试图解决的问题是:如何用声明式的代码书写风格,一对一直接翻译成纯粹命令式的DOM操作,从而达到无额外开销+极小runtime的效果。最终Svelte选择发明一套自制的模板语法来翻译到Javascript上。这个好理解,因为大家很熟悉,显然javascript以及其工具链没有任何实现这个目标的可能。

Compose

Compose在框架设计方面的野心明显超越Flutter。Compose团队多次表示Compose就是对Android SDK的重写。Compose对自身的定位估计类似于SwiftUI在苹果系生态中的定位,那就是高层次、生态内通用、外加依靠自身定位尽可能挖掘以及定制工具链以实现先进的开发模式。Compose使用了语法上和Swift神似的Kotlin,也面临相似的问题,于是Compose(出于一些原因)选择了简单粗暴的魔改Kotlin编译器(而不是模仿SwiftUI玩类型系统杂耍)。

Compose团队解释过Compose的出发点:构建一个通用的、描述树状结构渲染过程的框架,不管是是手机UI组件树或者是浏览器HTML Element。

Compose一不做二不休,直接把Kotlin编译器魔改到底。最后利用编译器魔改实现了几大功能。

对Compose设计的思考

目前Compose的官方资料仍然较为缺乏,因此很难知道Compose除此之外的优化设计以及runtime具体调度机制。但总体来说,我认为

  1. Compose的编译期优化潜力较为巨大,在未来完全有实现SwiftUI所有编译期优化的潜力,尽管没有使用类型系统可能会导致某些实现更为困难。
  2. Compose对于原版Kotlin的强势侵入是其得以实现设计目标的重要原因。但也是一个隐患,Compose对语义的入侵过深,我们可以看看Compose的编译器可能会干什么事情
    1. 对每个树的节点,按照源代码位置,生成一个唯一的int型ID
    2. 插入start和group来表明节点的边界
    3. 收集向函数传递的参数,参数的性质
    4. 根据收集的信息指导一个指令机的工作,那个指令机工作在一个无类型的纸带上

这个已经可以算作在Kotlin/JVM上重新发明了一个虚拟机了。结构化的执行流程,内存,ABI,call site,复杂的调度策略都有了,我觉得就差来个人来证明能跑操作系统了。在已经因为DSL特性高度特化的Kotlin语言上继续发明新虚拟机,总归是有点奇怪的事情。与此对比,SwiftUI对语言的入侵很少而且是隐式的。

  1. 作为未来取代Android SDK的候选者,有强烈的风格取向,opinionated。为了实现对树状渲染结构“通用”的描述,Compose捆绑了一整套非常新的解决方案,从Positional Memoization,到安卓上第xxxxxxx个响应式数据解决方案@State(目前仍然缺乏资料以证明其通用性)。好是好,但是Angular前车之鉴在那里。公平的来说,SwiftUI也是强烈的风格取向,但SwiftUI在苹果生态中的地位个人觉得谷歌没法在Compose上复刻。而且加上强势侵入原语言语义,一旦要调整估计就要大调整。
  2. Compose大量功能处于编译器层,导致这些功能其实是没办法灵活调节的。Flutter我觉得官方的xxx不好还可以自己重写一个发布出去,才有了一堆群魔乱舞的东西,engine虽大但除了engine以外都可以自己写。Compose感觉很容易就会碰到编译器层。
  3. Compose由于基于编译器,很多的优化都是类似于编译器过一个pass的模式来的,尤其是Diff和常量消除那些地方,比较细碎,不容易归档解释,给人一种“想到哪里写到哪里”的感觉,目前官方的文档就有很多这种问题。
  4. 总之,非常期待以后真正理想的“通用”编程语言配上先进的前端框架。也许就是swift加上MPS。

Android Studio 无法保存插件的加密配置信息的解决方案

缘起

笔者转移Android Studio配置信息到新电脑,采用了简单粗暴的复制粘贴方案。然后发现在新电脑上,Translation插件的应用密钥没了而且无法保存到本地。

缘由

Android Studio 把加密配置信息保存在KeePass中,换电脑后无法访问数据库文件,然后保存密码的方案就会变为不保存密码。

缘解

打开 %USERPROFILE%\.AndroidStudio4.0\config,删除 c.kdbx 和 c.pwd。

进入 options ,删除security.xml。

重新打开Android Studio。

Android 获取本地IP地址和广播地址

在AndroidManifest文件中配置权限:

<!-- internet -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

NetUtil文件:

/**
 * 获取本地IP地址
 */
public static String getLocalIP() {
    try {
        // 获取本地设备的所有网络接口
        Enumeration<NetworkInterface> niEnum = NetworkInterface.getNetworkInterfaces();
        while (niEnum.hasMoreElements()) {
            // 获取网络接口的所有IP地址
            Enumeration<InetAddress> addressEnum = niEnum.nextElement().getInetAddresses();
            while (addressEnum.hasMoreElements()) {
                // 返回枚举集合中的下一个IP地址信息
                InetAddress inetAddress = addressEnum.nextElement();
                // 不是回环地址,并且是IPv4地址
                if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
                    return inetAddress.getHostAddress();
                }
            }
        }
    } catch (SocketException e) {
        e.printStackTrace();
    }
    return null;
}
/**
 * 获取广播地址
 */
public static String getBroadcastIP() {
    try {
        for (Enumeration<NetworkInterface> niEnum = NetworkInterface.getNetworkInterfaces(); niEnum.hasMoreElements(); ) {
            NetworkInterface ni = niEnum.nextElement();
            if (!ni.isLoopback()) {
                for (InterfaceAddress interfaceAddress : ni.getInterfaceAddresses()) {
                    if (interfaceAddress.getBroadcast() != null) {
                        return interfaceAddress.getBroadcast().getHostAddress();
                    }
                }
            }
        }
    } catch (SocketException e) {
        e.printStackTrace();
    }
    return null;
}

参考链接:

Android获取有线网ip地址

Android获取如何获取当前手机IP地址

[Android]获取局域网广播地址的两种方法

【转】一个简单的内存同步到数据库的方法

转载自:一个简单的内存同步到数据库的方法


为了提高大量数据运算的效率,我一般都会采用以下方式:
1.数据映射到内存
2.在内存中进行数据运算
3.将结果放入队列
4.队列与数据库同步数据

假设有一张表,并对此表做增删改操作

字段名 类型
Key INT
Value INT

如果操作的次数很多,且每次都直接用数据库更新,这很显然是不现实的
我需要映射到内存中做操作,然后用计时器批量更新数据。
为了保证内存数据与数据库的同步,我构建了以下对象:

Class Unit
{
public int key;
public int value;
public int control;
}

字段control是实现同步的关键。他表示内存中数据的几种状态,分别是:正常(0)、待新增(1)、待更新(2)、待删除(3)、直接删除(4)
状态的解释:
0:数据库映射到内存中时,control = 0,计时器更新时,不理会此条数据
1:New Unit时,control = 1,计时器更新时会把它添加入库
2:对Unit做更新操作时,control = 2,计时器更新时会做数据库更新操作
3:对Unit做删除操作时,control = 3,表示打上删除标记,计时器更新时会做数据库删除操作
4:对New Unit做删除后,control = 4,计时器更新时会直接把对象删除 继续阅读【转】一个简单的内存同步到数据库的方法

Android判断应用在前台和切换应用到前台的方法

/**
 * 判断应用在前台
 */
public static boolean isForeground(Context context) {
    boolean isForeground = false;
    ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
    if (activityManager != null) {
        ComponentName componentName = activityManager.getRunningTasks(1).get(0).topActivity;
        if (componentName != null) {
            isForeground = componentName.getPackageName().equals(context.getPackageName());
        }
    }
    if (isForeground) {
        isForeground = isForegroundByProcess();
    }
    return isForeground;
}

/**
 * 根据进程判断应用在前台
 */
private static boolean isForegroundByProcess() {
    boolean isForeground = true;
    String filePath = "/proc/" + android.os.Process.myPid() + "/cgroup";
    try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
        String line = br.readLine();
        if (line != null && line.contains("bg_non_interactive")) {
            isForeground = false;
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return isForeground;
}

/**
 * 切换应用到前台
 */
public static void switchToForeground(Context context) {
    ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
    List<ActivityManager.RunningTaskInfo> taskInfoList = activityManager.getRunningTasks(100);
    for (ActivityManager.RunningTaskInfo taskInfo : taskInfoList) {
        ComponentName componentName = taskInfo.topActivity;
        if (componentName != null && componentName.getPackageName().equals(context.getPackageName())) {
            activityManager.moveTaskToFront(taskInfo.id, 0);
            break;
        }
    }
}

参考资料:

判断App位于前台或者后台的6种方法

Android 将后台应用切换到前台

微信数据库解密

节选自:微信数据库解密


  • 微信数据库加密方式:
1.获取手机IMEI码

2.获取当前登录微信账号的uin(存储在sp里面)

3.拼接IMEI和uin

4.将拼接完的字符串进行md5加密

5.截取加完密的字符串的前七位(字母必须为小写)

上面可以看到就两个变量,`uin`和`imei`

代码送上直接使用:https://github.com/l123456789jy/WxDatabaseDecryptKey

Android点击EditText文本框之外任意位置隐藏键盘

实现思路:通过 dispatchTouchEvent 每次 ACTION_DOWN 事件中动态判断非 EditText 本身区域的点击事件,然后在事件中进行屏蔽。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        getWindow().getDecorView().postDelayed(() -> {
            View view = getCurrentFocus();
            if (isShouldHideInput(view, ev)) {
                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                if (imm != null) {
                    imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
                    view.clearFocus();
                }
            }
        }, 100);
    }
    return super.dispatchTouchEvent(ev);
}

private boolean isShouldHideInput(View v, MotionEvent event) {
    if (v instanceof EditText) {
        int[] leftTop = new int[]{0, 0};
        //获取输入框当前的location位置
        v.getLocationInWindow(leftTop);
        int left = leftTop[0];
        int top = leftTop[1];
        int bottom = top + v.getHeight();
        int right = left + v.getWidth();
        return !(event.getRawX() > left && event.getRawX() < right && event.getRawY() > top && event.getRawY() < bottom);
    }
    return false;
}

参考链接:

Android点击EditText文本框之外任何地方隐藏键盘的解决办法

Android取消EditText自动默认获取焦点行为

转载自:Android取消EditText自动默认获取焦点行为


Android控件EditText自动默认会获取屏幕焦点,取消这个行为只需要两行代码,如下:

android:focusable="true"
android:focusableInTouchMode="true"

我们只需要把这两行代码添加至EditText控件的父级控件中,把屏幕焦点设置到父级控件上,EditText就会暂时失去焦点。

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:orientation="vertical">

    <EditText
        android:id="@+id/et_user_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>