• 我的2017年总结


    本篇文章已授权微信公众号 hongyangAndroid (鸿洋)独家发布

    2017就这么的过了,最近几天朋友圈里一直在晒18岁的梗,90后彻底退出青少年时代了,不服老不行啊,今天是17年最后一天,大家都去看晚会了,空巢老人还是写篇总结来记录下自己的2017吧。

    先回顾一下17年

    3月4月偷偷离校满怀憧憬的跑去公司实习;
    5月6月回校做毕设、写论文,享受最后的校园时光,同时喜欢上了每天跑3公里,因为有着一个腹肌梦;
    6月底找了个基友,啥准备也没有就来了趟毕业旅行,跑到了人生中离开家最远的一次,爬了山,看了水,满足;
    7月作为职场小菜鸟步入公司,开始打怪升级,同时找了几个同学一起合租,开始学做菜,每天一下班就想着赶回去练手做菜;
    8月用第一次工资给老爸、老妈换了部手机,当然,找老姐资助了点,但功劳都是我的,哈哈哈;
    9月用剩余的工资给自己买了很多健身器材,开始了自己的健身梦;
    10月发现自己还是职场小菜鸟,经验条太长升级太慢,决定做做支线任务,每月至少写篇技术博客;
    10月底写了第一篇源码分析博客,投给了郭神,过了!成就感瞬间充满,开心,又更有动力了;
    11月发现小腹肌有了雏形,开心,工作也开始适应了,虽然还是小菜鸟,但多少可以为公司做点贡献了;
    11月12月事情开始多了,做菜的事也放一边了,锻炼也放一边了,给自己找了个借口:天冷;
    12月底想要总结一下,发现这一年来,喜欢的事很多,尝试的事也很多,但更多的都是没能坚持下去;
    18年给自己说了句话:锻炼的事得重新拿起来,不能放,博客的事也不能放,得坚持;

    笔记整理

    我平时是会通过 word 来记录一些学习笔记:
    笔记目录.png

    这一年零零散散记了有150几页,记的内容很泛,平时遇见一些不熟悉的知识点都会顺手记下来,方便后面查阅。最可惜的是,我这个习惯其实是从大三开始的,大三记了一年,记的内容好像比今年多多了,后面因为电脑坏了重装系统,居然忘记备份,就这么没了,没了!

    其实笔记记多了,反而会懒得去看,时间一久,如果没再次整理一下的话,笔记反而会更乱,当需要时也会忘记自己是否曾经记过某个知识点。所以,感觉还是得来梳理一下这个笔记,好的,那就开始吧。

    归纳了一下,笔记的主要内容大概可以分为以下几类吧:基础篇进阶篇工具篇,其他还包括一些热门框架的基本使用记录或一些编程思想,但内容不多。

    那么,这篇就大概把这些笔记内容分类梳理一下吧,方便自己以后查阅,也希望可以帮助到别人。

    工具篇

    1. abd 常用命令

    (1)删除系统应用

    步骤:

    1. mount -o rw,remount /system 卸载系统应用时先运行这句
    2. chome 777 system 添加 system 目录权限
    3. 然后把 /system/app/data/data 下的相关文件删掉
    4. reboot 重启盒子
    5. 然后就可以安装 debug 应用

    场景:
    如果开发盒子的系统应用时,当通过 AS 编译运行到盒子时,如果盒子上已装有 release 版,那么 AS 是无法将项目跑到盒子上的,需要先将系统应用删除后才可以正常开发。

    (2)修改host文件

    当 pc 跟盒子连接后(通过 usb 或 wifi 连接),在 pc 端的 cmd 窗口中输入下列命令:
    adb remount
    adb pull /system/etc/hosts //备份
    adb push hosts /system/etc/hosts //将准备好的 hosts 文件 push 进去

    (3)wifi 连接调试

    方法一:需要 root 权限
    手机端下载超级终端,然后输入下面命令:
    su
    setprop service.adb.tcp.port 5555
    stop adbd
    start adbd

    附 Android 终端下载地址:https://github.com/jackpal/Android-Terminal-Emulator

    方法二:需要 usb 线,不需要 root
    usb 连接手机调试后,在 pc 端的 cmd 窗口中输入下列命令:
    adb kill-server
    adb start-server
    adb tcpip 5555

    场景:盒子应用开发时,经常会出现有的盒子 usb 无法跟 pc 连接,那么此时可以考虑通过 wifi,当 pc 和 盒子在同一个局域网内时可以通过 adb connect <ip address> 来连接盒子,当连接失败时才考虑用上面的方法操作后再次调用 adb connect <ip address> 来连接盒子即可。

    (4)查看目前 Activity 栈

    命令: adb shell dumpsys activity

    (5)启动任意 Activity

    启动任意活动.png
    这位大神的博客有详细说明:http://www.jianshu.com/p/54fd9627860a

    2. Chrome 插件

    我用的插件不多,但我感觉是都挺实用的,如下:

    Octotree: 方便 github 项目结构查看

    Postman: 模拟 http 请求

    ChromeADB: 发送 adb 命令,没有遥控器的时候可以暂时用这个来控制

    JSON-handle: json

    AdBlock: 禁广告

    Agar/Slither Infinity: 标签页

    crxMouse Chrome™ Gestures: 手势操作

    3. AS调试工具

    查看当前线程信息
    as调试查看线程信息.png

    开启显示方法的返回值
    as调试开启方法返回值.png

    不添加代码临时添加日志输出
    as调试添加日志输出.png
    as调试日志输出.png

    4. AS版本管理工具(SVN and Git)

    这部分内容写过两篇简单的博客,记录了下基本使用:
    Android Studio的git功能的使用介绍
    如何用Android Studio同时使用SVN和Git管理项目

    基础篇(Android)

    ps:以下内容有些是平时记笔记时直接在一些博客里将自己认为的重点直接复制粘贴记录下来的,当时都没有记出处,所以如果这样会有抄袭的侵权,告知下来删。

    1. xml控件属性

    (1)android:clipToPadding android:clipToChildren

    这两个都是 ViewGroup 的属性,一般我们在 ViewGroup 里,比如在 LinearLayout 设置 padding = 10dp,那么它的子控件就都会在距离父控件边界 10dp 的内部区域显示。即使我们对子控件设置了 layout_marginTop = -10dp,来将子控件往上移到父控件的上边界,虽然子控件实际位置确实是往上移了,但是在这个 10dp 的区域内是不会绘制的,也就是说子控件上面 10dp 部分是被遮住了,不会显示出来的。而 android:clipToPadding 这个属性作用就是允许绘制在 padding 内子控件,这个属性默认值是 true,当我们把它设置成 false 后,子控件在父控件的 padding 区域内就可以显示出来了。

    adnroid:clipToClildren 性质是一样的,默认值也是 true,只是这个属性是允许绘制超出父控件区域的子控件。正常情况下,如果我们对子控件设置 layout_marginTop 为负来将子控件的一部分区域移出父控件的边界,那么子控件超出父控件边界的这部分是不会被绘制出来的,如果对这个属性设置了 false,那么就允许绘制超出的这部分内容了。

    这两个属性一般是在 Tv 应用上比较常用,因为 Tv 应用经常会有一些 View 获取焦点后需要放大的效果,而有时放大后的 View 刚好会在父控件的 padding 区域内,甚至是会超出父控件的边界,如果不用这两个属性来控制,放大后的 View 就会呈现被截断的效果。

    (2)android:drawableRight

    这个属性是 TextView 的属性,Tv 应用里有很多既有小图标又有文字的这种小标题 View,虽然可以用 LinearLayout 内放一个 ImageView 和一个 TextView 来搞定,但也可以直接用一个 TextView 就搞定,就是通过这个属性,这样可以优化一层布局。

    android:drawablePadding: 是用来设置 drawable 与 text 之间的距离的。

    但使用这个有一些注意事项需要关注一下:

    注意事项:

    1. TextView 的 padding 作用在 drawable 之外
    2. TextView 的高度或宽度为 wrap_content 时将是文字和 drawable 中较大的那一个,再加上 padding 和 margin
    3. gravity只对文字起作用,对 drawable 不起作用
    4. drawable 会在其所在的维度居中显示,比如 drawableLeft 是上下垂直居中的,以此类推

    (3)xml焦点控制

    android:descendantFocusability-----父容器和子控件的焦点获取问题

    这个属性定义了当一个焦点要传递给父容器或者子控件时,父容器和子控件之间获得焦点的关系。具体值如下:
    beforeDescendants:父容器会比其子控件率先获得焦点。
    afterDescendants:如果没有任何子控件要获得焦点的话,那么父容器才会获得焦点。
    blocksDescendants:父容器会阻止其子控件获得焦点(也就是说焦点会由父容器获得)。

    android:duplicateParentState

    这个属性指的是当前控件是否跟随父控件的(点击、焦点等)状态。
    我一般是结合上面的 blocksDescendants 和这个属性一起用,达到防止子控件获取焦点但同时子控件又能响应父控件焦点的变化状态。

    android:nextFocusRight------控制下一个焦点

    (4)xml 动画文件里值的含义

    xml动画里值的含义1.png

    xml动画里值的含义2.png

    三种形式
    绝对坐标:数字
    相对于View本身控件坐标:数字+%
    相对于父控件坐标:数字+%p

    2.单元测试中通过反射测试私有方法

    代码示例.png

    3.OnGlobalLayoutListener

    这个回调可以用于获取 view 宽高:

    private int mViewHeight;
    private View mView;
    ...
    
    //注册监听
    mView.getViewTreeObserver().addOnGlobalLayoutListener(
        new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //获取View高度
                mViewHeight = mView.getHeight();
                //取消监听,否则该方法会不断回调
                mView.getViewTreeObserver().removeGlobalLayoutListener(this);
            }
        }
    )
    

    我们知道在 onCreate() 中 View.getWidth 和 View.getHeight 无法获得一个 view 的高度和宽度,这是因为 View 组件布局要在 onResume() 回调后完成。所以现在需要使用 getViewTreeObserver().addOnGlobalLayoutListener() 来获得宽度或者高度。这是获得一个 view 的宽度和高度的方法之一。

    但是需要注意的是 OnGlobalLayoutListener 可能会被多次触发,因此在得到了高度之后,要将OnGlobalLayoutListener 注销掉。

    4.TextView设置不同字体样式

    效果图.png

    使用方法:

    //step1:
    SpannableString text = new SpannableString(“输入课程名称,如管理”);  
    
    //step2:
    text.setSpan(new ForegroundColorSpan(ContextCompat.getColor(this, R.color.text_color_gray)), 0, 10,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);  
    
    //step3:
    text.setSpan(new AbsoluteSizeSpan(textSize1), 10, 14, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    
    //step4:
    4.TextView.setText(text);  
    

    关键方法:setSpan()

    支持设置的字体格式(继承 CharacterSty 均可):

    1. 字体颜色-------ForegroundColorSpan
    2. 字体大小-------AbsoluteSizeSpan
    3. 背景颜色-------BackgroundColorSpan
    4. 超链接----------URLSpan
    5. 粗体、斜体----StyleSpan
    6. 删除线----------StrikethroughSpan
    7. 下划线----------UnderlineSpan
    8. 图片-------------ImageSpan

    5.merge 标签注意事项

    1. merge 必须放在布局文件的根节点上。
    2. merge 并不是一个 ViewGroup,也不是一个 View,它相当于声明了一些视图,等待被添加。
    3. merge 标签被添加到 A 容器下,那么 merge 下的所有视图将被添加到 A 容器下。
    4. 因为 merge 标签并不是 View,所以在通过 LayoutInflate.inflate 方法渲染的时候, 第二个参数必须指定一个父容器,且第三个参数必须为 true,也就是必须为 merge 下的视图指定一个父亲节点。
    5. 如果 Activity 的布局文件根节点是 FrameLayout,可以替换为 merge 标签,这样,执行 setContentView之后,会减少一层 FrameLayout 节点。
    6. 自定义 View 如果继承 LinearLayout,建议让自定义 View 的布局文件根节点设置成 merge,这样能少一层结点。
    7. 因为 merge 不是 View,所以对 merge 标签设置的所有属性都是无效的

    6.调用系统的安装应用界面

    步骤:

    1. 设置 intent 的 dataAndType ,用隐式调用法来启动系统的安装界面
    2. 传入apk的uri路径:file://mnt/sdcard/aa.apk
    3. 和指定的type:application/vnd.android.package-archive

    示例代码:

    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    //传入apk路径还有设置type
    intent.setDataAndType(Uri.fromFile(new File(Config.getAppDir(content), apkName)), "application/vnd.android.package-archive")  
    

    7.坐标基础

    View 的坐标系统是相对于父控件而言的:

    getTop(); //获取子View左上角距父View顶部的距离
    getLeft(); //获取子View左上角距父View左侧的距离
    getBottom(); //获取子View右下角距父View顶部的距离
    getRight(); //获取子View右下角距父View左侧的距离

    坐标系.png

    MotionEvent 中 get 和 getRaw 的区别:

    event.getX(); //触摸点相对于其所在组件坐标系的坐标
    event.getY();
    event.getRawX(); //触摸点相对于屏幕默认坐标系的坐标
    event.getRawY();

    坐标系.png

    8.SharePreference监听

    registerOnSharedPreferenceChangeListener(listener)

    这个方法才是我要说的重点,因为之前有些需求就是更改了 SharedPreferences 之后,要通知相应的组件做出改变,我以前的处理方式是通过事件订阅实现的,发一个 event 出去,然后目标收到 event 再做出反应,当时觉得特别蛋疼,两边都要做些操作,显的特别啰嗦,当时就在想可不可以在 SharedPreferences 上设置一个观察者,一旦有什么风吹草动,就自动通知目标,不曾想,人家早已经实现了,只是我愚昧无知,今天去看了下源码发现了这个方法,相见恨晚。

    代码示例:

    SharedPreferences sp1 = getSharedPreferences(getPackageName() + "test", MODE_PRIVATE);
    sp1.registerOnSharedPreferenceChangeListener(
        new SharedPreferences.OnSharedPreferenceChangeListener() { 
            @Override 
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
                 // do any thing you want 
            }
        });
    
    

    9.TextView各种padding解析、长度测量

    详细说明:TextView的文字长度测量及各种padding解析

    TextView.png

    这部分内容专门写了一篇博客分析,上面这张图概括了链接博客主要分析的内容,感兴趣的可以点进去看看。

    进阶篇(Android)

    进阶篇主要是自己在这一年里接触的一些源码阅读的笔记,现在已经陆陆续续发表了几篇源码分析的博客了,还有很多都是先简单的记录下来,想等有空闲时间再慢慢组织语言写成博客出来。

    1.View.post()原理

    原文跳转:【Andorid源码解析】View.post() 到底干了啥

    Q1: 为什么 View.post() 的操作是可以对 UI 进行操作的呢,即使是在子线程中调用 View.post()?

    Q2:网上都说 View.post() 中的操作执行时,View 的宽高已经计算完毕,所以经常看见在 Activity 的 onCreate() 里调用 View.post() 来解决获取 View 宽高为0的问题,为什么可以这样做呢?

    Q3:用 View.postDelay() 有可能导致内存泄漏么?

    这是最近发表的一篇关于 View.post() 到底干了些什么事的博客,内容主要是从源码上分析了上述三个问题,投给了郭神了,也过啦,感谢郭神。虽然深度不是特别深,但内容应该还是有些干货的,尤其是那些想开始阅读源码来学习的同学,这篇里有介绍一些如何阅读源码的思路还有工具,因为我也是个新人,也刚刚摸索,也碰到很多问题,所以我清楚作为菜鸟对于阅读源码恐惧的种种,希望可以帮助到大伙。

    2.KeyEvent的点击事件分发机制

    原文跳转:Android KeyEvent 点击事件分发处理流程(一)

    KeyEvent流程图

    手机上的应用多数的触屏点击事件 MotionEvent,而 Tv 上的应用基本都是遥控器的点击事件 KeyEvent,两者虽然都是点击事件,分发消费机制大体上也差不了多少,但还是有些地方是有区别的。

    上面那篇博客里我主要是分析了在一个 Activity 界面里的遥控器点击事件 KeyEvent 的分发传递流程,但仍然还有很多遗留问题尚未搞清楚。

    3.RecyclerView回收复用机制

    原文跳转:基于滑动场景解析RecyclerView的回收复用机制原理

    RecyclerView 的源码实在是太复杂了,之前项目有个关于滑动的问题,为了定位也去看了RecyclerView的相关源码,但最终还是失败了,因为还是没搞清楚。后来又遇到回收复用的优化需求,所以又过了这部分的源码,硬着头上了,分析到最后稍微有些收获,所以记录下来分享给大伙。

    Q1:如果向下滑动,新一行的5个卡位的显示会去复用缓存的 ViewHolder,第一行的5个卡位会移出屏幕被回收,那么在这个过程中,是先进行复用再回收?还是先回收再复用?还是边回收边复用?也就是说,新一行的5个卡位复用的 ViewHolder 有可能是第一行被回收的5个卡位吗?

    Q2:在这个过程中,为什么当 RecyclerView 再次向上滑动重新显示第一行的5个卡位时,只有后面3个卡位触发了 onBindViewHolder() 方法,重新绑定数据呢?明明5个卡位都是复用的。

    Q3:接下去不管是向上滑动还是向下滑动,滑动几次,都不会再有 onCreateViewHolder() 的日志了,也就是说 RecyclerView 总共创建了17个 ViewHolder,但有时一行的5个卡位只有3个卡位需要重新绑定数据,有时却又5个卡位都需要重新绑定数据,这是为什么呢?

    链接给的博客里,我主要是基于滑动的场景,从源码上分析了上述三个问题,问题有结合一些前提和日志,所以如果感觉问题就看的有点懵的可以点进去看看具体的说明。

    4.Activity切场动画

    原文跳转:Activity 切换动画---点击哪里从哪放大

    这篇介绍的是如何实现 Activity 的切换动画,只是记录了下实现这个功能的一个思路,以及这过程中碰到的一大堆奇葩问题,尤其是 android:windowIsTranslucent 属性的问题。这段时间的主要经历都放在的动画的原理上,所以后期会渐渐分享这部分的知识。

    5.app Launch源码阅读前基础

    startActivity() 打开一个页面时做的事是非常非常多的,这个过程是复杂得要死的,但大体的流程静下心来应该还是可以梳理出来的,但在这之前,需要对一些类或者变量先了解一下,这样阅读源码才不会一脸懵逼,下面的内容不是我整理的,原博客地址会贴出:

    Android 4.4(KitKat)窗口管理子系统 - 体系框架

    Activity:描述一个Activity,它是与用户交互的基本单元。
    ActivityThread:每一个App进程有一个主线程,它由ActivityThread描述。它负责这个App进程中各个Activity的调度和执行,以及响应AMS的操作请求等。
    ApplicationThread:AMS和Activity通过它进行通信。对于AMS而言,ApplicationThread代表了App的主线程。简而言之,它是AMS与ActivityThread进行交互的接口。注意ActivityThread和ApplicationThread之间的关系并不像Activity与Application。后者的关系是Application中包含了多个Activity,而前者ActivityThread和ApplicationThread是同一个东西的两种"View",ApplicationThread是在AMS眼中的ActivityThread。
    ViewRootImpl:主要责任包括创建Surface,和WMS的交互和App端的UI布局和渲染。同时负责把一些事件发往Activity以便Activity可以截获事件。每一个添加到WMS中的窗口对应一个ViewRootImpl,通过WindowManagerGlobal向WMS添加窗口时创建。大多数情况下,它管理Activity顶层视图DecorView。总得来说,它相当于MVC模型中的Controller。
    ViewRootImpl::W:用于向WMS提供接口,让WMS控制App端的窗口。它可看作是个代理,很多时候会调用ViewRootImpl中的功能。这种内嵌类的用法很多,特别是这种提供接口的代理类,如PhoneWindow::DecorView等。
    Instrumentation:官方提供的Hook,主要用于测试。如果只关注窗口管理流程的话可以先无视。
    WindowManagerImpl:Activity中与窗口管理系统通信的代理类,实现类是WindowManagerGlobal。WindowManagerGlobal是App中全局的窗口管理模块,因此是个Singleton。
    Window:每个App虽然都可以做得各不相同,但是作为有大量用户交互的系统,窗口之间必须要有统一的交互模式,这样才能减小用户的学习成本。这些共性比如title, action bar的显示和通用按键的处理等等。Window类就抽象了这些共性。另外,它定义了一组Callback,Activity通过实现这些Callback被调用来处理事件。注意要和在WMS中的窗口区分开来,WMS中的窗口更像是App端的View。
    PhoneWindow:PhoneWindow是Window类的唯一实现,至少目前是。这样的设计下如果要加其它平台的Window类型更加方便。每个Activity会有一个PhoneWindow,在attach到ActivityThread时创建,保存在mWindow成员中。
    Context:运行上下文,Activity和Service本质上都是一个Context,Context包含了它们作为运行实体的共性,如启动Activity,绑定Service,处理Broadcast和Receiver等等。注意Application也会有Context。Activity的Context是对应Activity的,Activity被杀掉(比如转屏后)后就变了。所以要注意如果有生命周期很长的对象有对Activity的Context的引用的话,转屏、返回这种会引起Activity销毁的操作都会引起内存泄露。而Application的Context生命周期是和App进程一致的。

    6 消息机制MessageQueue原理

    这部分内容想等抽个时间整理篇博客出来,下面只是我阅读源码过程中的一些笔记,还没组织语言,大体上记录了 MessageQueue 往队列里插消息和取消息的流程分析:

    插入消息 enqueueMessage()
    enqueueMessage.png

    enqueueMessage.png

    也就是说,我们自己通过Handler发送的Message的Target是绝对不会为空的,因为Handler内部会自动将Message的Target设置为Handler自己。然后消息队列其实是根据when来排队的。

    读取消息 next()
    next.png

    读操作要么返回该执行的message,要么就进入阻塞。当返回Null时表示当前Lopper已被关闭,如果是主线程,那么就意味着程序退出了。

    next.png

    next.png

    读取操作本身会伴随着删除操作,读操作会有阻塞,原理好像是通过Linux的epoll机制,但还不懂。按道理next()是会按照消息队列已排好序的取下一个Message,但如果碰到有同步屏障Message时,则后面所有同步的消息都不会取,只会取异步的消息,直到该同步屏障被移除。

    7.startActivityForResult()失效问题

    如果项目里的 Activity 有 singleTask 模式的话,而且又用到这个方法的话,那也许有可能你就碰到了这个问题,下面这篇博客里把这个问题讲解得很清楚了:
    我打赌你一定没搞明白的Activity启动模式

    8.ThreadLocal 原理

    我们知道,在 new 一个 Handler 的时候,Handler 会和当前的线程的消息队列绑定,而每个线程都可以有自己单独的消息队列,互不影响,那么它的原理其实是通过 ThreadLocal 实现的,关于这部分内容后期也想单独抽一篇博客来记录下,下面的内容也是简单的记录下 set() 和 get() 方法:

    get.png

    getMap.png

    threadLocal.png

    所以,get() 方法会先获取线程的一个成员变量 threadLocals,这个变量类型是 ThreadLocal的内部类 ThreadLocalMap 对象,内部有一个 table 数组用于存放数据。所以即使是在不同的线程中使用同一个ThreadLocal对象的get() 方法来获取数据,内部其实只会到当前线程的数组里去取相应数据,所以才能做到线程之间互不影响的存取数据,不是当前线程自然拿不到当前线程的成员变量threadLocals,也就拿不到数据了。

    set.png

    set() 方法同理。

    当然,ThreadLocal Map 内部 get(),和 set() 方法并不简单,set() 方法存放每个 value 时,都会将当前的 ThreadLocal 实例作为 key 值来绑定value,也就是说如果使用不同的 ThreadLocal 对同一个线程中做get(),set() 操作的话,虽然操作的是同一个 table 数组,但即使get(),set()的key值一致,实际上获取的数据是相互独立的。


    虽然还有其他一些七七八八的小笔记,但这里就不列出来了,这次梳理的目的一是重新过一遍这一年来所做的笔记,权当复习;二是将这些零散的笔记稍微归类整理一下,毕竟新的一年也还是会记很多笔记,省得到时更乱;三是分享在网上,方便自己后面查阅也希望可以帮助有需要的大伙。

    新的一年来了,大伙一起加油吧~

  • 相关阅读:
    HDU 1698-Just a Hook
    HDU 1394 Minimum Inversion Number(线段树)
    HDU 4253-Two Famous Companies(二分+最小生成树)
    POJ 3279
    POJ 2251 Dungeon Master
    POJ1321 棋盘问题
    Charlie's Change POJ
    Coins —— POJ-1742
    sublime text主要快捷键列表
    根据电脑分辨率调整网站的布局
  • 原文地址:https://www.cnblogs.com/dasusu/p/8166384.html
Copyright © 2020-2023  润新知