热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

将PEBuilder转换为DIBooter.sh,集成DI工具至启动层(5):实现离线镜像引导安装

本文探讨了将PEBuilder转换为DIBooter.sh的方法,重点介绍了如何将DI工具集成到启动层,实现离线镜像引导安装。通过使用DD命令替代传统的grub-install工具,实现了GRUB的离线安装。此外,还详细解析了bootice工具的工作原理及其在该过程中的应用,确保系统在无网络环境下也能顺利引导和安装。

本文关键字:通过DD安装grub代替grub-install,grub离线安装技术,restore grub2 installation using dd,bootice工具原理

在《pebuilder变成dibuilder.sh,将di tools集入boot层4》的中间,我们提到过暂代方案的思路,这种思路的第一步,就是用grub作通用booter代替难实现的虚拟booter,为此我们在上文提出了初步的在dibuilder.sh中为离线镜像实现离线安装grub2的代码,----- 在整个《pebuilder变成dibuilder.sh,将di tools集入boot层》系列中,如果说(4)是(2)的增强继续,那么本文(5)就是对(3)的增强继续。前面准备好了所有debian dists(di,pve),我们最终的目的,是要在这些dists的基础上组装minstackos,并使dibuilder.sh中的生成raw镜像/localinstall生效,实现最终能安装在mbp实机上测试,这是后话


(为什么是di,pve这二者,因为我感觉debian installer的装机方案和pve的bearmetal hypervisor+lxc as app container越来越顺眼了,毕竟,用grub+dipe+pve作为多区段镜像第一个分区完全可以替换实现虚拟boot+payload的伪不死boot效果(这也可以理解为"在一块flash上装一个类似di的pe":类似白群将synope写在nand rom中),而debian pve也可以代替vm hypervisor inside boot to replace libos这些复杂目前还没用起来的东西),况且,我们也有dibuilder.sh对接),最后,过度虚拟化也真的不好用,这也是用grub代替虚拟boot作通用boot,及用lxc代替docker这种不好instant commit的原生云部署容器的又一原因。这些暂代方案的思路,贯穿了整个《minstackos的实践部分》


剩下的问题是考虑更好的dibuilder.sh对接,比如对接上文,在dibuilder.sh中继续实现为离线镜像生成grub2引导的问题提供方案。

好了,动工


测试研究mbr脚本定制,得到core_img

为什么要得到一个core_img,这个问题从何而来?先来看下grub2这个东西,它就像deb的动态control文件,我们要把离线镜像集成grub,必须要得到一个为此镜像预安装了的grub2,这绝非拷贝/boot/grub下的文件那么简单,


关于grub2 mbr安装

grub-install只是一个脚本,内部真正执行工作的是grub-mkimage和grub-setup,grub2-install 脚本的工作包括调用grub2-probe扫描计算机并收集磁盘和分区信息,调用grub2-mkimage构建一个基于/boot/grub下脚手架文件之上的新的new.img,即core.img,grub-mkimage的过程形如grub-mkimage -d x86_64-efi -p /boot/grub -o grubx64.efi -O x86_64-efi acpi all_video bitmap bitmap_scale blocklist exfat expr ....一般地,仅 fshelp ext2 part_msdos biosdisk就够了,最后grub2-setup把grub的boot.img写入MBR中,把core.img写进设备的第一个扇区

在BIOS平台下,boot.img是grub启动的第一个img文件,它被写入到MBR中或分区的boot sector中,因为boot sector的大小是512字节,所以该img文件的大小也是512字节。boot.img唯一的作用是读取属于core.img的第一个扇区并跳转到它身上,将控制权交给该扇区的img。由于体积大小的限制,boot.img无法理解文件系统的结构,因此grub2-install将会把core.img的位置硬编码到boot.img中(这并不一定就是物理扇区号,可能是虚拟offset),这样就一定能找到core.img的位置。

关于bios下写入booter的复杂性,其实我们在装黑苹果(1)处理clover的boot的biosdisk时候也遇到了。还有上次《普化群晖将其改造成正常磁盘布局及编译源码打开kernel message》说到grub-install在/dev/loop上失效,可能是在host上dev map string没有绑定https://itectec.com/superuser/how-to-install-grub-into-an-img-file/。可能需要mkdir -p /mnt/boot/grub,cat 进 /mnt/boot/grub/device.map:(hd0) /dev/loop0,(hd0,1) /dev/loop1等正确内容


这也就是说,(以上这是一个与具体磁盘绑定的动态配置过程)磁盘上的/boot/grub/i386-pc/*.img的这些文件全是脚手架文件,本质起作用的是具体磁盘环境下已经被通过grub-install嵌写入磁盘结构,我们的工作就是要将这个动态过程,变成静态的通过可恢复文件方式就能完成安装的grub-install-static。却要求能配合我们稍后以自由方式生成的1g的boot区一起工作。(而且这又有限制和条件:在dibuilder.sh中我们基于加入了osx考虑不打算用grub-install)接下来你会看到,正确的boot.img容易生成,而core.img却难于得到。

所幸我们得到了一个脚本:

bootinfoscript,https://github.com/arvidjaar/bootinfoscript,它会显示一个主输出,同时在/tmp下保留大量有价值的临时文件(如解包的core.img),及其它输出(如Trash),它给出了一些bash算法,很为珍贵,以下是二个重要函数:

grub2_read_blocklist ()
这个函数整理hdd上存在于旧core.img->第一个sector中的diskboot.img未端部分->里的fragment信息,收集重组为新core.img上的fragments并写入,未来供grub2_info用
参数中的hdd也可能是硬盘(full core.img from blocklists,因为多个分区上都有可能有引导目录和core.img)也可能是单个普通core.img文件,后者情形我们不考虑,因为我们基本都是在读写磁盘嵌入区的这些文件区段,而不是文件系统中的这些文件。
grub2_info ()
这个函数用上一步得到的新core.img fragment信息,进一步,求得boot.img里core.img里的位置,以及更多信息(core.img中的文件夹信息,模块,配置文件),完成整个grub解析工作
stage1即是boot.img或/dev/sda mbr区,grub2的core.img是stage2,这种说法引用了grub1的stage1,2

但是bootinfoscript的实现比较理想化,依赖linux,比如它用到filefrag(apt-get install binutils for strings command),应该换成用string代替加判断才更为portable。

无论如何,我们从这个脚本和它的输出中希望得到的帮助是:它可以用于为我们的镜像输出core_img。供下一部分直接dd,而实际上,它也的确可以办到:我们可以在生成的虚拟镜像中安装一个linux,使之形成一个能启动的/dev/sda或其它需要的分区结构,在其中测试bootinfoscript,提取/tmp下的core_img。


2,实际修改dibuilder.sh并得到boot.img

得到core_img,我们接下来修改dibuilder.sh的对接部分,首先修改dibuilder.sh local remastering and install支持部分,修改得到一套较为规则的/tmp/tmpdown->/tmp/tmpremastering->/tmp/tmpmnt target or /boot target的文件夹逻辑结构:

首先,原来prepare misc中的net,grub部分中的grub归入接下来的remastering all up部分,整个脚本只有三部了,这是新的整个第三部分,


echo -e "\n \033[36m # Remastering all up... \033[0m \n"
remasteringdir='/tmp/tmpremastering';
[[ -d $remasteringdir ]] && rm -rf $remasteringdir;
sleep 2s && echo -en "\runpacking grub files ..."
export tmpMNT=/tmp/tmpmnt
[[ "$tmpMODE" == '1' && "$tmpTARGET" == 'minstackos' ]] && {

prepareimg() {
tmpDEV=$(mount | grep "$tmpMNT" | awk '{print $1}')
[ -z "$tmpDEV" ] && {
rm -rf /tmp/tmpimage.raw
dd if=/dev/zero of=/tmp/tmpimage.raw bs=1024 count=1048576 >/dev/null 2>&1
[[ "$tmpPLAT" == "0" ]] && tmpDEV=`losetup -fP --show /tmp/tmpimage.raw | awk '{print $1}'` || tmpDEV=`hdiutil attach -imagekey diskimage-class=CRawDiskImage -nomount /tmp/tmpimage.raw | awk '{print $1}'`
注意osx与linux在工具能力上的差别和分区控制方面的差别。通常我们必须在mbr和第一个分区开始前预留一定空间放core.img,一般grub-install会把core.img写入到第一个分区,但是我们放在这个预留一些空白区以更方便控制和定制,比如刻意向我们的离线grub2合成方向靠拢
[ -n "$tmpDEV" ] && {
[[ "$tmpPLAT" == "0" ]] && parted -s "$tmpDEV" mktable msdos >/dev/null 2>&1 && parted -s "$tmpDEV" mkpart primary ext3 2048s 100% >/dev/null 2>&1 && parted -s "$tmpDEV" set 1 boot on >/dev/null 2>&1 && mkfs.ext3 "$tmpDEV"p1 >/dev/null 2>&1
[[ "$tmpPLAT" == "1" ]] && diskutil partitionDisk "$tmpDEV" MBR FREE %noformat% 1048576 "MS-DOS FAT16" TMPVOL 0 >/dev/null 2>&1
}
[ ! -d "$tmpMNT" ] && [[ "$tmpPLAT" == "0" ]] && mkdir "$tmpMNT" && mount "$tmpDEV"p1 "$tmpMNT" || tmpMNT=/Volumes/TMPVOL;[[ ! -d /tmp/tmpmnt ]] && ln -s /Volumes/TMPVOL /tmp/tmpmnt
#echo "tmpdev is:""$tmpDEV"
#echo "tmpmnt is:""$tmpMNT"
#echo "- before installing/remastering grub,unzip it"
mkdir -p /tmp/tmpremastering/grub
tar -xf /tmp/tmpdown/debian/dists/jessie/main/binary-amd64/grub.gz -C /tmp/tmpremastering/grub
(这里放接下来得到boot.img和离线镜像写grub的过程,这里的内容)
进生成core_img所在系统,针对/dev/sda得到其mbr(不要用脚本手架文件中的boot.img,开头字节是空的),得到原始字串并处理:sudo hexdump -v -n 446 /dev/sda | (手动处理掉换行,索引,并删掉最后一行) | sed -e "s/ //g" | sed -e "s/\([0-9a-z][0-9a-z]\)/\1 /g" | sed -e "s/ /\\\x/g | sed -E 's/(\\x..)(\\x..)/\2\1/g'"
(You can use xxd to dump binary files just like hexdump and od, but you can also use it to do the reverse: turn a hex dump back into binary. If y)
得到下列待处理字符串命名为grub202:
grub202='\xeb\x63\x90\x10\x8e\xd0\xbc\x00\xb0\xb8\x00\x00\x8e\xd8\x8e\xc0\xfb\xbe\x00\x7c\xbf\x00\x06\xb9\x00\x02\xf3\xa4\xea\x21\x06\x00\x00\xbe\xbe\x07\x38\x04\x75\x0b\x83\xc6\x10\x81\xfe\xfe\x07\x75\xf3\xeb\x16\xb4\x02\xb0\x01\xbb\x00\x7c\xb2\x80\x8a\x74\x01\x8b\x4c\x02\xcd\x13\xea\x00\x7c\x00\x00\xeb\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\xff\xfa\x90\x90\xf6\xc2\x80\x74\x05\xf6\xc2\x70\x74\x02\xb2\x80\xea\x79\x7c\x00\x00\x31\xc0\x8e\xd8\x8e\xd0\xbc\x00\x20\xfb\xa0\x64\x7c\x3c\xff\x74\x02\x88\xc2\x52\xbb\x17\x04\xf6\x07\x03\x74\x06\xbe\x88\x7d\xe8\x17\x01\xbe\x05\x7c\xb4\x41\xbb\xaa\x55\xcd\x13\x5a\x52\x72\x3d\x81\xfb\x55\xaa\x75\x37\x83\xe1\x01\x74\x32\x31\xc0\x89\x44\x04\x40\x88\x44\xff\x89\x44\x02\xc7\x04\x10\x00\x66\x8b\x1e\x5c\x7c\x66\x89\x5c\x08\x66\x8b\x1e\x60\x7c\x66\x89\x5c\x0c\xc7\x44\x06\x00\x70\xb4\x42\xcd\x13\x72\x05\xbb\x00\x70\xeb\x76\xb4\x08\xcd\x13\x73\x0d\x5a\x84\xd2\x0f\x83\xd0\x00\xbe\x93\x7d\xe9\x82\x00\x66\x0f\xb6\xc6\x88\x64\xff\x40\x66\x89\x44\x04\x0f\xb6\xd1\xc1\xe2\x02\x88\xe8\x88\xf4\x40\x89\x44\x08\x0f\xb6\xc2\xc0\xe8\x02\x66\x89\x04\x66\xa1\x60\x7c\x66\x09\xc0\x75\x4e\x66\xa1\x5c\x7c\x66\x31\xd2\x66\xf7\x34\x88\xd1\x31\xd2\x66\xf7\x74\x04\x3b\x44\x08\x7d\x37\xfe\xc1\x88\xc5\x30\xc0\xc1\xe8\x02\x08\xc1\x88\xd0\x5a\x88\xc6\xbb\x00\x70\x8e\xc3\x31\xdb\xb8\x01\x02\xcd\x13\x72\x1e\x8c\xc3\x60\x1e\xb9\x00\x01\x8e\xdb\x31\xf6\xbf\x00\x80\x8e\xc6\xfc\xf3\xa5\x1f\x61\xff\x26\x5a\x7c\xbe\x8e\x7d\xeb\x03\xbe\x9d\x7d\xe8\x34\x00\xbe\xa2\x7d\xe8\x2e\x00\xcd\x18\xeb\xfe\x47\x52\x55\x42\x20\x00\x47\x65\x6f\x6d\x00\x48\x61\x72\x64\x20\x44\x69\x73\x6b\x00\x52\x65\x61\x64\x00\x20\x45\x72\x72\x6f\x72\x0d\x0a\x00\xbb\x01\x00\xb4\x0e\xcd\x10\xac\x3c\x00\x75\xf4\xc3\xe1\x75\x0e\x09\x00\x00'
#(确认得到的显示正确,grub有特征码字串)
#printf $grub202 | hexdump -C
#虚拟offset号为1,无须处理
#printf $grub202 | hexdump -v -n 1 -s 92
#$grub202=`printf $grub202 | sed -e 's/\(x[0-9a-z][0-9a-z]\)/x01/93'`
#printf $grub202 | hexdump -v -n 1 -s 92
#添加可启动标识
#printf $grub202 | hexdump -v -n 1 -s 447
grub202+='\x80'
#printf $grub202 | hexdump -v -n 1 -s 447
diskutil unmountDisk "$tmpDEV"
printf $grub202 | dd of="$tmpDEV" bs=447 count=1
#确认已写入
hexdump -v -n 512 -C $tmpDEV
#core.img无特征码字串,前面10个字节判断
dd if=/tmp/tmpremastering/grub/i386-pc/core.img of="$tmpDEV" seek=512 bs=25760 count=1 cOnv=notrunc
hexdump -v -n 10 -C /tmp/tmpremastering/grub/i386-pc/core.img
hexdump -v -s 512 -n 10 -C $tmpDEV
diskutil mountDisk "$tmpDEV"
正式脚本中记得把上面调试输出适当注释掉
}
sleep 2s && echo -en "[ \033[32m /tmp/tmpremastering/grub/* \033[0m ]"
# Automatically remove DISK on exit
[[ "$tmpPLAT" == "0" ]] && trap 'echo; echo "- Ejecting tmpdev disk"; umount "$tmpMNT" && losetup -d "$tmpDEV"' EXIT || trap 'echo; echo "- Ejecting tmpdev disk"; diskutil eject "$tmpDEV";rm -rf /tmp/tmpmnt' EXIT
}
prepareimg
}
原grub remastering部分
LoaderMode='0'
setInterfaceName='0'
setIPv6='0'
......
sed -i '$a\\n' /tmp/tmpremastering/grub/grub.new;
fi
sleep 2s && echo -en "[ \033[32m /tmp/tmpremastering/grub/grub.new \033[0m ]"
}
在这里才写入脚手架文件,一切都是为了让down,remastering,copying to target这三部分清淅分开
sleep 2s && echo -en "\nprocessing grub ......"
[[ -d /Volumes/TMPVOL ]] && [[ "$tmpPLAT" == "1" ]] && mkdir -p "$tmpMNT"/boot/grub && cp -a /tmp/tmpremastering/grub/* "$tmpMNT"/boot/grub
[[ "$tmpINSTANTWITHOUTVNC" == '0' ]] && {
GRUBPATCH='0';
if [[ "$LoaderMode" == "0" && "$tmpPLAT" != "1" ]]; then
......
[[ -f $GRUBDIR/grubenv ]] && sed -i 's/saved_entry/#saved_entry/g' $GRUBDIR/grubenv;
fi
开始remaster initrd部分的解压步骤
sleep 2s && echo -en "\nunpacking initrd ......"
mkdir -p /tmp/tmpremastering/initrd/usr/bin;
cd /tmp/tmpremastering/initrd;
.....
[[ "$tmpMODE" != "2" ]] && [[ "$tmpTARGET" == "minstackos" ]] && cp -f "$downdir/dists/jessie/main/binary-amd64/initrd.img" "/tmp/tmpremastering/$NewIMG"
[[ "$tmpPLAT" != "1" ]] && [[ "$tmpMODE" == "2" ]] && cp -f "$downdir/dists/jessie/main/debian-installer/binary-amd64/initrd.img" "/tmp/tmpremastering/$NewIMG"
......
[[ "$tmpPLAT" == '0' ]] && $UNCOMP >/dev/null 2>&1 || $UNCOMP >/dev/null 2>&1
[[ -f '/boot/firmware.cpio.gz' ]] && {
gzip -d >/dev/null 2>&1
}
sleep 2s && echo -en "\nprocessing initrd ...."
find $downdir/dists/jessie -type f \( -name *.deb -o -name *.ldeb \) | while read line; do line2=${line##*/};echo -en "\033[s \033[K [ \033[32m ${line2:0:40} \033[0m ] \033[u";[[ $(ar -t ${line} | grep -E -o data.tar.gz) == 'data.tar.gz' ]] && ar -p ${line} data.tar.gz |zcat|tar -xf - -C /tmp/tmpremastering/initrd || ar -p ${line} data.tar.xz |xzcat|tar -xf - -C /tmp/tmpremastering/initrd; done
......
sleep 2s && echo -en "\nmake a safe wget wrapper to inc --no-check-certificate"
mv /tmp/tmpremastering/initrd/usr/bin/wget /tmp/tmpremastering/initrd/usr/bin/wget2
cat >/tmp/tmpremastering/initrd/usr/bin/wget<#!/bin/sh
rdlkf() { [ -L "\$1" ] && (local lk="\$(readlink "\$1")"; local d="\$(dirname "$1")"; cd "\$d"; local l="\$(rdlkf "\$lk")"; ([[ "\$l" = /* ]] && echo "\$l" || echo "\$d/\$l")) || echo "\$1"; }
DIR="\$(dirname "\$(rdlkf "\$0")")"
exec /usr/bin/env wget2 --no-check-certificate "\$@"
EOF
chmod +x /tmp/tmpremastering/initrd/usr/bin/wget
fi
清楚的remasterin /copying分离
echo -en "\ncopying vmlinuz to the target/mnt ......"
[[ -d /boot ]] && [[ "$tmpMODE" != "2" ]] && [[ "$tmpTARGET" == "minstackos" ]] && cp -f "$downdir/dists/jessie/main/binary-amd64/vmlinuz" /boot/vmlinuz
[[ -d /boot ]] && [[ "$tmpPLAT" != "1" ]] && [[ "$tmpMODE" == "2" ]] && cp -f "$downdir/dists/jessie/main/debian-installer/binary-amd64/vmlinuz" /boot/vmlinuz
[[ -d /Volumes/TMPVOL ]] && [[ "$tmpPLAT" == "1" ]] && cp -f "$downdir/dists/jessie/main/binary-amd64/vmlinuz" /Volumes/TMPVOL/vmlinuz
(全程注意新脚本变动中的文件夹新命名变化)
除此之外,脚本其实在原先第一部分checkmirros部分把主体中检测1,2都注释了,直接返回提供的mirror url,相当于没检测,这是为了适应导致下到/tmp/tmpdown里的新结构debian文件夹的源镜像变化部分。

然后在osx上brew install qemu,cd /tmp/测试。

sudo qemu-system-x86_64 -machine pc-q35-2.4 -smp 4,cores=2 -cpu Penryn,kvm=off,vendor=GenuineIntel -m 4096 -device ide-hd,bus=ide.2,drive=MacHDD -drive id=MacHDD,if=none,format=raw,file=./buildpackage.raw -monitor stdio

成功进入。继续在脚本中处理硬盘镜像。



关于整合或分开使用这几个dists,除了前面“用di代替synope进行通用式dd安装镜像”“三个initramfs合为一个”等。还有一个思路,将di U盘安装代替multiboot unetbootin或ventoy,这样,比如用grub+dibuilder.sh面向的多img为基础打造这样一个类似的本地化多启U盘呢(不考虑它们的运行),我们可以以那个1G的boot区为启动区,mbr始终是现在最得到全面兼容的格式,数据区放各种img.gz。grub中写好各种安装条目。它就是dibuilder.sh的本地版了:在前面《云主机装黑果实践(1)》我们讲到过制造一个本地多启U盘的方案。它是以iso解压出的东西为内容基础的和efi方案为基础的,(后来我们发现了ventoy,它以iso为基础的,比如osx镜像全部弄为cdr/iso了,如经过hdiutil convert -format UDRW -o cdr.dmg common.iso之后,白苹果其实也可以用,不过这二者启动和对各种ISO的支持都不太稳定,继续深入一点,dibuilder.sh提供了一种更具替代性的稳定和兼容方案。



推荐阅读
  • 烤鸭|本文_Spring之Bean的生命周期详解
    烤鸭|本文_Spring之Bean的生命周期详解 ... [详细]
  • 本文详细介绍了如何在云服务器上配置Nginx、Tomcat、JDK和MySQL。涵盖从下载、安装到配置的完整步骤,帮助读者快速搭建Java Web开发环境。 ... [详细]
  • 探讨ChatGPT在法律和版权方面的潜在风险及影响,分析其作为内容创造工具的合法性和合规性。 ... [详细]
  • 本文详细介绍了如何在 Android 中使用值动画(ValueAnimator)来动态调整 ImageView 的高度,并探讨了相关的关键属性和方法,包括图片填充后的高度、原始图片高度、动画变化因子以及布局重置等。 ... [详细]
  • 本文将详细探讨 Java 中提供的不可变集合(如 `Collections.unmodifiableXXX`)和同步集合(如 `Collections.synchronizedXXX`)的实现原理及使用方法,帮助开发者更好地理解和应用这些工具。 ... [详细]
  • 本文探讨了如何通过一系列技术手段提升Spring Boot项目的并发处理能力,解决生产环境中因慢请求导致的系统性能下降问题。 ... [详细]
  • 本文探讨如何利用Java反射技术来模拟Webwork框架中的URL解析过程。通过这一实践,读者可以更好地理解Webwork及其后续版本Struts2的工作原理,尤其是它们在MVC架构下的角色。 ... [详细]
  • 本文详细介绍了 Kubernetes 集群管理工具 kubectl 的基本使用方法,涵盖了一系列常用的命令及其应用场景,旨在帮助初学者快速掌握 kubectl 的基本操作。 ... [详细]
  • 本文介绍了一个项目中如何在Windows平台上实现多声道音频数据的采集,特别是针对DANTE音频接口的8路立体声音频通道。文章详细描述了使用Windows底层音频API进行音频采集的方法,并提供了一个具体的实现示例。 ... [详细]
  • 本文介绍如何在Spring Boot项目中集成Redis,并通过具体案例展示其配置和使用方法。包括添加依赖、配置连接信息、自定义序列化方式以及实现仓储接口。 ... [详细]
  • 深入解析Java枚举及其高级特性
    本文详细介绍了Java枚举的概念、语法、使用规则和应用场景,并探讨了其在实际编程中的高级应用。所有相关内容已收录于GitHub仓库[JavaLearningmanual](https://github.com/Ziphtracks/JavaLearningmanual),欢迎Star并持续关注。 ... [详细]
  • 我有一个SpringRestController,它处理API调用的版本1。继承在SpringRestControllerpackagerest.v1;RestCon ... [详细]
  • 本文详细介绍了Linux内核中misc设备驱动框架的实现原理及应用方法,包括misc设备的基本概念、驱动框架的初始化过程、数据结构分析以及设备的注册与注销流程。 ... [详细]
  • 随着技术社区的发展,越来越多的技术爱好者选择通过撰写博客来分享自己的学习经验和项目进展。本文将介绍一个具体案例,即将一套原本运行于Windows平台的代码成功移植到Linux(Redhat)环境下的过程与挑战。 ... [详细]
  • 本文探讨了如何在Android应用中实现图片的保存至外部存储,并通过原生方式分享这些图片。主要介绍了保存图片的不同策略以及通过Intent进行文件分享的具体步骤。 ... [详细]
author-avatar
刘刘刘存乐_626
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有