林莺啼到无声处,青草池塘独听蛙

12月 22

[译]Android开发的最佳实践

上个月因为工作及个人原因遗漏了一篇博客,于是这个月就多写一篇,相当于给自己一个提醒,再忙,也不能忘了博客这一事情。
这次的题目其实是在找资料时不小心看到了,写得挺好,为给自己备一份底,于是就转译过来。
照旧,翻译得不好,请轻拍。

原文地址:https://github.com/futurice/android-best-practices

正文开始

Android开发的最佳实践

学习的课程来自Futurice的Android开发者网站.根据这些指导方针,可以避免重复造轮子的情况。如果你对iOS或者Windows Phone的开发感兴趣,也可以去看一下我们的iOS良好实践及Windows客户端的良好实践文档。

摘要

  • 使用Gradle并且用它推荐的项目结构
  • 把密码及敏感的数据放在gradle.properties里
  • 不要自己写Http请求,使用Volley或者OkHttp库代替
  • 使用Jackson的库解析JSON数据
  • 避免烂番茄,并且使用65K的限制方法来使用少量的库
  • 使用Fragment代替UI界面
  • Activity仅用来管理Fragment
  • Layout的XML也是代码,组织好它们
  • 在Layout的XML文件中,使用样式来避免重复的属性
  • 使用多个样式文件来避免单位文件过大
  • 保持你的colors.xml文件的简短及干净,定义色盘(Palette)就好了
  • 同样保持dimens.xml文件干净,定义一般的常量
  • 不要把你的ViewGroups层级弄得很深
  • 避免客户端去处理WebViews,并且当心一下泄漏
  • 使用Robolectric来进行单元测试,Robotium来进行UI测试
  • 使用Genymotion做为你的模拟器
  • 一直用ProGuard或者DexGuard

Android SDK

把你的Android SDK放在你的主目录的某个地方,或者其他应用程序的独立位置。有些IDE安装的时候有包含了SDK,并且在该IDE的一些目录下。在你需要升级(重装)或者换IDE的时候,这是不好的。也要避免把SDK放在系统目录下,否则,你的IDE在你的角色不是ROOT用户下运行,有可能需要超级(sudo)权限。

构建系统

默认的选择肯定是Gradle。Ant有更大的限制并且也更冗长。用Gradle是简单的,如下:

  • 可构建不同的渠道或者让你的app多样化
  • 创建简单的任务脚本
  • 管理和下载依赖性(dependencies)
  • 自定义keystores
  • 更多

Google也在新编译的系统中用新标准积极地开发Android的Gradle插件。

项目结构

这里有两个常用的选项:旧的Ant跟Eclipse ADT的项目结构,新的Gradle跟Android Studio的项目结构。你可以选择新的项目结构。如果你的项目使用旧结构,可以考虑放弃并且迁移到新的结构上来。

旧结构

old-structure
├─ assets
├─ libs
├─ res
├─ src
│  └─ com/futurice/project
├─ AndroidManifest.xml
├─ build.gradle
├─ project.properties
└─ proguard-rules.pro

新结构

new-structure
├─ library-foobar
├─ app
│  ├─ libs
│  ├─ src
│  │  ├─ androidTest
│  │  │  └─ java
│  │  │     └─ com/futurice/project
│  │  └─ main
│  │     ├─ java
│  │     │  └─ com/futurice/project
│  │     ├─ res
│  │     └─ AndroidManifest.xml
│  ├─ build.gradle
│  └─ proguard-rules.pro
├─ build.gradle
└─ settings.gradle

主要的不同点在于,新结构有明确的分隔区分资源包(main, androidTest),这是来自Gradle的概念。你可以这样理解,添加一个"paid"跟"free"的资源包在src里,这样你的app就有了paid跟free两种风格了。

拥有一个顶级的应用程序从其他库项目区分你的应用程序是非常有用的(如library-foobar),那是引用自你的App。settings.gradle可以保持这些项目库的引用,app/build.gradle也能引用。

Gradle的配置

一般的结构。请看Google为Android写的Gradle指导

一个小任务。你可以在Gradle里创建一个任务来替代(shell, Python, Perl之类)的脚本。查看更多的内容,请看Gradle的文档

密码。在你App的build.gradle里,你在发布时要定义签名配置,以下的作法你应当要避免:

不要下面这样做,要不然这个会被显示在版本库里。

signingConfigs {
    release {
        storeFile file("myapp.keystore")
        storePassword "password123"
        keyAlias "thekey"
        keyPassword "password789"
    }
}

替代方案是,创建一个以下内容的gradle.properties文件,这个文件不会被加到版本库中:

KEYSTORE_PASSWORD=password123
KEY_PASSWORD=password789

这个文件会自动被gradle导进去,然后你可以在build.gradle里这样用:

signingConfigs {
    release {
        try {
            storeFile file("myapp.keystore")
            storePassword KEYSTORE_PASSWORD
            keyAlias "thekey"
            keyPassword KEY_PASSWORD
        }
        catch (ex) {
            throw new InvalidUserDataException("You should define KEYSTORE_PASSWORD and KEY_PASSWORD in gradle.properties.")
        }
    }
}

提取Maven从属关系是导入jar文件的替代方案。如果你的项目里明确包含了jar文件,他们将被冻结在特定的项目版本里,类似于2.1.1。下载jar文件并升级是很笨重的,这个问题Mavan的从属关系解决了,它也支持Android的Gradle构建。你可以指定版本的范围,比如2.1+,Mavan将自动升级到最接近的版本,可以看以下示例:

dependencies {
    compile 'com.netflix.rxjava:rxjava-core:0.19.+'
    compile 'com.netflix.rxjava:rxjava-android:0.19.+'
    compile 'com.fasterxml.jackson.core:jackson-databind:2.4.+'
    compile 'com.fasterxml.jackson.core:jackson-core:2.4.+'
    compile 'com.fasterxml.jackson.core:jackson-annotations:2.4.+'
    compile 'com.squareup.okhttp:okhttp:2.0.+'
    compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.+'
}

IDE跟文本编辑器

无论用哪个编辑器,它必须能玩好项目结构。编辑器是个人的选择,你得对你根据项目结构和构建系统的选择的功能编辑器负责。
有段时间推荐最多的IDE是Android Studio,因为它是Google开发,最接近Gradle的,在beta平台最终是新项目默认用Gradle,为Android开发者量身定做的。

如果你喜欢,你也能用Eclipse ADT,但你必须要配置它,直到旧项目结构和Ant能构建它。你甚至可以使用纯文本编辑器,如Vim,Sublime Text或者Emacs。如果这样的话,你需要用命令行来使用Gradle及adb。如果你的Eclipse集成环境的Gradle不能工作,你的还有一种选择就是使用命令行来构建,或者迁移到Android Studio上。

无论你用什么,确保最终正式发布时使用的是Gradle和你的新项目结构,并且避免把你的编辑器配置文件加到版本控制系统中。举个例子,避免添加Ant的build.xml文件。如果你在Ant中修改了构建配置,特别不要忘记保持build.gradle文件和机能是最新的。同时,对其他开发者很友好,不会强迫他们改变他们的偏好及工具。


-
Jackson是一个java的库,主要目的是把Object对象转成成JSON,反之也一样。解决这个问题的话Gson是一个很受欢迎的选择,不管怎么样,我们发现Jackson在它支持的交叉处理JSON时性能更好:流,内存中的树模型及传统的JSON-POJO数据的绑定。记住,虽然Jackson比Gson库更大,依赖你的项目,你可以用Gson来避免65K方法限制。其他的替代方案:Json-smartBoon JSON

网络,缓存及图像。是一个经过验证的后台请求执行方案,你需要在你的客户端考虑使用更好的方案,用Volley或者Retrofit。Volley有提供助手来加载及缓存图像。如果你选用Retrofit,考虑Picasso来载入及缓存图像,并且使用OkHttp来进行有效的Http请求。这Retrofit的三个,Picasso、OkHttp都是由同一家公司提供的,所以他们的互补是很好的。OkHttp也能用在Volley上

RxJava是一个响应式编程的库,也可以说是一个异步的事件机制。它是一个有前途且强壮的范例,也要相信它是那么的与众不同。我们建议在使用这个库构建你的整个应用的时候需要注意一些事情。有一些项目用RxJava创建完后,如果你需要帮助,需要跟下面这些人之一取得联系:Timo Tuominen, Olli Salonen, Andre Medeiros, Mark Voit, Antti Lammi, Vera Izrailit, Juha Ristolainen. 我们有写一些博客的文章在 [这里],[这里],[这里],[这里]

如果你之前没有Rx的经验,刚开始的时候你只需要接受来自API的响应。反之,可以从简单的UI事件处理开始,比如点击事件或者在搜索框中输入事件。如果你相信你的Rx技能并且想在整个构建中使用它,一定要在备注中写Javadocs。记住,有不理解RxJava的程序员,可能会导致维护项目的时候花掉很多时间。提供你最好的帮助来让他们理解你的代码及Rx。

Retrolambda是一个用Lambda表达语法的Java库,它可以用在Android及JDK8以前的平台上。如果你的功能样式例子是用RxJava写的话,它可以保持你代码的紧凑及高可读性。要用它,要安装JDK8,在Android Studio的项目结构对话框中,将它设置成你的SDK目录,并且在环境变量中设置JAVA8_HOME及JAVA7_HOME,然后进入到项目根目录下的build.gradle里:

dependencies {
    classpath 'me.tatarka:gradle-retrolambda:2.4.+'
}

然后在每个模块的build.gradle里,添加:


apply plugin: 'retrolambda'

android {
    compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

retrolambda {
    jdk System.getenv("JAVA8_HOME")
    oldJdk System.getenv("JAVA7_HOME")
    javaVersion JavaVersion.VERSION_1_7
}

Android Studio提供代码助手来支持Java8的lambdas。如果你是一个lambdas新手,使用下面的方法来开始:

  • 任何用一种方式创建的界面都是“友好的lambda”,并且可以折叠成更紧凑的语法。
  • 如果对参数及其他有疑问,写一个常规的另一个内建class,然后让Android Studio为你指定到lambda里。

当心dex方法的限制,并且避免使用很多库。当Android应用打包成dex文件的时候,有一个65536引用方法的硬性限制(这里这里这里)。如果你绕过这些限制,你在编译的时候会看到重大错误。基于这个原因,使用最小数量的库,并且使用“dex方法统计”的工具来决定在当前限制下,哪些库可以被保留使用。尤其避免使用Guava库,直到它能控制超过13k的方法。

Activities及Fragments

在Android当中,Fragment应当是你实现UI屏幕的默认选项。在你的应用里,Fragment是被构成可以复用的用户界面。我们推荐使用fragment代替activity来表现用户的屏幕界面,原因有以下几点:

  • 多面板结构的解决方案。fragment是最先在平板的屏幕上引进可扩展的电话应用,因此,你可以在平板的屏幕上同时显示A面板及B面板,或者你可以让A或者B占据整个屏幕。如果你的应用最开始的时候就是用fragment来实现的,你可以很方便让你的应用针对不同厂商进行适配。
  • 屏对屏通讯 Android的API不提供自身在多个Activity间发送太过复杂的数据(如一些Java对象)。而用fragment,你可以用Activity的实例当成通道,让它的子fragment之间进行通讯。甚至用这种方式,比在Activity与Activity之间通讯更好,你需要考虑一个事件桥的结构,用类似于Otto或者greenrobot的事件桥等更轻便的方法。RxJava也能用在事件桥的实现,这种情况,你可以避免增加其他的库。
  • fragment通常够用但又不仅仅是一个UI。你可以有一个没有UI的fragment,当成后台运行在Activity里。你甚至能更深入地创建一个fragment来控制fragment的变换,代替activity里已有的逻辑。
  • 甚至ActionBar可以用fragment来管理。你可以选择一个没有UI的fragment来管理ActionBar的单一目的,或者你可以选择一个当前可见的fragment,然后添加到父类Activity的ActionBar中已有的事件项里。点击这里查看更多

回过头说,我们建议不要大范围使用嵌套的fragment,因为会有“俄罗斯套娃”的问题出现。嵌套的fragment仅能用在说得过去(例如,fragment在一个水平滚动的ViewPager里让屏幕看起来像是fragment)或者它是必须被用的时候。

在架构层面,你的app有一个顶级activity控制着许多正式的fragment。你可以有一些支持类的activity,比如与主activity通讯的activity是简单的并且被Intent.setData()Intent.setAction()之类的限制。

Java包架构

Java架构对于Android应用来说,可以粗略地解释成Model-View-Controller。在Android中,fragment及activity事实上是控制器的类。另一方面,它们是用户界面直接的部分,因此也可以看成是视图。

基于这个原因,严格区分fragment或者activity是控制器或者视图是困难的。让它们呆在它们自己的fragment包中更好。根据前文的建议,Activity保持顶级包中就好了。如果你在计划多于2、3个activity的话,把这些activity打到一个包中。

其他方面,让架构看起来像常规的MVC,model的包通过用API请求的JSON解析器做成常用的POJOs,并且让view包包含你的自定义界面,通知,action bar视图,控件等。适配器是灰盒,存在数据与视图之间。无论怎么样,它们通常需要通过getView()导出一些视图,因此你可以在views的包中包含适配器的子包。

一些控制类在应用端或者接近Android系统。它们存在管理包中。杂项数据处理类,比如“DateUtils”放在utils包中。与后端有相互影响的请求放在network的包中。

所有这些,排序由靠近后端变成靠近用户:

com.futurice.project
├─ network
├─ models
├─ managers
├─ utils
├─ fragments
└─ views
   ├─ adapters
   ├─ actionbar
   ├─ widgets
   └─ notifications

资源

命名。依照类型的前缀惯例,类似于type_foo_bar.xml。例子:fragment_contact_details.xml,view_primary_button.xml,activity_main.xml

组织结构的XMLs。如果你不确定怎么格式化layout XML,下面的惯例可能有帮助。

  • 一行一个属性,补空4个空格
  • android:id作为第一个属性
  • android:layout_** 属性在前面
  • style属性在最后
  • 关闭标签 />单独在它的那一行,方便排序及添加属性。
  • 相对于硬编码android:text,考虑使用Android Studio提供的属性编辑器
<?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"
    >

    <TextView
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="@string/name"
        style="@style/FancyText"
        />

    <include layout="@layout/reusable_part" />

</LinearLayout>

根据经验法则,属性android:layout_ 必须被定义在layout XML中,其他的属性 android: 应当在style的XML中。这个规则已经有异常了,但通常还能正常工作。这个说法仅对layout文件中的外框(定位(positioning),偏移(margin),大小(sizing))及内容有效,其他显示的细节则在style的文件中。

异常有:

  • android:id必须明确的在layout文件中
  • layout文件中,对LinearLayout来说,android:orientation是有意义的
  • android:text要在layout的文件中,因为它是定义内容的
  • 有时候,通常样式定义android:layout_width及android:layout_height是有意义的,但是默认的情况,这些是显示在layout文件中的。

使用样式。一直以来,项目需要适当使用样式,因为对于视图来说,它重复显示是很常用的。在你的应用中,你至少有一个通用样式来显示大多数的文本内容。如下:

<style name="ContentText">
    <item name="android:textSize">@dimen/font_normal</item>
    <item name="android:textColor">@color/basic_black</item>
</style>

应用到TextView:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/price"
    style="@style/ContentText"
    />

你或许需要为按钮做同样的事情,不要停下。超出或者移动一个有关系的组,并且重复android:**属性到常规样式。
拆分大的样式文件到其他文件中。你不是必须只能有单一的style.xml文件。Android SDK支持目录外有其他的文件,关于命名样式没有神奇的地方,只要XML文件中有< style >的标记就行了。因此你可以有文件叫styles.xml,styles_home.xml,styles_item_detail.xml,styles_forms.xml。不喜欢构建系统中自带的资源目录的命名,在res/values目录下文件命名是随意的。

colors.xml是一个色盘。你的colors.xml什么也不是,它只是颜色RGBA值的映射。不要用它来为不同类型的按钮定义RGBA值。

不要这样做:

<resources>
    <color name="button_foreground">#FFFFFF</color>
    <color name="button_background">#2A91BD</color>
    <color name="comment_background_inactive">#5F5F5F</color>
    <color name="comment_background_active">#939393</color>
    <color name="comment_foreground">#FFFFFF</color>
    <color name="comment_foreground_important">#FF9D2F</color>
    ...
    <color name="comment_shadow">#323232</color>

你可以用这个格式很轻松重复RGBA值,而且在需要的时候修改基本颜色会变得很困难。同样,这些定义连带一些上下文环境,比如“按钮”或者“评论”,并且存在于按钮样式中,不在colors.xml中。

替代方式应当是这样的:

<resources>

    <!-- grayscale -->
    <color name="white"     >#FFFFFF</color>
    <color name="gray_light">#DBDBDB</color>
    <color name="gray"      >#939393</color>
    <color name="gray_dark" >#5F5F5F</color>
    <color name="black"     >#323232</color>

    <!-- basic colors -->
    <color name="green">#27D34D</color>
    <color name="blue">#2A91BD</color>
    <color name="orange">#FF9D2F</color>
    <color name="red">#FF432F</color>

</resources>

向应用的设计师要这些色值。命名不需要颜色的名称,如“绿色”,“蓝色”之类。命名类似“brand_primary”,“brand_secondary”,“brand_nagative”则很好的被接受。颜色的格式应当方便修改或颜色引用,并且要够明确知道有多少种不同的颜色被使用了。正常一个完美UI,减少颜色种类的使用是很重要的。

对待dimens.xml及colors.xml。你也可以定义一个标准的空色盘和字体大小,达到颜色相同的基本目的。dimens文件的一个良好例子:

<resources>

    <!-- font sizes -->
    <dimen name="font_larger">22sp</dimen>
    <dimen name="font_large">18sp</dimen>
    <dimen name="font_normal">15sp</dimen>
    <dimen name="font_small">12sp</dimen>

    <!-- typical spacing between two views -->
    <dimen name="spacing_huge">40dp</dimen>
    <dimen name="spacing_large">24dp</dimen>
    <dimen name="spacing_normal">14dp</dimen>
    <dimen name="spacing_small">10dp</dimen>
    <dimen name="spacing_tiny">4dp</dimen>

    <!-- typical sizes of views -->
    <dimen name="button_height_tall">60dp</dimen>
    <dimen name="button_height_normal">40dp</dimen>
    <dimen name="button_height_short">32dp</dimen>

</resources>

你可以在布局的时候使用spacing_**的尺寸来偏移(margin)及补空(padding),替代硬编码的值,更像是string的常规用法。这将有一个感观的一致性,让管理及改变样式、外观变得更简单。

避免视图的层级太深。有时候你会受到LinearLayout的诱惑,它可以实现视图的整理。以下的情况可能有问题:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <RelativeLayout
        ...
        >

        <LinearLayout
            ...
            >

            <LinearLayout
                ...
                >

                <LinearLayout
                    ...
                    >
                </LinearLayout>

            </LinearLayout>

        </LinearLayout>

    </RelativeLayout>

</LinearLayout>

如果你在Java里加载该视图到其他视图里会有问题,你不能看到layout文件的具体结构。

有一些问题会存在。你可能会碰到体验的性能问题,因为处理器需要处理复杂的UI树结构。另外还有严重问题会造成堆栈溢出(StackOverflowError)的问题。

因此,尽可能保持你的视图层级轻便:怎么使用RelativeLayout,怎么优化你的layout及使用标签

当心WebViews的关联问题。当你必须要显示Web页的时候,比如新闻的文章,避免让客户端处理清楚HTML,宁可后端程序调用“pure” HTML。WebViews在它的Activity引用时会造成内存泄漏,替代方案是绑定ApplicationContext。避免使用WebView来实现文本或者按钮,更好的方案是TextView或者Button。

测试框架

Android SDK的测试框架还没成熟,尤其是UI测试。你现在创建的Android Gradle是基于当前一个叫测试任务的connectedAndroidTest实现的,还是JUnit测试,使用的是为Android开发的JUnit的扩展。这个意味着你需要连接设备或者模拟器测试。这些是官方的测试手册。这里,还有这里

Robolectric仅用来做单元测试,不能用在视图上。这个测试框架可以接受“从设备断开”的测试来用加快开发速度,适用于基于模型及视图模型的单元测试。无论如何,关于UI测试,用Robolectric是错误及不完整的。测试UI元素有关的动画,对话框之类的会有问题,而且事实上在“暗地里走路(测试不通过屏幕控制)”是很困难的。

Robotium测试UI是容易的。你不需要Robotium连接测试设备进行UI测试,但是它可能对你有益的,因为它有很多的助手可以获取并查询视图,并且控制屏幕。测试任务看起来像是:

solo.sendKey(Solo.MENU);
solo.clickOnText("More"); // searches for the first occurence of "More" and clicks on it
solo.clickOnText("Preferences");
solo.clickOnText("Edit File Extensions");
Assert.assertTrue(solo.searchText("rtf"));

模拟器

如果你正在开发专业的Android应用,买一个授权的Genymotion模拟器吧。Genymotion模拟器运行起来比常规的AVD模拟器帧率更快。它还提供工具模拟你的App,模拟网络连接的质量,GPS定位等。它也是理想的测试工具。你可以测试许多(不是全部)不同的设备,因此跟你买很多真实设备比起来,花钱买Genymotion的授权事实上是要更便宜。

警告:Genymotion模拟器不支持诸如Google Play商店及Maps之类所有Google服务。你需要测试三星特定API的时候,需要买真实的三星设备。

ProGuard配置

ProGuard常常用在Android项目压缩和混淆打包代码中。

不论你是用ProGuard或者不依赖你项目的配置,在构建并发布apk的时候通常你需要配置gradle使用ProGuard。

buildTypes {
    debug {
        minifyEnabled false
    }
    release {
        signingConfig signingConfigs.release
        minifyEnabled true
        proguardFiles 'proguard-rules.pro'
    }
}

根据决定哪部分代码被加密,哪部分代码被放弃或者混淆,你可以在代码中指定一个或多个实体,这些被指定的实体包括特殊类与主方法、applets、midlets、activity等。Android框架从SDK_HOME/tools/proguard/proguard-android.txt中读取的默认配置文件当作默认配置。自定义项目特定的ProGuard规则,要定义在my-project/app/proguard-rules.pro中,这样会被添加到默认的配置文件中。

一个常见的问题是在关联ProGuard时看到应用在启动的时候闪退,并报ClassNotFoundException或者NoSuchFieldException之类,虽然构建命令(比如:assembleRelease)没有警告就成功,这无外乎两个情况:

  1. ProGuard移除了类、枚举、方法、域或注释,考虑不是必须的。
  2. ProGuard已经混淆(重名)了类,枚举或者域的名称,但是它是间接用了原始名称,也就是Java的引用。

检查app/build/outputs/proguard/release/usage.txt,看这个问题对象已被删除。检查app/build/outputs/proguard/release/mapping.txt来看问题对象已被混淆。

为了预防来自剥离方式需要类或者成员类的ProGuard,添加一个keep项到你的proguard配置中:

-keep class com.futurice.project.MyClass { *; }

预防来自混淆的类或者成员类的ProGuard,添加一个keepnames:

-keepnames class com.futurice.project.MyClass { *; }

看一下这个为其他例子写的ProGuard配置的模板。查看更多的ProGuard的例子。

提示。每次向你的用户发布版本的时候都要保存mapping.txt文件。包括每个发布版本的mapping.txt文件,这样可以确保在你的用户碰到问题时向你提交出错的堆栈时可以debug问题。

DexGuard。如果你需要核心工具来优化,特别混淆最终的代码,考虑用DexGuard,一个创建ProGuard的一些团队打造的商用工具。它也能很轻松的拆分Dex文件,从而解决65k方法限制。

感谢

Antti Lammi, Joni Karppinen, Peter Tackage, Timo Tuominen, Vera Izrailit, Vihtori Mäntylä, Mark Voit, Andre Medeiros, Paul Houghton and other Futurice developers for sharing their knowledge on Android development.

协议

Futurice Oy Creative Commons Attribution 4.0 International (CC BY 4.0)

标签:android development, 最佳实践, 开发

还不快抢沙发

添加新评论

最新文章

最近回复

  • Arturia.Αμανίτιδα:青蛙哈哈哈,发现你了
  • 凯哥自媒体:分享的不错,谢谢
  • 青蛙:养成习惯了就还好。
  • crossjs:还在坚持写博客,不错。
  • 青蛙:唯一一次金卡是公司工资卡时申请的,免管理费。后来离职了,也就把这...
  • 青蛙:谢谢,再见。
  • Levon:招商银行金卡被取消资格,每个月扣我10元管理费,没有任何短信通知...
  • sbsb:穷B真是可怜,天天折腾垃圾机唉,小公司的looser
  • gen:我现在电脑前就摆着两条充电线,Old and New...
  • 青蛙:你可真仔细。我是电子账单,上面没有你说的那些东西。
  • 分类

    归档

    广告