溺死在零梦里吧。

Someone famous in 归零幻想

用Jetpack Compose重构我的连连看游戏

发布时间:2021-09-21  修改时间:2021-09-21  作者:归零幻想

此前的文章中提到,我闲暇时写了个连连看游戏。因为比较闲,正好又对Jetpack Compose比较感兴趣,于是我想用Compose重构界面,学习下Compose的使用。 然后没有花费太大力气就完成了。

GameViewModelCompose.kt

于是我想,既然用上Compose了,是不是可以直接迁移到桌面平台?

这篇文章实际上没什么内容,就是对于折腾过程的一个简单记录,最后的成品在github,直接去看代码就可以了。

阅读全文 ▼

我的朋友们

发布时间:2021-03-12  修改时间:2021-08-18  作者:归零幻想

阅读全文 ▼

关于

发布时间:2020-07-25  修改时间:2021-08-08  作者:归零幻想

归零幻想

归零幻想

归零幻想是本人的网名啦,这里是我的博客小站。本人在其他地方帐号大多也叫这个名字。

本人2021年本科毕业于中国石油大学(华东),现在人在抖音。

博客系统至今我写过三个版本[^1],毕竟写出个CRUD就可以厚着脸皮说自己是个博客了。当前版本最初是个Kotlin练手项目,源代码发布在github。不过也就自用罢了,不会有人对我的代码感兴趣吧……

博客系统作为学生时代练手作品,问题多多,但段时间没有重构或更新的计划。毕竟人在火星,信号延迟高,更新博客都比较少了。

阅读全文 ▼

安卓连连看游戏设计

发布时间:2021-08-08  修改时间:2021-08-08  作者:归零幻想

字节取消大小周了,于是周末时间多起来了。虽然想着到处去玩,但现在疫情形势又不好了,于是安心呆在家里当一个肥宅。除了补一补番剧,就是把之前就想过的连连看游戏做出来了。

连连看游戏规则简单,点击两个相同的元素,如果他们能在两次拐弯以内连接起来,那么就可以消除。消除后就会出现空位,可以连接的就更多了。在规定时间内连续操作,直到消除所有元素。

虽然规则比较简单,但真正动手实现一遍还是很费工夫的。游戏既然做好了,那么我水一篇博客不过分吧:>

连连看游戏界面 1

项目的代码我放到了github。写的贼丑,轻喷。 https://github.com/zerofancy/match


  1. 本图片由笑果图床 提供支持。

阅读全文 ▼

java默认修饰符问题

发布时间:2021-08-05  修改时间:2021-08-06  作者:归零幻想

问题描述

和工具线配合完成某个需求,我这边的改动很少,但一鼓作气搞完后却遇到了奇怪的报错。已知工具线的代码大多是java的,而我这边自然是力推Kotlin。我们的代码参考如下:

代码参考

工具线定义了一个接口用于callback

package a;

interface IPublishCallback {
    void onFinish();
}

工具线在执行完发布逻辑后无论成功还是失败都会调用我们的callback

package a;

public class PublishUtil {
    public static void publishVideo(String videoName, IPublishCallback callback) {
        Runnable runnable = () -> {
            try {
                System.out.println("[" + videoName + "]开始执行耗时发布操作……");
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            callback.onFinish();
        };
        Thread thread = new Thread(runnable);
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们这边的实现是Kotlin的,就是调用了下工具线的方法

package b

import a.PublishUtil

class ShareFromSdkImpl {
    fun doShare() {
        println("系统分享功能测试")
        PublishUtil.publishVideo("测试视频") {
            println("发布视频完成回调")
        }
        println("完成系统分享方法")
    }
}

主函数

import b.ShareFromSdkImpl

fun main() {
    ShareFromSdkImpl().doShare()
}

问题

上述代码在执行后输出如下:

系统分享功能测试
Exception in thread "main" java.lang.NoClassDefFoundError: a/IPublishCallback
	at b.ShareFromSdkImpl.doShare(ShareFromSdkImpl.kt:8)
	at MainKt.main(main.kt:4)
	at MainKt.main(main.kt)
Caused by: java.lang.ClassNotFoundException: a.IPublishCallback
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
	... 3 more

Process finished with exit code 1

在安卓中略有不同,异常类型为

java.lang.IllegalAccessError: Interface a.IPublishCallback implemented by class com.ss.android.ugc.aweme.plugin.xground.player……

排查

注意报错中提到的a/IPublishCallback,是我们前面定义的回调接口。

我们将lambda改为匿名内部类的写法,这才发现确实是找不到。

2021-08-06_01-33.png

在使用Lambda形式实现回调时这个错误没有被编译器检查出来,运行时才报出来。

阅读全文 ▼

装饰模式实现分享功能

发布时间:2021-07-19  修改时间:2021-07-19  作者:归零幻想

在看业务代码时发现了一段代码,应用了装饰模式处理了分享功能的实现,非常巧妙,共赏。

装饰模式.svg.png.lin.png

阅读全文 ▼

ClassLoader双亲委托机制探究

发布时间:2021-04-11  修改时间:2021-04-11  作者:归零幻想

最近在研究抖音进入热点内流的耗时问题,种种线索指向了类加载耗时上。为此,我研究了Java类加载的双亲委托机制,并尝试给出了优化建议。

双亲委托机制

双亲委托机制中最重要的是loadClass方法,让我们看看它是怎么实现的。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name); // 已加载过直接返回
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false); //尝试让parent加载
                    } else {
                        c = findBootstrapClassOrNull(name); // bootstrap class loader是否加载过
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name); // 自己加载(找不到会抛出异常)
                }
            }
            return c;
    }

findClass就是根据类名去加载具体类的方法,则整个加载机制就很清楚了。

  1. 首先部分类会被BootstrapClassLoader加载,这部分是native实现。
  2. 会先尝试让parent加载
  3. parent找不到时才会自己加载

如果我们能创建一个ClassLoader,将它插入到PathClassLoader和BootstrapClassLoader之间,那么所有被PathClassLoader加载的类就都可以记录下来了。

阅读全文 ▼

ViewBinding、ViewModel和LiveData

发布时间:2021-02-22  修改时间:2021-02-22  作者:归零幻想

毕设项目没有历史包袱,我可以尽量向best practice努力。

ViewBinding

无数人痛恨findViewById,并且为了干掉它做了许多尝试,比如ButterKnife、kotlin-android-extensions。

现在,有了ViewBinding,项目中真的可以不写findViewById了。至少目前为止我的毕设项目还没有一个findViewById。

其实与ViewBinding相似的,还有一个DataBinding,但我不太喜欢,感觉在xml里面写代码不是一个好主意。

使用

首先在build.gradle(或build.gradle.kts)中的android块添加

buildFeatures {
    viewBinding = true
}

在xml中正常定义你的布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <Button
        android:id="@+id/test_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="toast" />

    <Button
        android:id="@+id/change_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="change" />
</LinearLayout>

接下来就可以愉快使用了

package top.ntutn.viewmodeldemo

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import top.ntutn.viewmodeldemo.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initView()
    }

    private fun initView() {
        binding.apply {
            changeButton.setOnClickListener { textView.text = "Changed!" }
            testButton.setOnClickListener {
                Toast.makeText(
                    [email protected],
                    "Test",
                    Toast.LENGTH_LONG
                ).show()
            }
        }
    }
}

我们在xml里面用下划线分隔的id在这里就直接变成了binding里面的字段,相当舒服。

在RecyclerView中的使用

在RecyclerView中代码要稍微发生一点变化,因为我们是在onCreateViewHolder时反射创建布局的。

package top.ntutn.viewmodeldemo

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import top.ntutn.viewmodeldemo.databinding.ItemTestBinding

class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
    class ViewHolder(val binding: ItemTestBinding) : RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            ItemTestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.binding.textView.text = TODO("Not yet implemented")
    }

    override fun getItemCount(): Int {
        TODO("Not yet implemented")
    }
}

仍然不需要findViewById!

阅读全文 ▼

DebugEntrance和DebugConfig

发布时间:2021-02-22  修改时间:2021-02-22  作者:归零幻想

工欲善其事,必先利其器。毕设是一个相对复杂的项目了,我觉得要想顺利完成肯定是需要一些手段帮助我调试的。于是这里我准备了debug页面,主要功能就两个:提供某个功能的入口以及存储配置(最好能直接在手机上修改)

DebugEntrance

就是一个各种测试功能的入口。

1.jpg

这个一看实现就很简单,不细说了。

DebugConfig

因为字节自己的ABManager用着挺顺手,感觉自己项目调试时有类似这么个东西会比较舒服,于是搞了这么个东西。

2.jpg 3.jpg

使用

先看使用:

@ZeroConfig(key = "retrofit_config", title = "Retrofit配置", owner = "liuhaixin.zero")
data class RetrofitConfig(val baseUrl: String = RetrofitUtil.BASE_URL)

private val retrofitConfig by zeroConfig<RetrofitConfig>()

private val retrofit by lazy {
    Retrofit.Builder()
        .baseUrl(retrofitConfig?.baseUrl ?: BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .client(okHttpClient)
        .build()
}

看上去还是有点让人心动的吧。

阅读全文 ▼

【译】用BuildSrc和Kotlin_DSL管理Gradle依赖

发布时间:2021-02-21  修改时间:2021-02-21  作者:Siva Ganesh Kantamani

多模块工程中一个更好的引入依赖的方法

翻译自Gradle Dependency Management With BuildSrc and Kotlin DSL

第一次尝试翻译英文文章……

要点

主要集中在如何用buildSrc目录和Kotlin DSL脚本构建一个Gradle依赖管理系统,你也会学到这样做相对使用传统Groovy代码的好处。

如果你倾向于通过视频来看这篇博客,文末附有一个Youtube视频。

问题

众所周知在一个快速发展的项目中维护依赖是一个乏味的工作,而传统的Groovy脚本没有code navigation、自动补全,再加上性能问题和运行时错误让这一切变得更糟糕。

更重要的是,多数安卓开发者不懂Groovy,甚至我也不知道我之前在用Groovy做啥。

感谢Gradle团队和社区的工作提供了一个顺畅安全的构建流程,他们提出的最棒的主意之一就是用Kotlin DSL脚本写buildSrc目录。

解决

依赖库引入和自定义task不应该放到构建脚本中,它们应该被声明到一个独立文件中再被构建脚本使用。在这个实现的早期,开发者习惯于创建一个Gradle文件来声明所有库并在构建脚本中使用。

这确实在一定程度上解决了问题,你可以在这篇文章读到这种方法。但这个简单方案不能解决类似自动补全和code navigation的问题,这使得在长远上看这个方案不够可靠。在这之外,buildSrc似乎有希望解决这个问题。

这个目录被当作一个included build看待。在发现这个目录之后,Gradle自动编译和测试它的代码,并将编译结果放到你的构建脚本的class path中。在一个多模块的工程中只能有一个这样的目录,并且要放到工程的顶级目录中。应该优先通过script plugins因为这样更便于管理、重构和测试代码。 ——Gradle团队

创建buildSrc目录

使用Kotlin DSL脚本不但能解决构建脚本中的这些问题,还能得到先进的IDE支持,包括code navigation、编译时错误提示等。最重要的,我们再也不用使用Groovy了。

阅读全文 ▼