标签归档:Android

翻译:Andriod注解支持(Annotations)

Android 支持注解

这篇博客,我们将要说说Android Support Annotations 库,以及我们需要关注它。

support library 19.1版引入一个新的注解包,这个注解包里面有一系列有用的注解,使用它能很方便地捕获bugs。这些注解将会帮助 Android studio 来检测你代码里可能的错误并报告给你。在版本22.2中,这些注解进一步丰富了,新增了13个注解。

添加库到你的工程里

默认情况下,你的项目里没有注解库,因为它是一个独立的库。如果你使用appcompat库,则你可以使用这些注解,因为appcompat库依赖于它。

不管哪种情况, 为了在项目中使用注解, 添加以下代码到你的build.gradle文件中。

compile 'com.android.support:support-annotations:<latest-library-version>'

这些注解基于它们的使用和功能的相似性组合在一起,并根据这些分组,我们有以下注解类:

  • NULL的含量注解
  • 资源注释
  • 线程注释
  • 值约束注解
  • 其他是:权限注解,CheckResults注解和CallSuper注解。

Nullness 注解

@Nullable and @NonNull annotations 用于检查变量、参数以及方法的返回值是否为null。 @NonNull annotation 意味着变量、参数以及方法的返回值不能是null. 如果违法了注解规则,Android studio产生一条警告信息。例如:

//pass a null argument to a method annotated as @NonNull
doubleNumber(null);

public int doubleNumber(@NonNull int num) {
    return num * 2;
}

@Nullable 意味着变量的值可以是null, 如果注解用于方法,则指明方法的返回值可以是null。 不论何时使用@Nullable注解, 都应该在变量和方法返回值上做Null检查。

资源注解

因为在Android中,资源是做为一个整数在传递的, 一段期望字符串id的代码,可以传递一个指向drawable的整形值,这种情况下编译器居然是可以接受的。资源类型注解允许在这种情况下检查资源的类型。譬如添加一个@StringRes的注解, 能确保传递的是字符串资源,如果是其它资源,IDE就会标志。

public void setButtonText(@StringRes int id) {
     //set text on some button
}

//this will be flagged by the IDE
setButtonText(R.drawable.icon);

每个android资源类型都有相应的资源类型注解。譬如有 @DrawableRes, @ColorRes, @InterpolatorRes等等. 总得原则是,如果资源类型是“Foo”,那么相应的注解就是“FooRes”.

最后, 有一个特别的资源注解,它是@AnyRes表明是个资源,但不明确到底是什么资源类型.

线程注解

线程注解检查一个方法是否在特定的线程类型中被调用。支持的线程注解有:

  • @UiThread
  • @MainThread
  • @WorkerThread
  • @BinderThread
    @MainThread@UiThread 注解是可以互换的。

当类中的方法全都在同样类型的线程中调用时,可以把注解直接加到类上。一个演示线程注解使用的好例子是在AsyncTask中

@WorkerThread
protected abstract Result doInBackground(Params... params);

@MainThread
protected void onProgressUpdate(Progress... values) {
}

如果onProgressUpdate方法不是在主线程中调用,IDE就会标志一个错误出来。

值限制注解

@IntRange, @FloatRange and @Size annotations 用于验证参数的有效性。 @IntRange annotation 验证参数的值是在指定的整数范围内. 下面这个例子中,setAlpha方法会检查alpha的值是从0到255。

public void setAlpha(@IntRange(from=0, to=255) int alpha) {
    //set alpha
}

@FloatRange 类似,验证参数是在指定的浮点数范围内. @Size annotation 有点不一样, 它用于检查集合或数组的大小, 也用于检查字符串长度. @Size(min=1) annotation 用于检查数组非空, @Size(2) annotation 检查数组刚好有两个元素。

CheckResult Annotations

这个用于确保方法的返回值确实有使用。主要目的是帮助在使用一些API的时候,去使用它的返回值,而不是理所当然地认为函数有边际效应。在官方文档中有解释,一个好的例子是字符串。
trim 方法, 会让Java开发新手误用,认为方法调用完,字符串的值已经去掉了空白符。trim 方法使用@CheckResult注解。当调用者使用了trim方法,又没有使用返回值时,IDE就会标志。

@CheckResult
public String trim(@NonNull String string) {
    //remove whitespace from string
}

String s = "hello world ";

//this will make the IDE flag an error since the result from the @CheckResult
//annotated method is not used
s.trim();

其他值得关注的注解还有 @CallSuper, @Keep and @RequiresPermission. 完整的列表请参考官方的文档。

写在最后

使用注解支持库可以帮助我们更好地理解代码意图。它使用的代码结果是可预测的,同时方便其它开发人员整合你的代码,也方便你将来再次修改它。

更多关于Android的文章可以参考演道网

参考

Improve code Inspection with Annotations – Android developer doc

Support Annotation documentation

http://michaelevans.org/blog/2015/07/14/improving-your-code-with-android-support-annotations/

原文:http://mayojava.github.io/android/android-support-annotations/?utm_source=Android+Weekly&utm_campaign=c0a2159802-Android_Weekly_222&utm_medium=email&utm_term=0_4eb677ad19-c0a2159802-337909737

下一代Android打包工具:packer-ng-plugin

下一代Android渠道打包工具

最新版本

  • v1.0.2 – 2015.12.04 – 兼容productFlavors,完善异常处理
  • v1.0.1 – 2015.12.01 – 如果没有读取到渠道,默认返回空字符串
  • v1.0.0 – 2015.11.30 – 增加Java和Python打包脚本,增加文档
  • v0.9.9 – 2015.11.26 – 测试版发布,支持全新的极速打包方式

项目介绍

packer-ng-plugin 是下一代Android渠道打包工具Gradle插件,支持极速打包,1000个渠道包只需要5秒钟,速度是 gradle-packer-plugin 的1000倍以上,可方便的用于CI系统集成,支持自定义输出目录和最终APK文件名,依赖包: com.mcxiaoke.gradle:packer-ng:1.0.+ 简短名:packer,可以在项目的 build.gradle 中指定使用,还提供了命令行独立使用的Java和Python脚本。实现原理见本文末尾。

使用指南

Maven Central

修改项目根目录的 build.gradle

buildscript {
……
dependencies{
// add packer-ng
classpath ‘com.mcxiaoke.gradle:packer-ng:1.0.2’
}
}

修改Android模块的 build.gradle

apply plugin: ‘packer’

dependencies {
// add packer-helper
compile ‘com.mcxiaoke.gradle:packer-helper:1.0.2’
}

注意:packer-ng 和 packer-helper 的版本号需要保持一致

Java代码中获取当前渠道

// 如果没有使用PackerNg打包添加渠道,默认返回的是””
// com.mcxiaoke.packer.helper.PackerNg
final String market = PackerNg.getMarket(Context)
// 或者使用 PackerNg.getMarket(Context,defaultValue)
// 之后就可以使用了,比如友盟可以这样设置
AnalyticsConfig.setChannel(market)

渠道打包脚本

可以通过两种方式指定 market 属性,根据需要选用:

  • 打包时命令行使用 -Pmarket= yourMarketFilePath 指定属性
  • 在 gradle.properties 里加入 market=yourMarketFilePath

market是你的渠道名列表文件,market文件是基于项目根目录的 相对路径 ,假设你的项目位于 ~/github/myapp 你的market文件位于 ~/github/myapp/config/markets.txt 那么参数应该是 -Pmarket=config/markets.txt,一般建议直接放在项目根目录,如果market文件参数错误或者文件不存在会抛出异常。

渠道名列表文件是纯文本文件,每行一个渠道号,列表解析的时候会自动忽略空白行和格式不规范的行,请注意看命令行输出,渠道名和注释之间用 # 号分割开,可以没有注释,示例:

Google_Play#play store market
Gradle_Test#test
SomeMarket#some market
HelloWorld

渠道打包的Gradle命令行参数格式示例(在项目根目录执行):

./gradlew -Pmarket=markets.txt clean apkRelease

打包完成后你可以在 ${项目根目录}/build/archives/ 目录找到最终的渠道包。

任务说明

渠道打包的Gradle Task名字是 apk${buildType} buildType一般是release,也可以是你自己指定的beta或者someOtherType,使用时首字母需要大写,例如release的渠道包任务名是 apkRelease,beta的渠道包任务名是 apkBeta,其它的以此类推。

注意事项

如果你的项目有多个productFlavors,默认只会用第一个flavor生成的APK文件作为打包工具的输入参数,忽略其它flavor生成的apk,代码里用的是 ariant.outputs[0].outputFile。如果你想指定使用某个flavor来生成渠道包,可以用 apkFlavor1Release,apkFlavor2Beta这样的名字,示例(假设flavor名字是Intel):

./gradlew -Pmarket=markets.txt clean apkIntelRelease

插件配置说明(可选)

packer {
// 指定渠道打包输出目录
// archiveOutput = file(new File(project.rootProject.buildDir.path, “archives”))
// 指定渠道打包输出文件名格式
// 默认是 ${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}
// archiveNameFormat = ”
}

举例:假如你的App包名是 com.your.company ,渠道名是 Google_Play ,buildType 是 release ,versionName 是 2.1.15 ,versionCode 是 200115 ,那么生成的APK的文件名是 com.your.company-Google_Player-release-2.1.15-20015.apk

  • archiveOutput 指定渠道打包输出的APK存放目录,默认位于${项目根目录}/build/archives

  • archiveNameFormat – Groovy格式字符串, 指定渠道打包输出的APK文件名格式,默认文件名格式是: ${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode},可使用以下变量:

  • projectName – 项目名字

  • appName – App模块名字
  • appPkg – applicationId (App包名packageName)
  • buildType – buildType (release/debug/beta等)
  • flavorName – flavorName (对应渠道打包中的渠道名字)
  • versionName – versionName (显示用的版本号)
  • versionCode – versionCode (内部版本号)
  • buildTime – buildTime (编译构建日期时间)

命令行打包脚本

如果不想使用Gradle插件,这里还有两个命令行打包脚本,在项目的 tools 目录里,分别是 ngpacker-x.x.x-capsule.jar 和 ngpacker.py,使用命令行打包工具,在Java代码里仍然是使用packer-helper包里的 PackerNg.getMarket(Context) 读取渠道

Java脚本

java -jar ngpacker-x.x.x-capsule.jar release_apk_file market_file
// help: java -jar packer-ng-x.x.x-capsule.jar

Python脚本

python ngpacker.py [file] [market] [output] [-h] [-s] [-t TEST]
// help: python packer-ng.py -h
// python; import ngpacker; help(ngpacker)

不使用Gradle

使用命令行打包脚本,不想添加Gradle依赖的,可以完全忽略Gradle的配置,直接复制 PackerNg.java 到项目中使用即可

实现原理

PackerNg原理

优点
– 使用APK注释字段保存渠道信息和MAGIC字节,从文件末尾读取渠道信息,速度快
– 实现为一个Gradle Plugin,支持定制输出APK的文件名等信息,方便CI集成
– 提供Java版和Python的独立命令行脚本,不依赖Gradle插件,支持独立使用
– 由于打包速度极快,单个包只需要5毫秒左右,可用于网站后台动态生成渠道包
缺点
– 没有使用Android的productFlavors,无法利用flavors条件编译的功能

文件格式

Android应用使用的APK文件就是一个带签名信息的ZIP文件,根据 ZIP文件格式规范,每个ZIP文件的最后都必须有一个叫 Central Directory Record 的部分,这个CDR的最后部分叫”end of central directory record”,这一部分包含一些元数据,它的末尾是ZIP文件的注释。注释包含Comment Length和File Comment两个字段,前者表示注释内容的长度,后者是注释的内容,正确修改这一部分不会对ZIP文件造成破坏,利用这个字段,我们可以添加一些自定义的数据,PackerNg项目就是在这里添加和读取渠道信息。

细节处理

原理很简单,就是将渠道信息存放在APK文件的注释字段中,但是实现起来遇到不少坑,测试了好多次。

ZipOutputStream.setComment

FileOutputStream is = new FileOutputStream(“demo.apk”, true);
ZipOutputStream zos = new ZipOutputStream(is);
zos.setComment(“Google_Market”);
zos.finish();
zos.close();

ZipFile zipFile=new ZipFile(“demo.apk”);
System.out.println(zipFile.getComment());

使用Java写入APK文件注释虽然可以正常读取,但是安装的时候会失败,错误信息是:

adb install -r demo.apk
Failure [INSTALL_FAILED_INVALID_APK]

原因未知,可能Java的Zip实现写入了某些特殊字符导致APK文件校验失败,于是只能放弃这个方法。同样的功能使用Python测试完全没有问题,处理后的APK可以正常安装。

ZipFile.getComment

上面是ZIP文件注释写入,使用Java会导致APK文件被破坏,无法安装。这里是读取ZIP文件注释的问题,Java 7里可以使用 zipFile.getComment() 方法直接读取注释,非常方便。但是Android系统直到API 19,也就是4.4以上的版本才支持 ZipFile.getComment() 方法。由于要兼容之前的版本,所以这个方法也不能使用。

解决方法

由于使用Java直接写入和读取ZIP文件的注释都不可行,使用Python又不方便与Gradle系统集成,所以只能自己实现注释的写入和读取。实现起来也不复杂,就是为了提高性能,避免读取整个文件,需要在注释的最后加入几个MAGIC字节,这样从文件的最后开始,读取很少的几个字节就可以定位渠道名的位置。

几个常量定义:

// ZIP文件的注释最长65535个字节
static final int ZIP_COMMENT_MAX_LENGTH = 65535;
// ZIP文件注释长度字段的字节数
static final int SHORT_LENGTH = 2;
// 文件最后用于定位的MAGIC字节
static final byte[] MAGIC = new byte[]{0x21, 0x5a, 0x58, 0x4b, 0x21}; //!ZXK!

读写注释

Java版详细的实现见 PackerNg.java,Python版的实现见 ngpacker.py 。

写入ZIP文件注释:

public static void writeZipComment(File file, String comment)
throws IOException {
byte[] data = comment.getBytes(UTF_8);
final RandomAccessFile raf = new RandomAccessFile(file, “rw”);
raf.seek(file.length() – SHORT_LENGTH);
// write zip comment length
// (content field length + length field length + magic field length)
writeShort(data.length + SHORT_LENGTH + MAGIC.length, raf);
// write content
writeBytes(data, raf);
// write content length
writeShort(data.length, raf);
// write magic bytes
writeBytes(MAGIC, raf);
raf.close();
}

读取ZIP文件注释,有两个版本的实现,这里使用的是 RandomAccessFile ,另一个版本使用的是 MappedByteBuffer ,经过测试,对于特别长的注释,使用内存映射文件读取性能要稍微好一些,对于特别短的注释(比如渠道名),这个版本反而更快一些。

public static String readZipComment(File file) throws IOException {
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(file, “r”);
long index = raf.length();
byte[] buffer = new byte[MAGIC.length];
index -= MAGIC.length;
// read magic bytes
raf.seek(index);
raf.readFully(buffer);
// if magic bytes matched
if (isMagicMatched(buffer)) {
index -= SHORT_LENGTH;
raf.seek(index);
// read content length field
int length = readShort(raf);
if (length > 0) {
index -= length;
raf.seek(index);
// read content bytes
byte[] bytesComment = new byte[length];
raf.readFully(bytesComment);
return new String(bytesComment, UTF_8);
}
}
} finally {
if (raf != null) {
raf.close();
}
}
return null;
}

读取APK文件,由于这个库 packer-helper 需要同时给Gradle插件和Android项目使用,所以不能添加Android相关的依赖,但是又需要读取自身APK文件的路径,使用反射实现:

// for android code
private static String getSourceDir(final Object context)
throws ClassNotFoundException,
InvocationTargetException,
IllegalAccessException,
NoSuchFieldException,
NoSuchMethodException {
final Class<?> contextClass = Class.forName(“android.content.Context”);
final Class<?> applicationInfoClass = Class.forName(“android.content.pm.ApplicationInfo”);
final Method getApplicationInfoMethod = contextClass.getMethod(“getApplicationInfo”);
final Object appInfo = getApplicationInfoMethod.invoke(context);
final Field sourceDirField = applicationInfoClass.getField(“sourceDir”);
return (String) sourceDirField.get(appInfo);
}

Gradle Plugin

这个和旧版插件基本一致,首先是读取渠道列表文件,保存起来,打包的时候遍历列表,复制生成的APK文件到临时文件,给临时文件写入渠道信息,然后复制到输出目录,文件名可以使用模板定制。主要代码如下:

// 添加打包用的TASK
def archiveTask = project.task(“apk${variant.name.capitalize()}”,
type: ArchiveAllApkTask) {
theVariant = variant
theExtension = modifierExtension
theMarkets = markets
dependsOn variant.assemble
}
def buildTypeName = variant.buildType.name
if (variant.name != buildTypeName) {
project.task(“apk${buildTypeName.capitalize()}”, dependsOn: archiveTask)
}

// 遍历列表修改APK文件
theMarkets.each { String market ->
String apkName = buildApkName(theVariant, market)
File tempFile = new File(tempDir, apkName)
File finalFile = new File(outputDir, apkName)
tempFile << originalFile.bytes
copyTo(originalFile, tempFile)
PackerNg.Helper.writeMarket(tempFile, market)
if (PackerNg.Helper.verifyMarket(tempFile, market)) {
copyTo(tempFile, finalFile)
}
}

详细的实现可以查看文件 PackerNgPlugin.groovy 和文件 ArchiveAllApkTask.groovy

同类工具

  • gradle-packer-plugin – 旧版渠道打包工具,完全使用Gradle系统实现,能利用Android提供的productFlavors系统的条件编译功能,无任何兼容性问题,方便集成,但是由于每次都要重新打包,速度比较慢,不适合需要大量打包的情况。(性能:200个渠道包需要一到两小时)
  • Meituan-MultiChannelTool – 使用美团方案的实现,在APK文件的META-INF目里增加渠道文件,打包速度也非常快,但读取时需要遍历APK文件的数据项,比较慢,而且以后可能遇到兼容性问题
  • MultiChannelPackageTool – 将渠道写入APK文件的注释,这个项目没有提供Gradle插件,只有命令行工具,不方便CI集成,使用ZIP文件注释的思路就是来自此项目

关于作者

联系方式
– Blog: http://blog.mcxiaoke.com
– Github: https://github.com/mcxiaoke
– Email: github@mcxiaoke.com
开源项目
– Next公共组件库: https://github.com/mcxiaoke/Android-Next
– Gradle渠道打包: https://github.com/mcxiaoke/gradle-packer-plugin
– EventBus实现xBus: https://github.com/mcxiaoke/xBus
– Rx文档中文翻译: https://github.com/mcxiaoke/RxDocs
– MQTT协议中文版: https://github.com/mcxiaoke/mqtt
– 蘑菇饭App: https://github.com/mcxiaoke/minicat
– 饭否客户端: https://github.com/mcxiaoke/fanfouapp-opensource
– Volley镜像: https://github.com/mcxiaoke/android-volley

License

Copyright 2014 – 2015 Xiaoke Zhang

Licensed under the Apache License, Version 2.0 (the “License”);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

项目地址: https://github.com/mcxiaoke/packer-ng-plugin

Android反编译和二次打包实战

       作为Android开发者,工作中少不了要反编译别人的apk,当然主要目的还是为了学习到更多,取彼之长,补己之短。今天就来总结一下Android反编译和二次打包的一些知识。首先声明本文的目的是为了通过例子讲解反编译和二次打包的原理和方法,继而作为后续讲解防止二次打包和App安全的依据,并不是鼓励大家去重新打包别人的App,盗取他人劳动成果。

       本文首先介绍几种Android反编译工具的使用,然后实现在不需要知道源代码的情况下,仅通过修改反编译得到的smali文件实现修改apk逻辑功能的目的。

       Android中常用的反编译工具有三个:dex2jar、jd-gui和apktool,这三个工具的作用如下:

dex2jar:将apk中的classes.dex文件转换成jar文件。

jd-gui:查看由dex2jar转换成的jar文件,以界面的形式展示反编译出来的Java源代码。

apktool:反编译生成smali字节码文件,提取apk中的资源文件。

       为了尽可能的把问题讲清楚,我们来实现一个很简单的例子。首先创建一个工程DecompileDemo,在MainActivity中定义一个布局,其中包含一个Button,点击会打印一段日志。

  1. public class MainActivity extends AppCompatActivity implements View.OnClickListener {
  2.     private static final String TAG = “MainActivity”;
  3.     private Button btn;
  4.     @Override
  5.     protected void onCreate(Bundle savedInstanceState) {
  6.         super.onCreate(savedInstanceState);
  7.         setContentView(R.layout.activity_main);
  8.         btn = (Button) findViewById(R.id.btn);
  9.         btn.setOnClickListener(this);
  10.     }
  11.     @Override
  12.     public void onClick(View v) {
  13.         Log.d(TAG,“Button is clicked”);
  14.     }
  15. }

       将这个工程编译生成的apk解压,取出其中的classes.dex放在dex2jar工具的目录下,然后执行命令


       会在当前目录下生成class-dex2jar.jar文件


       然后打开jd-gui,将class-dex2jar.jar文件拖进去,就可以看到反编译出来的源代码。


       可以看到反编译的代码和原本的代码差别不大,主要差别是原来的资源引用全都变成了数字。

       下面我们来修改这个apk的内容。

       首先我们将apk拷贝到apktool工具目录下,执行命令apktool  d  app-release.apk。


       生成的目录中包含smali文件夹


       然后找到我们的主要的类MainActivity.smali,文件内容如下:

  1. .class public Lcom/viclee/decompiledemo/MainActivity;
  2. .super Landroid/support/v7/app/AppCompatActivity;
  3. .source “MainActivity.java”
  4. # interfaces
  5. .implements Landroid/view/View$OnClickListener;
  6. # static fields
  7. .field private static final TAG:Ljava/lang/String; = “MainActivity”
  8. # instance fields
  9. .field private btn:Landroid/widget/Button;
  10. # direct methods
  11. .method public constructor <init>()V
  12.     .locals 0
  13.     .prologue
  14.     .line 9
  15.     invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V
  16.     return-void
  17. .end method
  18. # virtual methods
  19. .method public onClick(Landroid/view/View;)V
  20.     .locals 2
  21.     .param p1, “v”    # Landroid/view/View;
  22.     .prologue
  23.     .line 23
  24.     const-string v0, “MainActivity”
  25.     const-string v1, “Button is clicked”
  26.     invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
  27.     .line 24
  28.     return-void
  29. .end method
  30. .method protected onCreate(Landroid/os/Bundle;)V
  31.     .locals 1
  32.     .param p1, “savedInstanceState”    # Landroid/os/Bundle;
  33.     .prologue
  34.     .line 14
  35.     invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V
  36.     .line 15
  37.     const v0, 0x7f040019
  38.     invoke-virtual {p0, v0}, Lcom/viclee/decompiledemo/MainActivity;->setContentView(I)V
  39.     .line 17
  40.     const v0, 0x7f0c0050
  41.     invoke-virtual {p0, v0}, Lcom/viclee/decompiledemo/MainActivity;->findViewById(I)Landroid/view/View;
  42.     move-result-object v0
  43.     check-cast v0, Landroid/widget/Button;
  44.     iput-object v0, p0, Lcom/viclee/decompiledemo/MainActivity;->btn:Landroid/widget/Button;
  45.     .line 18
  46.     iget-object v0, p0, Lcom/viclee/decompiledemo/MainActivity;->btn:Landroid/widget/Button;
  47.     invoke-virtual {v0, p0}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V
  48.     .line 19
  49.     return-void
  50. .end method

       其中36-40行是打印日志的位置,文件内容很清晰,每个区域的意义如下:

.class  类名

.super 父类名

.source  文件名

.implements  这个类实现的接口

.field  成员变量

.method 方法

       然后新建一个工程,在这个工程中实现想要替换的代码,我们这里是希望将原始工程中打印日志的地方替换为弹出一个Toast。

  1. public class MainActivity extends AppCompatActivity{
  2.     @Override
  3.     protected void onCreate(Bundle savedInstanceState) {
  4.         super.onCreate(savedInstanceState);
  5.         setContentView(R.layout.activity_main);
  6.         showToast();
  7.     }
  8.     public void showToast() {
  9.         Toast.makeText(this,“我是反编译后进行的修改。”,Toast.LENGTH_LONG).show();
  10.     }
  11. }


然后像前面一样执行apktool命令,生成的smali文件内容如下:

  1. .class public Lcom/viclee/decompiledemo/MainActivity;
  2. .super Landroid/support/v7/app/AppCompatActivity;
  3. .source “MainActivity.java”
  4. # direct methods
  5. .method public constructor <init>()V
  6.     .locals 0
  7.     .prologue
  8.     .line 7
  9.     invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V
  10.     return-void
  11. .end method
  12. # virtual methods
  13. .method protected onCreate(Landroid/os/Bundle;)V
  14.     .locals 1
  15.     .param p1, “savedInstanceState”    # Landroid/os/Bundle;
  16.     .prologue
  17.     .line 10
  18.     invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V
  19.     .line 11
  20.     const v0, 0x7f040019
  21.     invoke-virtual {p0, v0}, Lcom/viclee/decompiledemo/MainActivity;->setContentView(I)V
  22.     .line 13
  23.     invoke-virtual {p0}, Lcom/viclee/decompiledemo/MainActivity;->showToast()V
  24.     .line 14
  25.     return-void
  26. .end method
  27. .method public showToast()V
  28.     .locals 2
  29.     .prologue
  30.     .line 17
  31.     const-string v0, “u6211u662fu53cdu7f16u8bd1u540eu8fdbu884cu7684u4feeu6539u3002”
  32.     const/4 v1, 0x1
  33.     invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
  34.     move-result-object v0
  35.     invoke-virtual {v0}, Landroid/widget/Toast;->show()V
  36.     .line 18
  37.     return-void
  38. .end method

       上面代码中,33、39-56行就是弹出Toast的代码部分。将上面整个showToast方法拷贝到原始工程的smali文件中,这里要特别注意修改行号,这个行号表示的是代码在原始Java文件中的行号,需要参考两个smali文件的行号来修改。我认为只要保证方法内的行号不乱序,并且方法之间的行号不冲突就可以。然后,需要将原始工程中打印日志的代码替换为显示Toast的代码,也就是将原始smali文件中36-40行修改为新建工程中33、39-56行的内容。修改后的内容如下,主要关注下面内容中36行、75-91行与原始smali文件的差异。

  1. .class public Lcom/viclee/decompiledemo/MainActivity;
  2. .super Landroid/support/v7/app/AppCompatActivity;
  3. .source “MainActivity.java”
  4. # interfaces
  5. .implements Landroid/view/View$OnClickListener;
  6. # static fields
  7. .field private static final TAG:Ljava/lang/String; = “MainActivity”
  8. # instance fields
  9. .field private btn:Landroid/widget/Button;
  10. # direct methods
  11. .method public constructor <init>()V
  12.     .locals 0
  13.     .prologue
  14.     .line 9
  15.     invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V
  16.     return-void
  17. .end method
  18. # virtual methods
  19. .method public onClick(Landroid/view/View;)V
  20.     .locals 2
  21.     .param p1, “v”    # Landroid/view/View;
  22.     .prologue
  23.     .line 23
  24.     invoke-virtual {p0}, Lcom/viclee/decompiledemo/MainActivity;->showToast()V
  25.     .line 24
  26.     return-void
  27. .end method
  28. .method protected onCreate(Landroid/os/Bundle;)V
  29.     .locals 1
  30.     .param p1, “savedInstanceState”    # Landroid/os/Bundle;
  31.     .prologue
  32.     .line 14
  33.     invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V
  34.     .line 15
  35.     const v0, 0x7f040019
  36.     invoke-virtual {p0, v0}, Lcom/viclee/decompiledemo/MainActivity;->setContentView(I)V
  37.     .line 17
  38.     const v0, 0x7f0c0050
  39.     invoke-virtual {p0, v0}, Lcom/viclee/decompiledemo/MainActivity;->findViewById(I)Landroid/view/View;
  40.     move-result-object v0
  41.     check-cast v0, Landroid/widget/Button;
  42.     iput-object v0, p0, Lcom/viclee/decompiledemo/MainActivity;->btn:Landroid/widget/Button;
  43.     .line 18
  44.     iget-object v0, p0, Lcom/viclee/decompiledemo/MainActivity;->btn:Landroid/widget/Button;
  45.     invoke-virtual {v0, p0}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V
  46.     .line 19
  47.     return-void
  48. .end method
  49. .method public showToast()V
  50.     .locals 2
  51.     .prologue
  52.     .line 27
  53.     const-string v0, “u6211u662fu53cdu7f16u8bd1u540eu8fdbu884cu7684u4feeu6539u3002”
  54.     const/4 v1, 0x1
  55.     invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
  56.     move-result-object v0
  57.     invoke-virtual {v0}, Landroid/widget/Toast;->show()V
  58.     .line 28
  59.     return-void

       然后我们需要将修改后的文件目录重新打包,执行命令 apktool   b  app-release,就会在app-releae目录下生成两个文件夹:build 文件夹里面是一些中间文件(classes.dex等内容),dist 文件夹里面存放着重新打包出来的apk文件。

       最后还要记得对生成的apk进行签名,否则安装时会报错。执行下面的命令行:

jarsigner -verbose -keystore viclee.keystore -signedjar app-release-signed.apk app-release.apk viclee.keystore

-verbose 输出签名详细信息
-keystore 指定密钥对的存储路径
-signedjar 后面三个参数分别是签名后的apk、未签名的apk和密钥对的别名

       安装签名后的apk,点击按钮,确实弹出了Toast,内容和我们所设置的一致,说明我们的修改成功了。

       我们注意到,修改smali文件的时候并不是直接在文件上进行修改,毕竟smali文件的可读性差,直接修改是十分困难的。我们的解决办法是新建一个工程将需要增加的代码实现,最好抽成一个单独的方法(方便替换),然后将新工程打包产生的apk反编译,得到对应的smali文件,再用其中的内容对原始smali文件进行替换。这样的修改方式降低了修改的难度也减小了犯错误的风险。


       另外,apk反编译后也可以修改资源,将反编译出来的资源文件修改一通,然后按照之前的方法,重新打包、签名、安装。下面两个页面是修改之前和修改之后的对比图。

                  

       到这里,本文的全部内容就讲解完了,欢迎大家评论交流~

 

android 设置EditText光标位置

Android中有很多可编辑的弹出框,其中有些是让我们来修改其中的字符,这时光标位置定位在哪里呢?
刚刚解了一个bug是关于这个光标的位置的,似乎Android原生中这种情况是把光标定位到字符串的最前面。需求是将光标定位到字符的最后面。
修改的地方是TextView这个控件,因为EditText也是继承了TextView。在setText方法中有:

1  private void setText(CharSequence text, BufferType type,
2                          boolean notifyBefore, int oldlen) {
3 ……
4         if (text instanceof Spannable) {
5             Spannable sp = (Spannable) text;
6
7             ……
8             if (mMovement != null) {
9                 mMovement.initialize(this, (Spannable) text);
10         //文本是不是Editable的。
11         if(this instanceof Editable)
12                      //设定光标位置
13                      Selection.setSelection((Spannable)text, text.length());
14
15                ……
16     }

从红色代码中可以看出,google是要光标处在缺省文本的末端,但是,log发现 (this instanceof Editable)非真,也就是说Selection.setSelection((Spannable)text, text.length());并不会被执行。

1    Log.d(“TextView”, “(type == BufferType.EDITABLE)=”+(type == BufferType.EDITABLE));
2    if(type == BufferType.EDITABLE){
3          Log.d(“TextView”,”Format text.Set cursor to the end “);
4          Selection.setSelection((Spannable)text, text.length());
5    }

这个样修改后即可。

 

在编写应用的时候,如果我们要将光标定位到某个位置,可以采用下面的方法:

1 CharSequence text = editText.getText();
2 //Debug.asserts(text instanceof Spannable);
3 if (text instanceof Spannable) {
4     Spannable spanText = (Spannable)text;
5     Selection.setSelection(spanText, text.length());
6 }

其中红色标记的代码为你想要设置的位置,此处是设置到文本末尾。

android 主线程和子线程之间通过handler关联消息传递

从主线程发送消息到子线程(准确地说应该是非UI线程)

复制代码
 package com.zhuozhuo;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;

public class LooperThreadActivity extends Activity{
/** Called when the activity is first created. */

private final int MSG_HELLO = 0;
private Handler mHandler;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
new CustomThread().start();//新建并启动CustomThread实例

findViewById(R.id.send_btn).setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {//点击界面时发送消息
                String str = “hello”;
Log.d(“Test”, “MainThread is ready to send msg:” + str);
mHandler.obtainMessage(MSG_HELLO, str).sendToTarget();//发送消息到CustomThread实例

}
});

}

class CustomThread extends Thread {
@Override
public void run() {
//建立消息循环的步骤
            Looper.prepare();//1、初始化Looper
            mHandler = new Handler(){//2、绑定handler到CustomThread实例的Looper对象
                public void handleMessage (Message msg) {//3、定义处理消息的方法
                    switch(msg.what) {
case MSG_HELLO:
Log.d(“Test”, “CustomThread receive msg:” + (String) msg.obj);
}
}
};
Looper.loop();//4、启动消息循环
        }
}
}

复制代码

从非UI线程传递消息到UI线程(界面主线程),因为主界面已经有MessageQueue,所以可以直接获取消息处理消息。而上面由主线程向非UI线程中处理消息的时候,非UI线程需要先添加消息队列,然后处理消息循环。

复制代码

public class ThreadHandlerActivity extends Activity {
/** Called when the activity is first created. */

private static final int MSG_SUCCESS = 0;//获取图片成功的标识
    private static final int MSG_FAILURE = 1;//获取图片失败的标识

private ImageView mImageView;
private Button mButton;

private Thread mThread;

private Handler mHandler = new Handler() {
public void handleMessage (Message msg) {//此方法在ui线程运行
            switch(msg.what) {
case MSG_SUCCESS:
mImageView.setImageBitmap((Bitmap) msg.obj);//imageview显示从网络获取到的logo
                Toast.makeText(getApplication(), getApplication().getString(R.string.get_pic_success), Toast.LENGTH_LONG).show();
break;

case MSG_FAILURE:
Toast.makeText(getApplication(), getApplication().getString(R.string.get_pic_failure), Toast.LENGTH_LONG).show();
break;
}
}
};

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mImageView= (ImageView) findViewById(R.id.imageView);//显示图片的ImageView
        mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
if(mThread == null) {
mThread = new Thread(runnable);
mThread.start();//线程启动
                }
else {
Toast.makeText(getApplication(), getApplication().getString(R.string.thread_started), Toast.LENGTH_LONG).show();
}
}
});
}

Runnable runnable = new Runnable() {

@Override
public void run() {//run()在新的线程中运行
            HttpClient hc = new DefaultHttpClient();
HttpGet hg = new HttpGet(“http://csdnimg.cn/www/images/csdnindex_logo.gif”);//获取csdn的logo
            final Bitmap bm;
try {
HttpResponse hr = hc.execute(hg);
bm = BitmapFactory.decodeStream(hr.getEntity().getContent());
catch (Exception e) {
mHandler.obtainMessage(MSG_FAILURE).sendToTarget();//获取图片失败
                return;
}
mHandler.obtainMessage(MSG_SUCCESS,bm).sendToTarget();//获取图片成功,向ui线程发送MSG_SUCCESS标识和bitmap对象

//            mImageView.setImageBitmap(bm); //出错!不能在非ui线程操作ui元素

//            mImageView.post(new Runnable() {//另外一种更简洁的发送消息给ui线程的方法。
//
//                @Override
//                public void run() {//run()方法会在ui线程执行
//                    mImageView.setImageBitmap(bm);
//                }
//            });
        }
};

复制代码

}

作者:Jackhuclan
出处:http://jackhuclan.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

Java synchronized详解

第一篇:

使用synchronized

在编写一个类时,如果该类中的代码可能运行于多线程环境下,那么就要考虑同步的问题。在Java中内置了语言级的同步原语--synchronized,这也大大简化了Java中多线程同步的使用。我们首先编写一个非常简单的多线程的程序,是模拟银行中的多个线程同时对同一个储蓄账户进行存款、取款操作的。
在程序中我们使用了一个简化版本的Account类,代表了一个银行账户的信息。在主程序中我们首先生成了1000个线程,然后启动它们,每一个线程都对John的账户进行存100元,然后马上又取出100元。这样,对于John的账户来说,最终账户的余额应该是还是1000元才对。然而运行的结果却超出我们的想像,首先来看看我们的演示代码:

class Account {
    String name;
    float amount;
    
    
    public Account(String name, float amount) {
        this.name = name;
        this.amount = amount;
    }

    public  void deposit(float amt) {
        float tmp = amount;
        tmp += amt;
        
        try {
            Thread.sleep(100);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }
        
        amount = tmp;
    }

    public  void withdraw(float amt) {
        float tmp = amount;
        tmp -= amt;

        try {
            Thread.sleep(100);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }        

        amount = tmp;
    }

    public float getBalance() {
        return amount;
    }
}



public class AccountTest{
    private static int NUM_OF_THREAD = 1000;
    static Thread[] threads = new Thread[NUM_OF_THREAD];
    
    public static void main(String[] args){
        final Account acc = new Account("John"1000.0f);
        for (int i = 0; i< NUM_OF_THREAD; i++) {
            threads[i] = new Thread(new Runnable() {
                public void run() {
                        acc.deposit(100.0f);
                        acc.withdraw(100.0f);
                }
            });
            threads[i].start();
        }

        for (int i=0; i<NUM_OF_THREAD; i++){
            try {
                threads[i].join(); //等待所有线程运行结束
            } catch (InterruptedException e) {
                // ignore
            }
        }
        System.out.println("Finally, John's balance is:" + acc.getBalance());
    }

}

注意,上面在Account的deposit和withdraw方法中之所以要把对amount的运算使用一个临时变量首先存储,sleep一段时间,然后,再赋值给amount,是为了模拟真实运行时的情况。因为在真实系统中,账户信息肯定是存储在持久媒介中,比如RDBMS中,此处的睡眠的时间相当于比较耗时的数据库操作,最后把临时变量tmp的值赋值给amount相当于把amount的改动写入数据库中。运行AccountTest,结果如下(每一次结果都会不同):

E:javaexerbin>java AccountTest
Finally, John's balance is:3900.0

E:javaexerbin>java AccountTest
Finally, John's balance is:4900.0

E:javaexerbin>java AccountTest
Finally, John's balance is:4700.0

E:javaexerbin>java AccountTest
Finally, John's balance is:3900.0

E:javaexerbin>java AccountTest
Finally, John's balance is:3900.0

E:javaexerbin>java AccountTest
Finally, John's balance is:5200.0

为什么会出现这样的问题?这就是多线程中的同步的问题。在我们的程序中,Account中的amount会同时被多个线程所访问,这就是一个竞争资源,通常称作竞态条件。

拿a,b3个线程举例:

a线程存(事件1)->b线程存一半(事件2,刚记下amount)->a线程提(事件3)->b线程存剩下一半(事件4)->b线程提(事件5)

amount变化值:

1000->a线程存(事件1)->1100

1100->b线程存一半(事件2,刚记下amount)->1100

1100->a线程提(事件3)->1000

1100->b线程存剩下一半(事件4)->1200

1200->b线程提(事件5)->1100

事件2,3,4之间出现问题。

对于这样的多个线程共享的资源我们必须进行同步,以避免一个线程的改动被另一个线程所覆盖。在我们这个程序中,Account中的amount是一个竞态条件,所以所有对amount的修改访问都要进行同步,我们将deposit()和withdraw()方法进行同步,修改为:

 public synchronized void deposit(float amt) {
        float tmp = amount;
        tmp += amt;

        try {
            Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }

        amount = tmp;
    }

    public synchronized void withdraw(float amt) {
        float tmp = amount;
        tmp -= amt;

        try {
            Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }

        amount = tmp;
    }

此时,再运行,我们就能够得到正确的结果了。Account中的getBalance()也访问了amount,为什么不对getBalance()同步呢?因为getBalance()并不会修改amount的值,所以,同时多个线程对它访问不会造成数据的混乱。

 

同步加锁的是对象,而不是代码。

因此,如果你的类中有一个同步方法,这个方法可以被两个不同的线程同时执行,只要每个线程自己创建一个的该类的实例即可。

 

参考下面的代码:

class Foo extends Thread
{
    private int val;
    public Foo(int v)
    {
        val = v;
    }
    public synchronized void printVal(int v)
    {
        while(true)
            System.out.println(v);
    }
    
    public void run()
    {
        printVal(val);
    }
}

class SyncTest
{
    public static void main(String args[])
    {
        Foo f1 = new Foo(1);
        f1.start();
        Foo f2 = new Foo(3);
        f2.start();
    }
}

运行SyncTest产生的输出是1和3交叉的。如果printVal是断面,你看到的输出只能是1或者只能是3而不能是两者同时出现。程序运行的结果证明两个线程都在并发的执行printVal方法,即使该方法是同步的并且由于是一个无限循环而没有终止。

类的同步:

要实现真正的断面,你必须同步一个全局对象或者对类进行同步。下面的代码给出了一个这样的范例。

class Foo extends Thread
{
    private int val;
    public Foo(int v)
    {
        val = v;
    }
    public void printVal(int v)
    {
        synchronized(Foo.class) {
            while(true)
                System.out.println(v);
        }
    }
    
    public void run()
    {
        printVal(val);
    }
}

 

上面的类不再对个别的类实例同步而是对类进行同步。对于类Foo而言,它只有唯一的类定义,两个线程在相同的锁上同步,因此只有一个线程可以执行printVal方法。

这个代码也可以通过对公共对象加锁。例如给Foo添加一个静态成员。两个方法都可以同步这个对象而达到线程安全。

下面笔者给出一个参考实现,给出同步公共对象的两种通常方法:

1、

class Foo extends Thread
{
    private int val;
    private static Object lock=new Object();
    public Foo(int v)
    {
        val = v;
    }
    
    public void printVal(int v)
    {
        synchronized(lock) {
            while(true)
                System.out.println(v);
        }
    }
    public void run()
    {
        printVal(val);
    }
}

 

上面的这个例子比原文给出的例子要好一些,因为原文中的加锁是针对类定义的。在有多个方法需要同步时,如果使用类作为锁,他们的并发粒度会太小,不如针对每个需要保护的成员变量 定义一个锁,这样并发粒度要大些。

2、

class Foo extends Thread
{
    private String name;
    private String val;
    public Foo(String name,String v)
    {
        this.name=name;
        val = v;
    }
    public void printVal()
    {
        synchronized(val) {
            while(true) System.out.println(name+val);
        }
    }
    public void run()
    {
        printVal();
    }
}

public class SyncMethodTest
{
    public static void main(String args[])
    {
        Foo f1 = new Foo("Foo 1:","printVal");
        f1.start();
        Foo f2 = new Foo("Foo 2:","printVal");
        f2.start();
    }
}

 

上面这个代码需要进行一些额外的说明,因为JVM有一种优化机制,因为String类型的对象是不可变的,因此当你使用””的形式引用字符串时,如果JVM发现内存已经有一个这样的对象,那么它就使用那个对象而不再生成一个新的String对象,这样是为了减小内存的使用。

上面的main方法其实等同于:

public static void main(String args[])
{
    String value="printVal";
    Foo f1 = new Foo("Foo 1:",value);
    f1.start();
    Foo f2 = new Foo("Foo 2:",value);
    f2.start();
}

 

总结:

1、synchronized关键字的作用域有二种:
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象;

3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;

Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

五、以上规则对其它对象锁同样适用.

 

第二篇:

synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。
1. synchronized 方法:通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:
public synchronized void accessVal(int newVal);
synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有

一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。
在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可

以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。
2. synchronized 块:通过 synchronized关键字来声明synchronized 块。语法如下:
synchronized(syncObject) {
//允许访问控制的代码
}
synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机

制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
对synchronized(this)的一些理解
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线

程必须等待当前线程执行完这个代码块以后才能执行该代码块。
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
五、以上规则对其它对象锁同样适用
第三篇:

打个比方:一个object就像一个大房子,大门永远打开。房子里有 很多房间(也就是方法)。

这些房间有上锁的(synchronized方法), 和不上锁之分(普通方法)。房门口放着一把钥匙(key),这把钥匙可以打开所有上锁的房间。

另外我把所有想调用该对象方法的线程比喻成想进入这房子某个 房间的人。所有的东西就这么多了,下面我们看看这些东西之间如何作用的。

在此我们先来明确一下我们的前提条件。该对象至少有一个synchronized方法,否则这个key还有啥意义。当然也就不会有我们的这个主题了。

一个人想进入某间上了锁的房间,他来到房子门口,看见钥匙在那儿(说明暂时还没有其他人要使用上锁的 房间)。于是他走上去拿到了钥匙,并且按照自己 的计划使用那些房间。注意一点,他每次使用完一次上锁的房间后会马上把钥匙还回去。即使他要连续使用两间上锁的房间,中间他也要把钥匙还回去,再取回来。

因此,普通情况下钥匙的使用原则是:“随用随借,用完即还。”

这时其他人可以不受限制的使用那些不上锁的房间,一个人用一间可以,两个人用一间也可以,没限制。但是如果当某个人想要进入上锁的房间,他就要跑到大门口去看看了。有钥匙当然拿了就走,没有的话,就只能等了。

要是很多人在等这把钥匙,等钥匙还回来以后,谁会优先得到钥匙?Not guaranteed。象前面例子里那个想连续使用两个上锁房间的家伙,他中间还钥匙的时候如果还有其他人在等钥匙,那么没有任何保证这家伙能再次拿到。 (JAVA规范在很多地方都明确说明不保证,象Thread.sleep()休息后多久会返回运行,相同优先权的线程那个首先被执行,当要访问对象的锁被 释放后处于等待池的多个线程哪个会优先得到,等等。我想最终的决定权是在JVM,之所以不保证,就是因为JVM在做出上述决定的时候,绝不是简简单单根据 一个条件来做出判断,而是根据很多条。而由于判断条件太多,如果说出来可能会影响JAVA的推广,也可能是因为知识产权保护的原因吧。SUN给了个不保证 就混过去了。无可厚非。但我相信这些不确定,并非完全不确定。因为计算机这东西本身就是按指令运行的。即使看起来很随机的现象,其实都是有规律可寻。学过 计算机的都知道,计算机里随机数的学名是伪随机数,是人运用一定的方法写出来的,看上去随机罢了。另外,或许是因为要想弄的确定太费事,也没多大意义,所 以不确定就不确定了吧。)

再来看看同步代码块。和同步方法有小小的不同。

1.从尺寸上讲,同步代码块比同步方法小。你可以把同步代码块看成是没上锁房间里的一块用带锁的屏风隔开的空间。

2.同步代码块还可以人为的指定获得某个其它对象的key。就像是指定用哪一把钥匙才能开这个屏风的锁,你可以用本房的钥匙;你也可以指定

用另一个房子的钥匙才能开,这样的话,你要跑到另一栋房子那儿把那个钥匙拿来,并用那个房子的钥匙来打开这个房子的带锁的屏风。

记住你获得的那另一栋房子的钥匙,并不影响其他人进入那栋房子没有锁的房间。

为什么要使用同步代码块呢?我想应该是这样的:首先对程序来讲同步的部分很影响运行效率,而一个方法通常是先创建一些局部变

量,再对这些变量做一些 操作,如运算,显示等等;而同步所覆盖的代码越多,对效率的影响就越严重。因此我们通常尽量缩小其影响范围。

如何做?同步代码块。我们只把一个方法中该同 步的地方同步,比如运算。

另外,同步代码块可以指定钥匙这一特点有个额外的好处,是可以在一定时期内霸占某个对象的key。还记得前面说过普通情况下钥

匙的使用原则吗。现在不是普通情况了。你所取得的那把钥匙不是永远不还,而是在退出同步代码块时才还。

还用前面那个想连续用两个上锁房间的家伙打比方。怎样才能在用完一间以后,继续使用另一间呢。用同步代码块吧。先创建另外

一个线程,做一个同步代码 块,把那个代码块的锁指向这个房子的钥匙。然后启动那个线程。只要你能在进入那个代码块时抓到这房子的钥匙,你就可以一直保留到退出那个代码块。也就是说 你甚至可以对本房内所有上锁的房间遍历,甚至再sleep(10*60*1000),而房门口却还有1000个线程在等这把钥匙呢。很过瘾吧。

在此对sleep()方法和钥匙的关联性讲一下。一个线程在拿到key后,且没有完成同步的内容时,如果被强制sleep()了,那key还一直在 它那儿。直到它再次运行,做完所有同步内容,才会归还key。记住,那家伙只是干活干累了,去休息一下,他并没干完他要干的事。为了避免别人进入那个房间 把里面搞的一团糟,即使在睡觉的时候他也要把那唯一的钥匙戴在身上。

最后,也许有人会问,为什么要一把钥匙通开,而不是一个钥匙一个门呢?我想这纯粹是因为复杂性问题。一个钥匙一个门当然更安全,但是会牵扯好多问题。钥匙 的产生,保管,获得,归还等等。其复杂性有可能随同步方法的增加呈几何级数增加,严重影响效率。这也算是一个权衡的问题吧。为了增加一点点安全性,导致效 率大大降低,是多么不可取啊。

synchronized的一个简单例子

public class TextThread {

    public static void main(String[] args) {
        TxtThread tt = new TxtThread();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
    }
}

class TxtThread implements Runnable {
    int num = 100;
    String str = new String();
    
    public void run() {
        synchronized (str) {
        while (num > 0) {
            try {
                Thread.sleep(1);
            } catch (Exception e) {
                e.getMessage();
            }
            System.out.println(Thread.currentThread().getName()+ "this is " + num--);
            }
        }
    }
}

上面的例子中为了制造一个时间差,也就是出错的机会,使用了Thread.sleep(10)

Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题。到底如何?――还得对synchronized关键字的作用进行深入了解才可定论。

总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。

在进一步阐述之前,我们需要明确几点:

A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。

B.每个对象只有一个锁(lock)与之相关联。

C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

接着来讨论synchronized用到不同地方对代码产生的影响:

假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。

1. 把synchronized当作函数修饰符时,示例代码如下:

Public synchronized void methodAAA()

{

//….

}

这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。

上边的示例代码等同于如下代码:

public void methodAAA()

{

synchronized (this)      // (1)

{

//…..

}

}

(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:(

2.同步块,示例代码如下:

public void method3(SomeObject so)

{

synchronized(so)

{
//…..
}

}

这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Foo implements Runnable

{

private byte[] lock = new byte[0]; // 特殊的instance变量

Public void methodA()
{

synchronized(lock) { //… }

}

//…..

}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock= new Object()则需要7行操作码。

3.将synchronized作用于static 函数,示例代码如下:

Class Foo
{

public synchronized static void methodAAA()   // 同步的static 函数
{
//….
}

public void methodBBB()
{

synchronized(Foo.class)   // class literal(类名称字面常量)

}
}

代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。

记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。

可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。

小结如下:

搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。

还有一些技巧可以让我们对共享资源的同步访问更加安全:

1. 定义private 的instance变量+它的 get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。

2. 如果instance变量是一个对象,如数组或ArrayList什么的,那上述方法仍然不安全,因为当外界对象通过get方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。 这个时候就需要将get方法也加上synchronized同步,并且,只返回这个private对象的clone()――这样,调用端得到的就是对象副本的引用了。

利用Android的 IntentService完成后台异步任务

初学android比较纠结的一点就是service和activity的交互,在service中不能进行耗时的操作,否则会阻塞UI进程,我们可以在service中new一个Thread出来进行耗时的操作,但毕竟有点麻烦。所以android还提供了IntentService类来帮助解决这个问题。

IntentService是继承至Service子类,用于处理异步的请求,你可以通过startService(Intent)来发送你需要的请求。在IntentService中使用了一个ServiceHandler(继承至Handler)来处理接收到的请求。对于每个异步的startService请求,IntentService会处理完成一个之后再处理第二个。每一个请求都会在一个单独的worker thread中处理,不会阻塞应用程序的主线程。

IntentService的特点如下:

(1)它创建了一个独立的工作线程来处理所有的通过onStartCommand()传递给服务的intents。

(2)创建了一个工作队列,来逐个发送intent给onHandleIntent()。

(3)不需要主动调用stopSelft()来结束服务。因为,在所有的intent被处理完后,系统会自动关闭服务。

(4)默认实现的onBind()返回null

(5)默认实现的onStartCommand()的目的是将intent插入到工作队列中。

IntentService是一个基于消息的服务,每次启动该服务并不是马上处理你的工作,而是首先会创建对应的Looper,Handler并且在MessageQueue中添加的附带客户Intent的Message对象,当Looper发现有Message的时候接着得到Intent对象通过在onHandleIntent((Intent)msg.obj)中调用你的处理程序.处理完后即会停止自己的服务.意思是Intent的生命周期跟你的处理的任务是一致的.所以这个类用下载任务中非常好,下载任务结束后服务自身就会结束退出.

使用方法也十分简单,所需要做的就是实现 onHandleIntent() 方法,在该方法内实现你想进行的操作。另外,继承IntentService时,你必须提供一个无参构造函数,且在该构造函数内,你需要调用父类的构造函数

启动服务:

findViewById(R.id.intentService).setOnClickListener(

new OnClickListener() {

@Override

public void onClick(View v) {

startService( new Intent(Main.this,

IntentServiceExm. class));

}

});

IntentService服务:

public class IntentServiceExm extends IntentService {

public IntentServiceExm() {

super(“IntentServiceExm” );

}

@Override

protected void onHandleIntent(Intent intent) {

try {

// 这里模拟耗时操作,睡眠5秒

Thread.sleep(5000);

Log. i(“IntentService”, “in IntentService” );

catch (InterruptedException e) {

e.printStackTrace();

}

}

}

在此特别需要注意的是IntentService是线性处理请求的。如果有多个Intent请求执行,则会被放在工作队列中,依次等待,顺序执行。所以若是想在Service中让多个线程并发的话,就得另想法子,比如本文开头所说的在service中新建多个Thread来解决。

java 多线程 之 CountDownLatch 代码示例

CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
这个概念和unix中的屏障(barrier)很相似,可能底层实现就是barrier。

屏障允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出。所有线程达到屏障后可以接着工作。

#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier,const pthread_barrierattr_t *restirct attr, unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);

ccount指定屏障计数。
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
调用pthread_barrier_wait的线程在屏障计数未满足条件时,进入休眠状态。如果满足屏障计数,所有的线程都被唤醒。
    其中一个线程会返回PTHREAD_BARRIER_SERIAL_THREAD,剩下的线程看到的返回值是0,这可以控制其中一个线程
    作为主线程,它可以工作在其他线程已经完成的工作结果上。

CountDownLatch的主要方法

 public CountDownLatch(int count);

 public void countDown();

 public void await() throws InterruptedException

构造方法参数指定了计数的次数

countDown方法,当前线程调用此方法,则计数减一

awaint方法,调用此方法会一直阻塞当前线程,直到计时器的值为0

例子

public class CountDownLatchDemo {  
    final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    public static void main(String[] args) throws InterruptedException {  
        CountDownLatch latch=new CountDownLatch(2);//两个工人的协作  
        Worker worker1=new Worker("zhang san", 5000, latch);  
        Worker worker2=new Worker("li si", 8000, latch);  
        worker1.start();//  
        worker2.start();//  
        latch.await();//等待所有工人完成工作  
        System.out.println("all work done at "+sdf.format(new Date()));  
    }  
      
      
    static class Worker extends Thread{  
        String workerName;   
        int workTime;  
        CountDownLatch latch;  
        public Worker(String workerName ,int workTime ,CountDownLatch latch){  
             this.workerName=workerName;  
             this.workTime=workTime;  
             this.latch=latch;  
        }  
        public void run(){  
            System.out.println("Worker "+workerName+" do work begin at "+sdf.format(new Date()));  
            doWork();//工作了  
            System.out.println("Worker "+workerName+" do work complete at "+sdf.format(new Date()));  
            latch.countDown();//工人完成工作,计数器减一  
  
        }  
          
        private void doWork(){  
            try {  
                Thread.sleep(workTime);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
      
       
}  

输出:

Worker zhang san do work begin at 2011-04-14 11:05:11
Worker li si do work begin at 2011-04-14 11:05:11
Worker zhang san do work complete at 2011-04-14 11:05:16
Worker li si do work complete at 2011-04-14 11:05:19
all work done at 2011-04-14 11:05:19

java多线程之Callable代码示例

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/** *//**
 * Callable 和 Future接口
 * Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。
 * Callable和Runnable有几点不同:
 * (1)Callable规定的方法是call(),而Runnable规定的方法是run().
 * (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
 * (3)call()方法可抛出异常,而run()方法是不能抛出异常的。
 * (4)运行Callable任务可拿到一个Future对象,
 * Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
 * 通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。
 */
public class CallableAndFuture {

/** *//**
     * 自定义一个任务类,实现Callable接口
     */
    public static class MyCallableClass implements Callable{
        // 标志位
        private int flag = 0;
        public MyCallableClass(int flag){
            this.flag = flag;
        }
        public String call() throws Exception{
            if (this.flag == 0){
                // 如果flag的值为0,则立即返回
                return "flag = 0";
            }
            if (this.flag == 1){
                // 如果flag的值为1,做一个无限循环
                try {
                    while (true) {
                        System.out.println("looping.");
                        Thread.sleep(2000);
                    }
                } catch (InterruptedException e) {
                    System.out.println("Interrupted");
                }
                return "false";
            } else {
                // falg不为0或者1,则抛出异常
                throw new Exception("Bad flag value!");
            }
        }
    }

public static void main(String[] args) {
        // 定义3个Callable类型的任务
        MyCallableClass task1 = new MyCallableClass(0);
        MyCallableClass task2 = new MyCallableClass(1);
        MyCallableClass task3 = new MyCallableClass(2);

// 创建一个执行任务的服务
        ExecutorService es = Executors.newFixedThreadPool(3);
        try {
            // 提交并执行任务,任务启动时返回了一个 Future对象,
            // 如果想得到任务执行的结果或者是异常可对这个Future对象进行操作
            Future future1 = es.submit(task1);
            // 获得第一个任务的结果,如果调用get方法,当前线程会等待任务执行完毕后才往下执行
            System.out.println("task1: " + future1.get());

Future future2 = es.submit(task2);
            // 等待5秒后,再停止第二个任务。因为第二个任务进行的是无限循环
            Thread.sleep(5000);
            System.out.println("task2 cancel: " + future2.cancel(true));

// 获取第三个任务的输出,因为执行第三个任务会引起异常
            // 所以下面的语句将引起异常的抛出
            Future future3 = es.submit(task3);
            System.out.println("task3: " + future3.get());
        } catch (Exception e){
            System.out.println(e.toString());
        }
        // 停止任务执行服务
        es.shutdownNow();
    }
}

Android 中 Socket的简单用法

Socket通常也称做”套接字“,用于描述IP地址和端口,废话不多说,它就是网络通信过程中端点的抽象表示。值得一提的是,Java在包java.net中提供了两个类Socket和ServerSocket,分别用来表示双向连接的客户端和服务端。这是两个封装得非常好的类,使用起来很方便!

下面将首先创建一个SocketServer的类作为服务端如下,该服务端实现了多线程机制,可以在特定端口处监听多个客户请求,一旦有客户请求,Server总是会创建一个服务纯种来服务新来的客户,而自己继续监听。程序中accept()是一个阻塞函数,所谓阻塞性方法就是说该方法被调用后将等待客户的请求,直到有一个客户启动并请求连接到相同的端口,然后accept()返回一个对应于客户的Socket。这时,客户方和服务方都建立了用于通信的Socket,接下来就是由各个Socket分别打开各自的输入、输出流。

SocketServer类,服务器实现:

package HA.Socket;

import java.io.*;
import java.net.*;

 public class SocketServer {
    
    ServerSocket sever;
    
    public SocketServer(int port){
        try{
            sever = new ServerSocket(port);
        }catch(IOException e){
            e.printStackTrace();
        }
    }
    
    public void beginListen(){
        while(true){
            try{
                final Socket socket = sever.accept();
                
                new Thread(new Runnable(){
                    public void run(){
                        BufferedReader in;
                        try{
                            in = new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
                            PrintWriter out = new PrintWriter(socket.getOutputStream());
                            while (!socket.isClosed()){
                                String str;
                                str = in.readLine();
                                out.println("Hello!world!! " + str);
                                out.flush();
                                if (str == null || str.equals("end"))
                                    break;
                                System.out.println(str);
                            }
                            socket.close();
                        }catch(IOException e){
                            e.printStackTrace();
                        }
                    }
                }).start();
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}

SocketClient类,客户端实现:

package HA.Socket;

import java.io.*;
import java.net.*;

 public class SocketClient {
    static Socket client;
    
    public SocketClient(String site, int port){
        try{
            client = new Socket(site,port);
            System.out.println("Client is created! site:"+site+" port:"+port);
        }catch (UnknownHostException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
    
    public String sendMsg(String msg){
        try{
            BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
            PrintWriter out = new PrintWriter(client.getOutputStream());
            out.println(msg);
            out.flush();
            return in.readLine();
        }catch(IOException e){
            e.printStackTrace();
        }
        return "";
    }
    public void closeSocket(){
        try{
            client.close();
        }catch(IOException e){
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws Exception{
        
    }

}

接下来就是来测试Socket通信了!
先运行TestSocketServer类,打开服务端,在12345端口处监听!

package HA.Socket;

 public class TestSocketServer {
    
    public static void main(String[] argvs){
        SocketServer server = new SocketServer(12345);
        server.beginListen();
    }
}

再运行TestSocketClient类:

package HA.Socket;

 public class TestSocketClient {

    public static void main(String[] args){
        
        SocketClient client = new SocketClient("127.0.0.1",12345);
        System.out.println(client.sendMsg("nimei1"));
        client.closeSocket();
        
        SocketClient client1 = new SocketClient("127.0.0.1",12345);
        System.out.println(client1.sendMsg("nimei1111"));
        client1.closeSocket();
        
        SocketClient client11 = new SocketClient("127.0.0.1",12345);
        System.out.println(client11.sendMsg("nimei11111111"));
        client11.closeSocket();
        
        SocketClient client111 = new SocketClient("127.0.0.1",12345);
        System.out.println(client111.sendMsg("nimei11111111111111111"));
        client111.closeSocket();
        
    }
}

输出结果如下:

服务端:

Client is created! site:127.0.0.1 port:12345
Hello!world!! nimei1
Client is created! site:127.0.0.1 port:12345
Hello!world!! nimei1111
Client is created! site:127.0.0.1 port:12345
Hello!world!! nimei11111111
Client is created! site:127.0.0.1 port:12345
Hello!world!! nimei11111111111111111

客户端:

nimei1
nimei1111
nimei11111111
nimei11111111111111111