测试环境

设备:Sony Xperia XZ Premium (docomo版本)
版本:官方Android 9.0
内核版本:4.4.302
版本号:47.2.E.3.29
Screenshot_20220212-105754


前言

With CONFIG_ZRAM_WRITEBACK, zram can write idle/incompressible page
to backing storage rather than keeping it in memory.
(抄自kernel中Documentation/blockdev/zram.txt的介绍)

通俗解答,就是相当于swap那样把zram中空闲与不可压缩的玩意写进存储设备里,需要时再读回去。

那么为啥搞这玩意呢,因为4G内存不够了,zram再怎么压也确实缺,就算sony有z3fold加成,开个微信开个qq后android lmk还是乱杀,simple lmk也没能改善。

按照你google的意思就是不用内核里的android lmk,改用用户空间的lmkd,通过压力失速信息 (PSI) 监视器来获取关于内存压力水平的通知。然后PSI在4.9内核中才有,由于本人菜鸡没能backport(

所以,我还是顶着就算是ufs2.1,我也要搞writeback,就为了多一些后台存活能力(

说的就是你微信,国内不走fcm不走mipush非要进程在后台才给我推消息


需要做什么

  • 有源码,有root的设备

  • 内核中的zram需要支持writeback,这个特性在4.4的android内核上应该是没有的(起码索官方没看到)

  • 需要系统支持(Android10开始引入支持,google的Android9源码上没有)

  • 以及需要一个好歹会点基础中的皮毛的你(

关于系统支持这个,确实也有dalao做模块去使用,因为这玩意也就是在合适的时候(例如设备空闲,锁屏后)把值写进/sys/block/zramX/下的接口里,但是直接从系统框架里走JobScheduler不是更香么(


内核部分

需要pick的commit有些多,故我就直接找了别人压缩了一堆upstream和backport的squash版本,可参考
zram: squash partial IO refactoring
zram: introduce ZRAM_IDLE flag
pick上去后自行修正冲突,然后defconfig里将writeback打开,编译刷入即可,具体不做累述。


系统部分(系统干了啥)

如题,我们得先知道它系统干了啥,才能做对应的移植工作。
在高版本中,zram的配置直接写在fstab中,包括zram大小,writeback的位置和大小。系统会读取fstab中的参数并且进行配置。这一部分的东西由fs_mgr来完成。鉴于这玩意是c++编译出来的lib,不方便移植进系统内(毕竟大法原生)。故这一块手动完成并且写个自启脚本即可代替。

(最简单的,写个sh,延迟一下,然后写上要做的命令,再扔magisk的service.d下)。

那么,fs_mgr.cpp干了啥。
QQ截图20220212015416
我们可以看到,android现在把writeback的文件位置写死为/data/per_boot下的zram_swap文件。在PrepareZramBackingDevice函数中创建writeback的文件并且用DirectIO的方式挂载为loop设备。
QQ截图20220212015451
在fs_mgr_swapon_all根据从fstab中读到的参数进行调用创建。

然后就是Framework部分了,从google在android10的commit Create a Zram writeback job 中可以看到,在zramwriteback.java部分中,先判断zram下的backing_dev是否为none,为none就没开zramwriteback,则不执行,开了就用onStartJob执行任务。

    private static final String IDLE_SYS = "/sys/block/zram%d/idle";
    private static final String IDLE_SYS_ALL_PAGES = "all";
    private static final String WB_SYS = "/sys/block/zram%d/writeback";
    private static final String WB_SYS_IDLE_PAGES = "idle";
    private static final String WB_STATS_SYS = "/sys/block/zram%d/bd_stat";
    private static final int WB_STATS_MAX_FILE_SIZE = 128;

    private void markPagesAsIdle() {
        String idlePath = String.format(IDLE_SYS, sZramDeviceId);
        try {
            FileUtils.stringToFile(new File(idlePath), IDLE_SYS_ALL_PAGES);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to write to " + idlePath);
        }
    }
    private void flushIdlePages() {
        if (DEBUG) Slog.d(TAG, "Start writing back idle pages to disk");
        String wbPath = String.format(WB_SYS, sZramDeviceId);
        try {
            FileUtils.stringToFile(new File(wbPath), WB_SYS_IDLE_PAGES);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to write to " + wbPath);
        }
        if (DEBUG) Slog.d(TAG, "Finished writeback back idle pages");
    }

    private int getWrittenPageCount() {
        String wbStatsPath = String.format(WB_STATS_SYS, sZramDeviceId);
        try {
            String wbStats = FileUtils
                    .readTextFile(new File(wbStatsPath), WB_STATS_MAX_FILE_SIZE, "");
            return Integer.parseInt(wbStats.trim().split("\\s+")[2], 10);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to read writeback stats from " + wbStatsPath);
        }
        return -1;
    }
    private void markAndFlushPages() {
        int pageCount = getWrittenPageCount();
        flushIdlePages();
        markPagesAsIdle();
        if (pageCount != -1) {
            Slog.i(TAG, "Total pages written to disk is " + (getWrittenPageCount() - pageCount));
        }
    }

上面这串就是主要部分了(懒得截图了),原理与

echo "all" > /sys/block/zram0/idle
echo "idle" > /sys/block/zram0/writeback

是基本一样的。
声明zram页空闲,然后在合适的时间请求空闲页写回。
那么什么是合适的时间?

        // Schedule a one time job to flush idle pages to disk.
        // After the initial writeback, subsequent writebacks are done at interval set
        // by ro.zram.periodic_wb_delay_hours.
        js.schedule(new JobInfo.Builder(WRITEBACK_IDLE_JOB_ID, sZramWriteback)
                        .setMinimumLatency(TimeUnit.MINUTES.toMillis(firstWbDelay))
                        .setRequiresDeviceIdle(true)
                        .build());

其中setRequireDeviceIdle,判断设备进入idle状态后才会执行。任务执行时间可由prop定义,如prop无定义则由默认值

        int markIdleDelay = SystemProperties.getInt(MARK_IDLE_DELAY_PROP, 20);
        int firstWbDelay = SystemProperties.getInt(FIRST_WB_DELAY_PROP, 180);`

默认是20分钟后进行一次标志,180分钟后写回操作。

然后就是StorageManagerService类部分
QQ截图20220212022416
在storagemanagerservice中判断persist.sys.zram_enabled值是否为1,然后framework-res.apk中的config_zramWriteback的值是否为true,才会去调用执行zrambackwrite.java中的任务。


系统部分(需要做的)

我们需要手动替代fs_mgr做的事

(位置,大小均可以自定义,framework只通过检测是否为none判断是否启用了writeback,这里以官方地址为例)
同样,也在/data下创建一个per_boot文件夹,在手机终端/adb下

mkdir -p /data/per_boot

然后我们创一个1G的writeback文件

dd if=/dev/zero of=/data/per_boot/zram_swap bs=1024 count=1048576

创建后我们将这个设备挂载到loop设备上
我这边loop0-7都是空的,那么我就挂上loop0

losetup /dev/block/loop0 /data/per_boot/zram_swap

如果没有报错,即正常挂上了。
我们测试一下
先关闭原有zram

swapoff /dev/block/zram0

然后触发zram初始化

echo 1 > /sys/block/zram/reset

将挂载的zram_swap的地址写到writeback里
(这一步执行后可以通过dmesg |grep zram来查看有没有正常挂上)

echo "/dev/block/loop0" > /sys/block/zram0/backing_dev

那么接着配置zram大小,这以2g为例

echo 2147483648 > /sys/block/zram0/disksize

最后开启zram

mkswap /dev/block/zram0
swapon /dev/block/zram0

可以通过cat命令或者用scene5去查看writeback的情况

以上我们需要的步骤可以全部做成自启脚本,在系统启动时就完成

我们测试一下回读情况

echo all > /sys/block/zram0/idle
echo idle > /sys/block/zram0/writeback

然后在scene5里可以看到zram压缩后的大小变小了,writeback有回写数据了,即正常。


将代码注入到framework里

在此之前,由于需要修改framework-res的AndroidManifest,故需要对services.jar修改支持核心破解(或者幸运破解器),反编译修改请参考去除签名验证

这部分我是用jadx换回java对着看改的(我菜炸了
ZramWriteback.smali和ZramWriteback$1.smali可以直接扔进去,放在com/android/server下。
我们需要修改的就是StorageManagerService.smali了,但是这里我们先不改这个,我们先去把framework-res.apk改了,原因后面会提到。

那么反编译framework-res后,打开AndroidManifest.xml,找到

<service android:exported="true" android:name="com.android.server.MountServiceIdler" android:permission="android.permission.BIND_JOB_SERVICE"/>

那么我们在下面新加一句

<service android:exported="false" android:name="com.android.server.ZramWriteback" android:permission="android.permission.BIND_JOB_SERVICE"/>

这是给zramwriteback任务使其拥有BIND_JOB_SERVICE权限的,必须要加,不然会因为没有权限而在启动时报错。然后我们进入res/values里,在最后一行

</resources>

的上面一行,添加

<bool name="config_zramWriteback">true</bool>

然后修改完成,回编译
回编译后再次对修改后的framework-res.apk进行反编译。

一是检查AndroidManifest里是否修改成功,我这边有时候apktool抽风并没有改成。二是需要找一个值。

在res/values文件夹打开public.xml,搜索config_zramWriteback
QQ截图20220212164516
记住后面这个id,例如我这边就是0x01120113,这个id是需要用的。现在反编译的这个不需要回编译,上面修改的回编译就是修改好的。

再去处理services.jar的storagemanagerservice.smali
需要修改两个method,先找到
.method private handleSystemReady()V

invoke-direct {p0}, Lcom/android/server/StorageManagerService;->refreshZramSettings()V

的后面到return-void的前面加上

    const-string/jumbo v0, "persist.sys.zram_enabled"

    invoke-static {v0}, Landroid/os/SystemProperties;->get(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v0

    .local v0, "zramPropValue":Ljava/lang/String;
    const-string v1, "0"

    invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result v1

    if-nez v1, :cond_0

    iget-object v1, p0, Lcom/android/server/StorageManagerService;->mContext:Landroid/content/Context;

    invoke-virtual {v1}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;

    move-result-object v1

    const v2, 0x01120113

    invoke-virtual {v1, v2}, Landroid/content/res/Resources;->getBoolean(I)Z

    move-result v1

    if-eqz v1, :cond_0

    iget-object v1, p0, Lcom/android/server/StorageManagerService;->mContext:Landroid/content/Context;

    invoke-static {v1}, Lcom/android/server/ZramWriteback;->scheduleZramWriteback(Landroid/content/Context;)V

    :cond_0

在这个最后的cond_0后是return-void
这里由于我Android9上并没有refreshIsolatedStorageSettings函数,故去除了。
注意:寄存器要自己调整!

TIP:想偷懒的话,拿jadx转换下自己原版的smali,和google的对比下,然后把整个method换过去,少了补上,多的去掉即可(

然后是.method private refreshZramSettings()V在这块由于我设备上的和google的基本一样,就直接替换过来了(

当然,要补的话就是在

    .local v2, "desiredPropertyValue":Ljava/lang/String;
    :goto_26
    invoke-virtual {v2, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result v4

    if-nez v4, :cond_49

的后面,:cond_xx return-void的前面插入

    invoke-static {v0, v2}, Landroid/os/SystemProperties;->set(Ljava/lang/String;Ljava/lang/String;)V

    invoke-virtual {v2, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result v0

    if-eqz v0, :cond_49

    iget-object v0, p0, Lcom/android/server/StorageManagerService;->mContext:Landroid/content/Context;

    invoke-virtual {v0}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;

    move-result-object v0

    const v3, 0x01120113

    invoke-virtual {v0, v3}, Landroid/content/res/Resources;->getBoolean(I)Z

    move-result v0

    if-eqz v0, :cond_49

    iget-object v0, p0, Lcom/android/server/StorageManagerService;->mContext:Landroid/content/Context;

    invoke-static {v0}, Lcom/android/server/ZramWriteback;->scheduleZramWriteback(Landroid/content/Context;)V

同样,自行修改寄存器

插入进去后没结束,还记得前面记得id的值么,要改,上面两部分中都有(不包括const v数字)

const v2, 0x01120113

这个0x啥的就是对应com.android.internal.R.bool.config_zramWriteback的id,修改成我们上面记录的id

至于为什么上面framework-res的时候先回编译,因为apktool会自己在public里生成我们新加布尔值的id,生成后再反编译查看

这里提供下这三文件,放在我的E盘里了,在Blog_Files目录下,自行参考,文件为大法markII官方rom提取。


后续&调试

把回编译的文件自行放入system里替换掉,做好备份,然后开机

小贴士:测试前开好adb,授权好,一旦炸了还能根据logcat来查问题

注意:/sys/block/zram/下的接口如果是root所有者与用户组的话,框架层是没有权限写入的!
根据logcat我们可以看到“Failed to write to /sys/block/zram0/idle”
我们需要在前面搞得自启脚本里加上

chown system:system /sys/block/zram0/idle
chown system:system /sys/block/zram0/writeback
chown system:system /sys/block/zram0/bd_stat
chown system:system /sys/block/zram0/backing_dev

使得系统有权限写入。

这里说个调试小技巧,在最早给我报写入错误时是不知道原因的,因为google的这个代码里只用Slog.e打印出,并没有抛出异常。没有具体异常我们肯定看不到什么的。以markpageasidle为例,在

invoke-static {v3, v2}, Landroid/util/Slog;->e(Ljava/lang/String;Ljava/lang/String;)I·

也就是slog.e的后面一行我们加上

invoke-virtual {v1}, Ljava/io/IOException;->printStackTrace()V

也就是加上了e.printStackTrace() ;

最后的最后,在build.prop中加上(或者在自启脚本中使用setprop)

ro.zram.mark_idle_delay_mins = 60
ro.zram.first_wb_delay_mins = 1440
ro.zram.periodic_wb_delay_hours = 24

以上值来自pixel官方,其中,个人建议将first_wb_delay_mins改小,不然1440分钟就是24小时,60分钟标记一次,24小时后才进行写回,为了
保 护 闪 存 寿 命
也不至于这么久(


参考文章

除文中引用的
Android zram writeback
zram writeback 代码分析
常用android的smali注入代码
zram.txt翻译版


Special Thanks

@zobbz (提供了一份合并了odex,android10的services.jar)
@cramfs28 (提供官方writeback和storagemanagerservice的smali文件以及反编译上的一些技术支持)


本文部分来源于参考文章,其他均为原创

转载请注明