String对象的存储、拼接和比较 string对象存在哪里
bigegpt 2024-10-13 01:25 12 浏览
- 一、String类型介绍
- 二、String类型的存储
- 虚拟机运行时内存(JDK1.8以后)常量池String对象的创建
- 三、String类型的拼接
- 通过concat方法拼接通过+号拼接
- 四、字符串的比较
- equals方法"=="运算符
( 以下源码都基于jdk11)
一、String类型介绍
String类型是引用数据类型,表示字符串类型。String底层使用byte[]数组来存储char[]数组。(JDK1.9及以后的版本,JDK1.9之前是使用char数组保存,1.9为了节省空间,开始使用byte数组保存)
@Stable
private final byte[] value;//定义byte数组用于存储构造函数传进的char数组,最下方的代码中有用到。
12
从上方的代码中可以看出,String用于保存数据的数组是private、final的,因此String类型是不可变的。
//String的构造函数
public String(char value[]) { this(value, 0, value.length, null);//调用另一个构造函数,代码在下方 }
12
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
if (COMPACT_STRINGS) {
byte[] val = StringUTF16.compress(value, off, len);
if (val != null) {
this.value = val;
this.coder = LATIN1;
return;
}
}
this.coder = UTF16;
this.value = StringUTF16.toBytes(value, off, len);
}
1234567891011121314151617
二、String类型的存储
虚拟机运行时内存(JDK1.8以后)
JVM内存中与String类型存储相关的结构主要有堆和虚拟机栈。
常量池
常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = "java"这种申明方式;当然也可扩充,执行器产生的常量也会放入常量池,故认为常量池是JVM的一块特殊的内存空间。
通过常量池的使用String实现了多个引用指向同一个常量池中的对象,大大的节省了内存空间的开销。
JDK1.8之后,常量池存放于JVM运行时内存中的堆内存中。
String对象的创建
主要有以下两种创建String对象的方式
1、String a="abcd";
使用这种创建方式时,若常量池中不存在"abcd"这个String对象,则会创建2个对象:在常量池中创建String类型的对象"abcd",常量池位于上图所示的堆内存中、在栈中创建引用a保存"abcd"的内存地址,从而指向常量池中的"abcd"对象,栈既上图所示的虚拟机栈。
若常量池中已存在"abcd"对象,则会直接返回这个对象,只在栈中创建一个引用a指向该对象。
2、String a=new String("abcd");
使用这种创建方式时,若常量池中不存在值为"abcd"的String对象,则会先在常量池中创建一个值为“abcd”的String对象,然后将其复制一份到堆内存中(常量池外,堆内存中,地址不同),然后在栈中创建一个引用a保存"abcd"在堆中的地址,从而指向堆内存中的该对象。共创建了三个对象
若常量池重已存在对象“abcd”,则省去在常量池中创建对象的这一步,共创建两个对象。
三、String类型的拼接
通过concat方法拼接
String a="a";
String b="b";
System.out.println(a.concat(b));//通过a对象concat方法连接b对象,结果为"ab"
123
下面来看看concat方法的源码
public String concat(String str) {
int olen = str.length();
if (olen == 0) {
return this;
}
if (coder() == str.coder()) {//coder来标识字符串的编码格式是LATIN1还是UTF16,若两个字符串的编码格式相等,则不用进行编码格式转换
byte[] val = this.value;
byte[] oval = str.value;
int len = val.length + oval.length;//拼接后字符串的长度
byte[] buf = Arrays.copyOf(val, len);//创建一个新数组存放拼接后的字符串
System.arraycopy(oval, 0, buf, val.length, oval.length);
return new String(buf, coder);
}
int len = length();
byte[] buf = StringUTF16.newBytesFor(len + olen);
getBytes(buf, 0, UTF16);
str.getBytes(buf, len, UTF16);
return new String(buf, UTF16);
}
12345678910111213141516171819
从concat源码中容易得出,concat方法通过创建一个长度为两字符串长度之和的byte数组来存放两字符串,然后将两个字符串依次放入数组中,实现了字符串的拼接。
至于为什么使用byte数组,上面讲过,String类型底层使用byte数组存储char数组,因此concat使用byte数组来存储字符串,如果用其他类型的数组就要进行类型转换。
注意:concat方法并不会对原对象进行改变,而是会返回一个新的String对象。
通过+号拼接
通过+号的拼接主要分为两种情况:有字符串变量(既在栈中创建的引用)参与的拼接,无字符串变量参与,只有字符串常量(常量池中的String对象)参与的拼接。
有字符串变量(既在栈中创建的引用)参与的拼接:
在网上找了下有字符串变量参与+号拼接的实现原理,大部分说的都是:
运行时, 两个字符串str1, str2的拼接首先会调用String.valueOf(obj),这个Obj为str1,而String.valueOf(Obj)中的实现是return obj ==null ? “null” : obj.toString()。
然后产生StringBuilder, 调用的StringBuilder(str1)构造方法, 把StringBuilder初始化,长度为str1.length()+16,并且调用append(str1)!接下来调用StringBuilder.append(str2), 把第二个字符串拼接进去, 然后调用StringBuilder.toString返回结果。
下面我就得从底层中看看它们是如何实现拼接的。
打以下代码:
public class Test{
public static void main(String[] args){
String str1 = "111111";
String str2 = "222222";
String str = str1 + str2;
System.out.println(str);
}
}
12345678
然后进入dos界面,在dos界面中进入文件所在文件夹,使用javac Test.java命令生成字节码,再使用javap -verbose Test命令进行反编译,可以看到以下结果。(JDK1.9及以后的版本才能看到如下结果,JDK1.8及以前的可参考这篇博文:Java String + 拼接字符串原理)
容易看出以下两行代码 ,对应的是String str = str1 + str2;语句
8: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: astore_3
12
动态指令invokedynamic指令会调用makeConcatWithConstants方法进行字符串的连接。
该方法位于java.lang.invoke.StringConcatFactory类中。
下面是源码,容易看出这个方法里如果没出问题,是直接调用doStringConcat方法
public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup,
String name,
MethodType concatType,
String recipe,
Object... constants) throws StringConcatException {
if (DEBUG) {
System.out.println("StringConcatFactory " + STRATEGY + " is here for " + concatType + ", {" + recipe + "}, " + Arrays.toString(constants));
}
return doStringConcat(lookup, name, concatType, false, recipe, constants);
}
1234567891011
下面是doStringConcat方法的部分源码,多的就省略了。可以看到返回值中,mh调用asType方法适配得到MethodHandle对象,返回值的逻辑就是单纯的返回一个结果,字符串拼接是在mh对象生成的时候进行的,也就是在generate方法中进行。
private static CallSite doStringConcat(MethodHandles.Lookup lookup,
String name,
MethodType concatType,
boolean generateRecipe,
String recipe,
Object... constants) throws StringConcatException {
......
MethodHandle mh;
if (CACHE_ENABLE) {
Key key = new Key(className, mt, rec);
mh = CACHE.get(key);
if (mh == null) {
mh = generate(lookup, className, mt, rec);
CACHE.put(key, mh);
}
} else {
mh = generate(lookup, className, mt, rec);
}
return new ConstantCallSite(mh.asType(concatType));
12345678910111213141516171819
下面是generate方法的源码
private static MethodHandle generate(Lookup lookup, String className, MethodType mt, Recipe recipe) throws StringConcatException {
try {
switch (STRATEGY) {
case BC_SB:
return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.DEFAULT);
case BC_SB_SIZED:
return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.SIZED);
case BC_SB_SIZED_EXACT:
return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.SIZED_EXACT);
case MH_SB_SIZED:
return MethodHandleStringBuilderStrategy.generate(mt, recipe, Mode.SIZED);
case MH_SB_SIZED_EXACT:
return MethodHandleStringBuilderStrategy.generate(mt, recipe, Mode.SIZED_EXACT);
case MH_INLINE_SIZED_EXACT:
return MethodHandleInlineCopyStrategy.generate(mt, recipe);
default:
throw new StringConcatException("Concatenation strategy " + STRATEGY + " is not implemented");
}
} catch (Error | StringConcatException e) {
// Pass through any error or existing StringConcatException
throw e;
} catch (Throwable t) {
throw new StringConcatException("Generator failed", t);
}
}
12345678910111213141516171819202122232425
generate方法通过不同的STRATEGY(策略)值来调用不同对象的generate方法。那么,接下来看看Strategy类型,对文档中的英文进行了一些简单的翻译。
private enum Strategy {
/**
* 字节码生成器,调用{@link java.lang.StringBuilder}.
*/
BC_SB,
/**
* 字节码生成器,调用 {@link java.lang.StringBuilder};
* 但要估计所需的存储空间。
*/
BC_SB_SIZED,
/**
* 字节码生成器,调用 {@link java.lang.StringBuilder};
* 但需要精确地计算所需的存储空间。
*/
BC_SB_SIZED_EXACT,
/**
*基于MethodHandle的生成器,最终调用 {@link java.lang.StringBuilder}.
* 此策略还尝试估计所需的存储空间。
*/
MH_SB_SIZED,
/**
* 基于MethodHandle的生成器,最终调用 {@link java.lang.StringBuilder}.
* 此策略也需要准确地计算所需的存储空间。
*/
MH_SB_SIZED_EXACT,
/**
* 基于MethodHandle的生成器, 基于MethodHandle的生成器,从参数构造自己的byte[]数组。它精确地计算所需的存储空间。
*/
MH_INLINE_SIZED_EXACT
}
1234567891011121314151617181920212223242526272829303132333435
主要就是针对不同的情况,使用不同的策略值,共六种策略,从而能调用适用于当前情况的generate方法。上面五种策略的实现都是基于StringBuilder。
接下来以上面的BytecodeStringBuilderStrategy中的generate方法为例,来具体看一看是怎么实现字符串拼接的(套了一堆娃,终于到正题了)
首先,是调用String的ValueOf()方法
if (mode.isExact()) {
/*在精确模式下,我们需要将所有参数转换为字符串表示,因为这允许精确计算它们的字符串大小。我们不能在这里使用私有的原语方法,因此我们也需要转换它们。
我们还记录了转换结果中保证为非null的参数。字符串.valueOf是否为我们检查空。唯一极端的情况是字符串.valueOf(对象)返回null本身。
此外,如果发生任何转换,则传入参数中的插槽索引不等于最终的本地映射。唯一可能会中断的情况是将2-slot long/double转换为1-slot时。因此,我们可以跟踪修改过的偏移,因为没有转换可以覆盖即将到来的参数。
*/
int off = 0;
int modOff = 0;
for (int c = 0; c < arr.length; c++) {
Class<?> cl = arr[c];
if (cl == String.class) {
if (off != modOff) {
mv.visitIntInsn(getLoadOpcode(cl), off);
mv.visitIntInsn(ASTORE, modOff);
}
} else {
mv.visitIntInsn(getLoadOpcode(cl), off);
mv.visitMethodInsn(
INVOKESTATIC,
"java/lang/String",
"valueOf",
getStringValueOfDesc(cl),
false
);
mv.visitIntInsn(ASTORE, modOff);
arr[c] = String.class;
guaranteedNonNull[c] = cl.isPrimitive();
}
off += getParameterSize(cl);
modOff += getParameterSize(String.class);
}
}
if (mode.isSized()) {
/*在调整大小模式(包括精确模式)下操作时,让StringBuilder附加链看起来熟悉优化StringConcat是有意义的。为此,我们需要尽早进行空检查,而不是使附加链形状更简单。*/
int off = 0;
for (RecipeElement el : recipe.getElements()) {
switch (el.getTag()) {
case TAG_CONST:
// Guaranteed non-null, no null check required.
break;
case TAG_ARG:
// Null-checks are needed only for String arguments, and when a previous stage
// did not do implicit null-checks. If a String is null, we eagerly replace it
// with "null" constant. Note, we omit Objects here, because we don't call
// .length() on them down below.
int ac = el.getArgPos();
Class<?> cl = arr[ac];
if (cl == String.class && !guaranteedNonNull[ac]) {
Label l0 = new Label();
mv.visitIntInsn(ALOAD, off);
mv.visitJumpInsn(IFNONNULL, l0);
mv.visitLdcInsn("null");
mv.visitIntInsn(ASTORE, off);
mv.visitLabel(l0);
}
off += getParameterSize(cl);
break;
default:
throw new StringConcatException("Unhandled tag: " + el.getTag());
}
}
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
然后是生成StringBuilder对象并使用append方法依次将字符串加入
// 准备StringBuilder实例
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
if (mode.isSized()) {
/*大小模式要求我们遍历参数,并估计最终长度。
在精确模式下,这将仅在字符串上操作。此代码将在堆栈上累积最终长度。*/
int len = 0;
int off = 0;
mv.visitInsn(ICONST_0);
for (RecipeElement el : recipe.getElements()) {
switch (el.getTag()) {
case TAG_CONST:
len += el.getValue().length();
break;
case TAG_ARG:
/*
如果一个参数是String,那么我们可以对它调用.length()。大小/精确模式为我们转换了参数。
如果一个参数是原始的,我们可以猜测它的字符串表示大小。
*/
Class<?> cl = arr[el.getArgPos()];
if (cl == String.class) {
mv.visitIntInsn(ALOAD, off);
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/lang/String",
"length",
"()",
false
);
mv.visitInsn(IADD);
} else if (cl.isPrimitive()) {
len += estimateSize(cl);
}
off += getParameterSize(cl);
break;
default:
throw new StringConcatException("Unhandled tag: " + el.getTag());
}
}
// 常数具有非零长度,混合
if (len > 0) {
iconst(mv, len);
mv.visitInsn(IADD);
}
mv.visitMethodInsn(
INVOKESPECIAL,
"java/lang/StringBuilder",
"<init>",
"(I)V",
false
);
} else {
mv.visitMethodInsn(
INVOKESPECIAL,
"java/lang/StringBuilder",
"<init>",
"()V",
false
);
}
// 此时,堆栈上有一个空的StringBuilder,用.append调用填充它。
{
int off = 0;
for (RecipeElement el : recipe.getElements()) {
String desc;
switch (el.getTag()) {
case TAG_CONST:
mv.visitLdcInsn(el.getValue());
desc = getSBAppendDesc(String.class);
break;
case TAG_ARG:
Class<?> cl = arr[el.getArgPos()];
mv.visitVarInsn(getLoadOpcode(cl), off);
off += getParameterSize(cl);
desc = getSBAppendDesc(cl);
break;
default:
throw new StringConcatException("Unhandled tag: " + el.getTag());
}
mv.visitMethodInsn(//调用append方法
INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
desc,
false
);
}
}
if (DEBUG && mode.isExact()) {
/*
Exactness checks compare the final StringBuilder.capacity() with a resulting
String.length(). If these values disagree, that means StringBuilder had to perform
storage trimming, which defeats the purpose of exact strategies.
*/
/*
The logic for this check is as follows:
Stack before: Op:
(SB) dup, dup
(SB, SB, SB) capacity()
(int, SB, SB) swap
(SB, int, SB) toString()
(S, int, SB) length()
(int, int, SB) if_icmpeq
(SB) <end>
Note that it leaves the same StringBuilder on exit, like the one on enter.
*/
mv.visitInsn(DUP);
mv.visitInsn(DUP);
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/lang/StringBuilder",
"capacity",
"()I",
false
);
mv.visitInsn(SWAP);
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/lang/StringBuilder",
"toString",
"()Ljava/lang/String;",
false
);
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/lang/String",
"length",
"()I",
false
);
Label l0 = new Label();
mv.visitJumpInsn(IF_ICMPEQ, l0);
mv.visitTypeInsn(NEW, "java/lang/AssertionError");
mv.visitInsn(DUP);
mv.visitLdcInsn("Failed exactness check");
mv.visitMethodInsn(INVOKESPECIAL,
"java/lang/AssertionError",
"<init>",
"(Ljava/lang/Object;)V",
false);
mv.visitInsn(ATHROW);
mv.visitLabel(l0);
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
下面是该方法中末尾的几行代码,主要就是调用StringBuilder的toString()方法并返回该方法得到的对象。
mv.visitMethodInsn(//调用StringBuilder的toString()方法
INVOKEVIRTUAL,
"java/lang/StringBuilder",
"toString",
"()Ljava/lang/String;",
false
);
mv.visitInsn(ARETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
cw.visitEnd();
byte[] classBytes = cw.toByteArray();
try {
Class<?> hostClass = lookup.lookupClass();
Class<?> innerClass = UNSAFE.defineAnonymousClass(hostClass, classBytes, null);
UNSAFE.ensureClassInitialized(innerClass);
dumpIfEnabled(innerClass.getName(), classBytes);
return Lookup.IMPL_LOOKUP.findStatic(innerClass, METHOD_NAME, args);
} catch (Exception e) {
dumpIfEnabled(className + "$FAILED", classBytes);
throw new StringConcatException("Exception while spinning the class", e);
}
12345678910111213141516171819202122232425
所以,总结一下,有字符串变量参与拼接的过程:首先调用String的ValueOf方法,然后是生成一个StringBuilder对象并将用append方法将两个字符串依次加入,然后返回StringBuilder的toString()方法。
只有字符串常量(常量池中的String对象)参与的拼接:例如:String a=“ab”+cd;这种拼接,在编译时,编译器会自动将a变量编译为"abcd"
例如以下代码:
public class Test2{
public static void main(String[] args){
String str = “12”+“34”;
System.out.println(str);
}
}
用上述的方法同样查看反编译代码
可以看到编译器直接将str字符串编译为了”1234“.
四、字符串的比较
equals方法
String类型的对象有个equals方法,用于比较两个String对象的值是否相等。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {//判断编码格式是否相等
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
//根据编码格式调用不同的equals方法
}
}
return false;
}
1234567891011121314
下面是StringLatin1对象(以Latin1为编码格式的String对象)的equals方法
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
123456789101112
然后是StringUTF16对象的equals方法
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
int len = value.length >> 1;
for (int i = 0; i < len; i++) {
if (getChar(value, i) != getChar(other, i)) {
return false;
}
}
return true;
}
return false;
}
12345678910111213
可以看出equals方法的实现逻辑就是通过for循环遍历保存字符串的byte数组,一位一位地进行判断。
"=="运算符
“==”运算符用于比较两个对象的地址是否相等。用在字符串比较时,需要注意"abcd"与new String(“abcd”)所返回的地址值不相同,具体看上方String对象的创建。
注意:上面我们具体分析了有字符串变量参与的连接预算,最后的对象是由StringBuilder的toString()方法返回的,而toString()方法底层是返回的是new String()对象,存储的地址是在堆中,而不是在常量池中。
@Override
@HotSpotIntrinsicCandidate
public String toString() {//StringBuilder对象的toString方法
// Create a copy, don't share the array
return isLatin1() ? StringLatin1.newString(value, 0, count)
: StringUTF16.newString(value, 0, count);
}
1234567
//StringLatin1对象的newString方法
public static String newString(byte[] val, int index, int len) {
return new String(Arrays.copyOfRange(val, index, index + len),
LATIN1);
}
12345
//StringUTF16的toString方法
public static String newString(byte[] val, int index, int len) {
if (String.COMPACT_STRINGS) {
byte[] buf = compress(val, index, len);
if (buf != null) {
return new String(buf, LATIN1);
}
}
int last = index + len;
return new String(Arrays.copyOfRange(val, index << 1, last << 1), UTF16);
}
相关推荐
- 得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践
-
一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...
- warm-flow新春版:网关直连和流程图重构
-
本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...
- 扣子空间体验报告
-
在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...
- spider-flow:开源的可视化方式定义爬虫方案
-
spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...
- solon-flow 你好世界!
-
solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...
- 新一代开源爬虫平台:SpiderFlow
-
SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...
- 通过 SQL 训练机器学习模型的引擎
-
关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...
- 鼠须管输入法rime for Mac
-
鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...
- Go语言 1.20 版本正式发布:新版详细介绍
-
Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...
- iOS 10平台SpriteKit新特性之Tile Maps(上)
-
简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...
- 程序员简历例句—范例Java、Python、C++模板
-
个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...
- Telerik UI for iOS Q3 2015正式发布
-
近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...
- ios使用ijkplayer+nginx进行视频直播
-
上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...
- IOS技术分享|iOS快速生成开发文档(一)
-
前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...
- macOS下配置VS Code C++开发环境
-
本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...
- 一周热门
- 最近发表
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- resize函数 (64)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- mybatis大于等于 (64)
- xcode-select (66)
- httperror403.14-forbidden (63)
- logstashinput (65)
- hadoop端口 (65)
- dockernetworkconnect (63)
- esxi7 (63)
- vue阻止冒泡 (67)
- c#for循环 (63)
- oracle时间戳转换日期 (64)
- jquery跨域 (68)
- php写入文件 (73)
- java大写转小写 (63)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)