分类 编程相关 下的文章

用过Flutter,对数据的响应式,有进一步了解。于是,回过头来再看看VueJS,自然而然地理解了。

找到一个比较好的Vue3示例代码,展示了使用Vue3的主要代码,涉及Vue3的模板语言、组件等。

Vue3 起步简单示例
https://blog.csdn.net/thankseveryday/article/details/124741733

我在代码加上注释,更容易理解:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/vue@next"></script>
<title>Learn Vue3</title>
</head>

<body>
<!-- 界面模板 -->
<div id="app">
    <!-- 两个大括号,获取data()方法返回对象的属性的值,给文本绑定值 -->
    <p>{{counter}}</p>
    <p>
        <!-- :title是v-bind:title的缩写,给HTML标签的属性绑定值 -->
        <span :title="message">{{message}}</span>
    </p>
    
    <!-- @click 是 v-on:click 的缩写,指定事件的执行方法 -->
    <p><button @click="reverseMessage">翻转文字</button></p>
    
    <!-- “v-”前缀的特殊属性,是Vue的指令 -->
    <!-- 使用 v-model 指令来实现双向数据绑定 -->
    <p><input type="text" v-model="message"></p>
    
    <!-- v-if ,当值为true时,才显示对应HTML -->
    <p v-if="seen">你能看到我吗?</p>
    
    <p><button @click="seenYN">显示/隐藏</button></p>
    <h4>我爱吃的水果:</h4>
    <ul>
        <!-- v-for,循环输出HTML -->
        <li v-for="fruit in fruits">{{fruit}}</li>
    </ul>
    <h4>周末计划:</h4>
    <ol>
        <!-- todo-item对应自定义组件TodoItem -->
        <todo-item v-for="todo in todos" :todo="todo" :key="todo"></todo-item>
    </ol>
</div>
<script>
// 定义自定义组件
const TodoItem = {
    props: ['todo'],
    template: '<li>{{todo}}</li>'
}

// 数据对象,有点像Flutter的State或者Provider
const dataObj = {
    // 声明用到的自定义组件
    components: {
        TodoItem
    },
    
    // 定义数据对象,有用的属性可以先设置null,用于占位
    data() {
        console.log("vue3 demo, data()");
        return {
            counter: 0,
            message: "hello vue",
            seen: true,
            fruits: ["apple", "orange", "bananas"],
            todos: ["钢琴课", "绘画课", "看电影"]
        }
    },
    
    // 在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图
    created() {
        console.log("vue3 demo, created()");
    },
    
    // 在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
    mounted() {
        console.log("vue3 demo, mounted()");
        setInterval(() => {
            this.counter++
        }, 1000);
    },
    
    // 操作数据的方法,一般用于页面元素的事件处理
    methods: {
        reverseMessage: function () {
            // 操作数据,让Vue更新界面
            this.message = this.message.split('').reverse().join('');
        },
        seenYN: function () {
            this.seen = !this.seen;
        }
    }
}

// 数据与模板绑定
Vue.createApp(dataObj).mount('#app')
</script>
</body>
</html>

更详细的介绍和相关文档,可以访问Vue3的官网:
https://v3.cn.vuejs.org/guide/introduction.html#vue-js-%E6%98%AF%E4%BB%80%E4%B9%88

近来工作中用上了Flutter,并且使用了Provider作为状态管理,确实爽,但是也踩了一下坑。

一 概述

Provider是基于InheritedWidget组件,使用观察者模式 + 生产者消费者模式,实现状态共享,简直就是为了取代StatefulWidget而存在。相关资料:

Provider的Flutter插件网址:
https://pub.dev/packages/provider

Provider的官方中文说明:
https://github.com/rrousselGit/provider/blob/master/resources/translations/zh-CN/README.md

二 总结

  1. Provider可以定义在任意地方,其状态只提供给其子Widget访问。例如,定义在App之上可实现全局的状态共享的状态,定义在页面之上可实现页面内的状态共享。
  2. Provider的子Widget(即child参数)不能传入StatelessWidget对象,或者说不能直接直接传入Widget对象,否则后面的所有孙Widget不能通过context获取其状态。解决方案是使用builder参数,传入构建子Widget的函数,或者child参数设置带有builder函数的Widget,例如Builder对象。
  3. 数据变化,必然导致重绘。所以不要过于担心是否重绘,而重点关注重绘的点在哪里,如何减少重绘的Widget。重绘Widget,会向上找到最近的builder方法并执行。所以需要重绘的Widget,最好放在其builder方法内。需要变化的StatelessWidget对象,用Builder类的builder方法包裹,是个很好的做法。
  4. Provider是以类型区分数据的。如果是多个相同数据类型(例如int类型)的状态,则需要定义不同的类,且都含有该数据类型(例如int类型)的属性。
  5. 定义多个Provider,可以使用MultiProvider。
  6. 组合多个Provider对象,可以使用ProxyProvider。

三 Provider类型

一般使用ChangeNotifierProvider就可以,更多的Provider类型如下:

类型描述
Provider最基础的 provider 组成,接收一个任意值并暴露它。
ListenableProvider供可监听对象使用的特殊 provider。ListenableProvider 会监听对象,并在监听器被调用时更新依赖此对象的 widgets。
ChangeNotifierProvider为 ChangeNotifier 提供的 ListenableProvider 规范,会在需要时自动调用 ChangeNotifier.dispose。
ValueListenableProvider监听 ValueListenable,并且只暴露出 ValueListenable.value。
StreamProvider监听流,并暴露出当前的最新值。
FutureProvider接收一个 Future,并在其进入 complete 状态时更新依赖它的组件。

四 监听方式

获取Provider的状态,有以下三种方式:

  1. read,即只读。只获取状态,不进行监听。示例代码:

    // 使用Provider.of,需要加上参数“listen: false”
    T t = Provider.of<T>(context,listen: false));
    
    // 使用context.read方法最简单
    T t = context.read<T>();
    
  2. select,即只监听指定数据。指定数据有变化,才会执行重绘。

    // 使用Selector类,可以定义builder方法
    Selector<T, R>(
      selector: (_, t) {return t.r;},
      builder: (_, r, __) {return Text('${r}');}
    );
    
    // 使用context.select方法最简单。如果取出的数据需要重绘,则最好用Builder类包裹一下
    R r = context.select<T,R>(R cb(T value));
    
  3. watch,即监听状态的变化。状态有任何变化,都会执行重绘。

    // 使用Consumer类,可以定义builder方法
    Consumer<T>(
      builder: (_, t, __) {return Text('${t.r}');}
    );
    
    // 使用Provider.of方法。如果取出的数据需要重绘,则最好用Builder类包裹一下
    T t = Provider.of<T>(context);
    
    // 使用context.watch方法最简单。如果取出的数据需要重绘,则最好用Builder类包裹一下
    T t = context.watch<T>();
    

最近完成了一个小项目的数据库迁移,从微软SQL Server 2016迁移到MySQL 8。过程没什么复杂,只是需要注意一下数据类型和SQL语法的转换。

1 环境

原数据库是SQL Server 2016。迁移的目标环境,操作系统为Debian 11,安装了MySQL 8。

2 还原SQL Server数据库备份

拿到手的是SQL Server数据库备份,需要还原出来再迁移。幸好微软推出了SQL Server的Linux版,而且官方提供了可用于开发测试的Docker镜像,几个步骤就部署并还原好SQL Server数据库。

参考资料:

1)在Debian上安装Docker的官方教程:
Install Docker Engine on Debian
https://docs.docker.com/engine/install/debian/

2)运行SQL Server 2019 Docker镜像的官方教程:
Quickstart: Run SQL Server container images with Docker
https://docs.microsoft.com/en-us/sql/linux/quickstart-install-connect-docker?view=sql-server-ver15&pivots=cs1-bash

3)SQL Server 2019的微软官方Docker镜像:
dockerhub - Microsoft SQL Server
https://hub.docker.com/_/microsoft-mssql-server

3 MySQL的准备

由于SQL Server的数据库表名不区分大小写,MySQL为了兼容相关SQL语句,也需要设置表名不区分大小写。即设置MySQL的参数lower_case_table_names=1,MySQL在存储和查询时,都把表名转为小写后再执行处理。

这里最麻烦的是,如果MySQL原来设置了lower_case_table_names=0(一般Linux上安装MySQL的默认值),需要把data文件夹清空,更新设置后重新初始化MySQL的数据。如果直接更改该值,MySQL重启后会报错。

关键的操作步骤:

1)修改MySQL的配置文件(Debian的默认路径为:/etc/mysql/mysql.conf.d/mysql.cnf),在[mysqld]节点下,加入一行lower_case_table_names=1

2)重新初始化MySQL(已有数据库的话,先做好备份,初始化后再还原),先清空数据文件夹(Debian的默认路径:/var/lib/mysql),然后执行以下命令:

mysqld --user=root --initialize --lower-case-table-names=1

初始化成功后,root用户的密码会记录在/var/log/mysql/error.log

4 迁移数据库定义

即导出原数据库表的create语句。一般推荐使用MySQL Workbench的Migration功能,官方文档如下:

MySQL Workbench - Using the MySQL Workbench Migration Wizard
https://dev.mysql.com/doc/workbench/en/wb-migration-wizard.html

但是我所安装的MySQL Workbench不能连接到Docker部署的SQL Server,所以使用了已安装的HeidiSQL,导出原数据库表的create table语句,然后手工修正为MySQL的语法。一些修改操作如下:

  • 修正字符编码,特别是设置了COLLATE的,需求改为COLLATE utf8mb4_0900_ai_ci
  • 修正默认值设置,例如DEFAULT '(0)'改为DEFAULT '0'DEFAULT getDate()改为DEFAULT CURRENT_TIMESTAMP
  • 自增型字段会被忽略,需要加上AUTO_INCREMENT
  • 字段类型转换,例如NVARCHAR改为VARCHARBIT改为TININY(1)MONEY改为DECIMAL(19,4)
  • 需要补上索引设置。

5 迁移数据库的数据

即导出所有数据的insert语句,然后在目标数据库利用source命令进行导入。一般也是推荐使用MySQL Workbench操作,不用担心语法和数据类型的问题。

我使用了DBeaver导出所有表的insert语句,然后手工修正为MySQL语法。需要注意:

  • 一般一条insert语句包含10000行数据,已提高导入时的效率。
  • 所有表名以数据库名.dbo开头的,都改为以数据库名开头。
  • 列名以中括号“[]”括住的,要改为“`”。

6 修改程序的SQL语句

主要是把SQL Server的语法,改为MySQL的语法。总结如下:

  • TOP改为LIMIT
  • getDate()改为CURRENT_TIMESTAMP
  • 去掉表名前的dbo.
  • WITH(NOLOCK)的处理。SQL Server加了WITH(NOLOCK)的语句,如果MySQL的InnoDB设置innodb_autoinc_lock_mode=0,需要特殊处理该语句,否则直接去掉WITH(NOLOCK)。关于InnoDB的设置说明如下:

MySQL innodb_autoinc_lock_mode 详解
https://www.cnblogs.com/JiangLe/p/6362770.html

当Service方法被内部调用时,Spring注解会失效。就是Spring的Service类,如果public方法加上注解,类内部的其它方法使用this调用该方法,会导致注解失效。

例如Spring的Service实现类如下:

@Service("userService")
class UserServiceImpl implements UserService{
    
    @Override
    @Cacheable(CacheNames="USER_CACHE", key="#userId")
    public User getUser(String userId){
        // do something
    }

    @Override
    public String getUserName(String userId) {
        User u = this.getUser(userId); // getUser方法的@Cacheable注解失效了
        return u == null ? "" : u.getName();
    }
}

原因是this引用的对象没有被Spring代理,调用该对象的public方法时,Spring不能处理相关注解。

解决方法很简单,就是使用Spring代理过的对象,代替this。然后只需解决如果获取该Service类被Spring代理过的对象。


1 循环依赖

就是自己注入自己。在Service类定义一个自身对象的属性,让Spring装配时把自己注入到自己。虽然Spring 5(具体版本待查证)声称解决了循环依赖的问题,但是Spring Boot 2.6.0开始默认设置不允许循环依赖。循环依赖是一个古老的问题,一样认为要避免。所以此方法不推荐

1)先设置

# Spring Boot 2.6.0之后,允许循环依赖
spring.main.allow-circular-references = true

2)上面的例子改为

@Service("userService")
class UserServiceImpl implements UserService{
    
    @Autowired
    private UserService self; // 自己注入自己
    
    @Override
    @Cacheable(CacheNames="USER_CACHE", key="#userId")
    public User getUser(String userId){
        // do something
    }

    @Override
    public String getUserName(String userId) {
        User u = self.getUser(userId); // 用self代替this,注解生效
        return u == null ? "" : u.getName();
    }
}

2 获取装配后的自己

避免Bean的循环依赖,主要思路是,在Bean装配完成后,再获取被Spring代理的自己。至于怎样获取,实现方法是多种多样的。

方法1,从Bean容器中获取自己。即:

UserService self = applicationContext.getBean("userService");

至于怎么获取Bean容器applicationContext,方法也是多样的。

方法2,开启AspectJ自动代理来获取自己。

详细参考:一个Spring AOP的坑!很多人都犯过!

要注意,AspectJ自动代理不只是解决本文档的问题,需考虑是否会带来未知的问题。

开启AspectJ自动代理的方法有多种,这里列出三种:

1)在启动类添加注解:

@EnableAspectJAutoProxy(proxyTargetClass=true, exposeProxy=true)

2)Spring增加配置:

<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true" />

3)Spring Boot的配置文件增加配置:

# 开启AspectJ自动代理
spring.aop.auto=true
# 开启CGLIB代理
spring.aop.proxy-target-class=true

然后就可以在当前Service类的方法中,通过类似的代码调用自身的方法,且能保证该方法的注解正常执行:

User u = ((UserService) AopContext.currentProxy()).getUser(userId);

方法3,延迟执行自己注入自己。

很简单,就是使用@Lazy注解,达到Bean初始化不执行自己注入自己,避免循环依赖的错误。我记得解决Spring 2.x循环依赖的问题时,也是采用延迟注入的配置。此方法写的代码最少,目前倾向采用这种方法。于是,上面的代码改为:

@Service("userService")
class UserServiceImpl implements UserService{
    
    @Lazy
    @Autowired
    private UserService self; // 延迟自己注入自己
    
    @Override
    @Cacheable(CacheNames="USER_CACHE", key="#userId")
    public User getUser(String userId){
        // do something
    }

    @Override
    public String getUserName(String userId) {
        User u = self.getUser(userId); // 用self代替this,注解生效
        return u == null ? "" : u.getName();
    }
}

XDA论坛上看了很久,Android 11就只有crDroid有官方支持。加上Resurrection Remix OS用得真不爽,主要是自动杀进程,而且不分前后台。于是五一假期狠下心刷上crDroid,用了一段时间,很爽!流畅、不杀进程、支持国产支付app的指纹,续航还行。

注:目前此ROM处于无人维护状态!

1. 相关下载

1.1 crDroid ROM
官网不仅可以下载ROM,还提供小米官方固件(欧版MIUI)、Recovery、OpenGapps等的链接:
https://crdroid.net/picasso

1.2 欧版MIUI
目前最新版是12.1.8,据说今年6月会推出12.5适配。当然,这不是重点。下载地址:
xiaomi.eu_multi_HMK305G_V12.1.8.0.RGICNXM_v12-11.zip
https://sourceforge.net/projects/xiaomi-eu-multilang-miui-roms/files/xiaomi.eu/MIUI-STABLE-RELEASES/MIUIv12/

1.3 Recovery
crDroid推荐的Recovery是OrangeFox。界面跟Twrp非常不同,感觉更像是文件管理器,但功能还是一样。
OrangeFox-picasso-stable@R11.1_1.7.zip
https://orangefox.download/zh-CN/device/picasso

1.4 Margisk
目前几乎是获取root权限的唯一方案。下载地址:
https://github.com/topjohnwu/Magisk/releases

要注意,从Margisk 22开始,root安装包和root管理工具,都合并为一个apk文件。root系统时,只需用Recovery刷入该apk文件。

1.5 Gapps
由于OpenGapps的Android 11版还没正式发布(当前处于测试),所以我选用NikGApps。下载链接:
https://nikgapps.com/downloads.html#downloads

一般选在Core版就可以了。值得注意的是,如果要开启Google Assistant(Google助手),需要把Google Velvet(即Google搜索)安装为系统App。NikGApps提供了对应的Addons(文件名为:NikGapps-Addon-11-Velvet-signed.zip),可以通过Recovery直接刷入系统。

2. 刷机过程

2.1 解锁及刷Recovery
都是常识性操作。刷Recovery后,最好先备份当前系统。

2.2 备份数据
钛备份不再更新了,推荐使用OAndBackupX备份App及数据。可通过F-Droid安装:
https://f-droid.org/en/packages/com.machiav3lli.backup/

还有联系人和短信的备份,不再赘述。

2.3 刷机步骤
按刷机顺序排列,crDroid的方案是:OrangeFox -> 欧版MIUI -> crDroid -> Margisk -> OpenGapps。我的方案只是用NikGApps替换OpenGapps。另外,最好先“双清”再开刷。

3. 初始设置

3.1 去掉WiFi和信号出现“x”符号
需要使用adb执行以下命令:

adb shell settings put global captive_portal_mode 0
adb shell settings put global captive_portal_https_url https://www.google.cn/generate_204

命令执行成功后,手机开启飞行模式,然后关闭飞行模式,即可解决。
参考:https://www.uso.cn/post/view/47990

3.2 开启Google Assistant(可选)
初次进入系统后,不插入SIM卡,语言选English(US)。安装Google Assistant并设置开启。成功后,再插入SIM卡,并设语言为中文。

Eclipse虽老,但仍然好用,特别是对于老Java码农来说。

1. 版本选择

一般Java的Web开发,选择Eclipse IDE for Enterprise Java and Web Developers。官方下载链接如下:
https://www.eclipse.org/downloads/packages/

2. 实用的初始化设置

1)修改默认字符集为UTF-8

windows -> Preferences -> General -> Workspace -> Text file encoding -> 选UTF-8

2)修改默认字体,改善中文显示太小

windows -> Preferences -> General -> Appearance -> Colors and Fonts -> Basic -> Text Font -> Edit -> 选Courier New

3)代码自动提示功能失效的解决方法

Window -> Preferences -> Java -> Editor -> Content Assist -> Advanced -> Select the proposal kinds contained in the 'default' content assist list -> 勾选"Java Non-Type Proposals"、"Java Proposals"、"Java Type Proposals"三个选项

4)在输错方法后仍然出现代码自动提示

Window -> Preferences -> Java -> Editor -> Content Assist -> Auto Activation triggers for java -> 改为.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

5)禁用Language Servers,提高Eclipse性能

Window -> Preferences -> Language Servers -> 取消所有勾选

6)安装插件Quick Bookmarks plugin,快速的标记和访问书签

  • 安装:Help -> Eclipse Marketplace -> 搜“Quick Bookmarks plugin”并安装
  • 使用:Alt+[数字],标记书签。Alt+Shift+[数字],跳转到书签。

工作上遇到需要在浏览器展示PDF文件,于是找到了Mozilla开发的PDF.js。玩了一下,结论是,直接使用其自带的示例是简单的方法。

PDF.js的官网:https://mozilla.github.io/pdf.js/
github项目页面:https://github.com/mozilla/pdf.js

主要是没找到有用的开发文档,而自己也不会Node.js的开发。所以尝试自己写的页面,只能利用HTML 5的canvas展示PDF内容。官方自带的示例,会把PDF转换成HTML,并且有显示目录、缩略图、打印、等各种实用的功能,媲美很多完整的PDF阅读软件。

这里记下写过的页面,可以翻页、放大缩小:

<h4>显示PDF</h4>
<p>
    <button id="prePage">上一页</button> <input type="text" id="curPage" value="" readonly /> <button id="nextPage">下一页</button>
    <br />
    <button id="zoomIn">+</button> <input type="text" id="zoomScale" value="1" readonly /> <button id="zoomOut">-</button>
</p>
<p>
    <canvas id="the-canvas" style="border: 1px solid black; direction: ltr;"></canvas>
</p>

<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="/js/pdfjs/pdf.js"></script>
<script>
//加载PDF按钮
pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdfjs/pdf.worker.js';//自己的路径

var loadingTask = pdfjsLib.getDocument('/pdfjs/docker.pdf'); 
var pdfDoc = null;
var totalpage = 0;
var curPage = $("#curPage");
var zoomScale = $("#zoomScale");

loadingTask.promise.then(function (pdf) {
    //加载指定界面(第一页)
    pdfDoc = pdf;
    totalpage = pdfDoc.numPages;
    getPage(1);
});

function getPage(pageNum) {
    if(pageNum < 1) {
        pageNum = 1;
    } else if(pageNum > totalpage) {
        pageNum = totalpage;
    }
    setCurPageNum(pageNum);
    pdfDoc.getPage(pageNum).then(function (page) {
        //var scale = 1;
        var viewport = page.getViewport({ scale: getZoomScale() });
        var canvas = document.getElementById('the-canvas');
        var context = canvas.getContext('2d');
        canvas.height = viewport.height;
        canvas.width = viewport.width;
        var renderContext = {
            canvasContext: context,
            viewport: viewport,
        };
        page.render(renderContext);
    });
}

function makeThumb(page) {
    // draw page to fit into 96x96 canvas
    var vp = page.getViewport(1);
    var canvas = document.createElement("canvas");
    canvas.width = canvas.height = 96;
    var scale = Math.min(canvas.width / vp.width, canvas.height / vp.height);
    return page.render({canvasContext: canvas.getContext("2d"), viewport: page.getViewport(scale)}).promise.then(function () {
        return canvas;
    });
}

function loadThumb() {
    var pages = []; while (pages.length < pdfDoc.numPages) pages.push(pages.length + 1);
    return Promise.all(pages.map(function (num) {
        // create a div for each page and build a small canvas for it
        var div = document.createElement("div");
        document.body.appendChild(div);
        return pdfDoc.getPage(num).then(makeThumb).then(function (canvas) {
            div.appendChild(canvas);
        });
    }));
}

function getCurPageNum() {
    return parseInt(curPage.val());
}
function setCurPageNum(pageNum) {
    return curPage.val(pageNum);
}
function getZoomScale() {
    return parseFloat(zoomScale.val());
}
function setZoomScale(scale) {
    return zoomScale.val(scale);
}
$("#prePage").click(function(){
    var pageNum = getCurPageNum() - 1;
    getPage(pageNum);
})
$("#nextPage").click(function(){
    var pageNum = getCurPageNum() + 1;
    getPage(pageNum);
})
$("#zoomIn").click(function(){
    var scale = getZoomScale();
    scale = Math.round((scale + 0.2) * 100) / 100;
    scale > 2 && (scale = 2);
    setZoomScale(scale);
    getPage(getCurPageNum());
})
$("#zoomOut").click(function(){
    var scale = getZoomScale();
    scale = Math.round((scale - 0.2) * 100) / 100;
    scale < 0.6 && (scale = 0.6);
    setZoomScale(scale);
    getPage(getCurPageNum());
})
</script>

最近接触了两款开源、跨平台、支持多种SQL数据库的数据库管理工具,值得记录一下。

DBeaver
官网:https://dbeaver.io/
2020年疫情期间,在家办公,想找个数据库管理工具,可以在Linux上访问SQL Server数据库,于是遇到DBeaver。界面像Eclipse,容易上手;基于Java,可以跨平台使用;使用JDBC,几乎支持所有数据库。在Linux上,几乎是万能的数据库管理工具了。

HeidiSQL
官网:https://www.heidisql.com/
在Windows上安装MariaDB 10.4.12时,发现自带了HeidiSQL数据库管理工具。界面及操作都跟MySQL Workbench相似,清晰明了,而且支持各种SQL数据。比较意外的是,其基于Delphi开发,所以Linux上需要利用Wine运行。

Google Camera(简称gcam)自推出以来,一直是最喜爱的摄影应用(可惜后来变成Pixel机型专属应用)。幸好有开发者移植到其它机型,特别是像红米Note 4X那种渣拍照的手机,极大提升了拍照能力。近来在红米K30 5G上也装上了gcam,并默认开启了“动态照片(Motion Photo)”功能。本来没啥影响,只是拍出来一堆文件名以MVIMG开头或者扩展名前带有“.MP”的照片,昨天好奇研究了一下,才发现就是动态照片。

动态照片,简单来说,就是把拍照前几秒录下视频,并把视频与拍出来的照片整合在一起,生成一个jpg文件。目前发现两种格式:文件名后面带有“.MP”的,在EXIF信息里会说明mp4文件在整个jpg文件中位置;文件名以“MVIMG_”开头的,jpg文件里有个“ftypmp4”标识,该标识后面的数据就是mp4文件。

其实动态照片也没什么坏处,但是找了一圈也找不到可以直接浏览动态图片的Android应用。如果是看不到的视频,那保存在照片中,有什么意义?还浪费了存储空间,也不利于分享、传输。最后找了个工具,叫GoMoPho,把所有拍摄的动态照片都转为普通jpg文件。其实就是把动态文件切割为jpg和mp4两个文件,我保留了jpg并删除了mp4。GoMoPho的相关网址如下:

Google motion photos video extractor.
https://github.com/cliveontoast/GoMoPho

GoMoPho虽然只有命令界面,但是支持上面提到的两种动态照片,而且已移植到多种操作系统,还能支持批量处理文件夹的文件。

作为MIUI恐惧者,无奈LineageOS官方没有支持这台红米K30 5G,只能诚惶诚恐地使用欧版MIUI。直到某天发现了Resurrection Remix OS官方支持这款手机,才终于脱离MIUI。

查了资料,才知道Resurrection Remix OS是基于LineageOS的一款开源ROM,基本体验与LineageOS一致,并增加了很多设置(基本上是界面的,个人感觉用途不大),适合喜欢原生Android的用户。

LineageOS的优点基本继承了,总结一下缺点吧:

  • 1)国内支付应用,基本不能使用指纹支付。历史原因,一直遗留下来的问题。跟LineageOS一样。
  • 2)关屏后不能双击屏幕打开锁屏界面
  • 3)不能双击桌面锁屏。可以设置双击任务栏或者三大金刚键进行锁屏。
  • 4)不能拍摄6400万像素的照片。ROM自带相机和Google Camera移植版,最高都只支持1610万像素。不过即使是MIUI的自带相机,拍出来的6400万像素照片基本直出(纯粹自我安慰)。
  • 5)刚开始使用,比较耗电。做了一些优化后,后面变得相对省电一点。不过,这手机本身也不怎么省电。
  • 6)长时间使用的应用,容易自动退出。比如Chrome开个视频后,很大几率会自动关掉并回到桌面。可能是电源管理自动优化吧。

总的来讲,曾经的LineageOS用户可以放心刷,也没有遇到影响日常使用的bug 。

刷机过程,跟LineageOS一样。重点还是那句:刷机前先备份好数据。Recovery备份分区(刷机失败时可以还原系统) + Ti Backup备份应用(用于迁移应用及数据)。

1. 下载相关数据

2. 刷机

  • 备份数据。最好连sdcard的数据也备份一下
  • wipe手机,即Recovery格式化data分区
  • 解锁、刷recovery
  • 刷ROM
  • Recovery里Root系统
  • 刷Opengapps
  • 清cache
  • 重启进入系统

要注意,进入系统后,需要连Google验证。

3. 耗电优化的处理。当前的节电设置如下:

  • a)屏幕刷新率设为 60Hz,在“设置”->“系统”->Device-specific settings->Minimum Refresh Rate。120Hz屏幕其实很丝滑,但60Hz确实够用。
  • b)关闭 5G 网络,只用 4G 。设置移动网络的首选网络类型为“LTE/WCDMA”。当前来说,5G除了耗电比较快,没感受到什么优势。
  • c)限制应用使用电量。在电池管理器设置受限应用。目前设了 Google Play 商店。
  • d)关闭 Google 账户的自动同步功能,只开启了 Chrome 和 Gmail 。
  • e)冻结了一些不常用但不可缺的应用。冰箱、Shelter 、island 等。其中 island 的名称太中二,个人接受不了而放弃。用 Shelter 的话,如果工作空间的应用没用到 GMS,最好冻结它,并尽量安装非无依赖 GMS 的应用。